@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,1129 +0,0 @@
1
- /**
2
- * Message list component — renders paginated message rows.
3
- * Reads from message-state; operations mutate state, list reacts.
4
- */
5
-
6
- import { getMessages as apiGetMessages, getUnifiedInbox as apiGetUnifiedInbox, searchMessages, abortMessageListRequests, updateFlags, getThreadMessages, moveMessages as apiMoveMessages } from "../lib/api-client.js";
7
- import * as state from "../lib/message-state.js";
8
- import type { ListMessage } from "../lib/message-state.js";
9
- import { showContextMenu, type MenuItem } from "./context-menu.js";
10
- import { pickFolder } from "./folder-picker.js";
11
-
12
- type MessageSelectHandler = (accountId: string, uid: number, folderId: number) => void;
13
-
14
- let onMessageSelect: MessageSelectHandler;
15
- let currentAccountId: string;
16
- let currentFolderId: number;
17
- let currentSpecialUse = ""; // Cached for reloads — an empty value on reload used to reset Sent/Drafts/Outbox to "From"
18
- let lastClickedRow: HTMLElement | null = null;
19
- let currentPage: number;
20
- let totalMessages: number;
21
- let loading = false;
22
- let unifiedMode = false;
23
- let searchMode = false;
24
- let currentSearchQuery = "";
25
- let showToInsteadOfFrom = false;
26
- let touchWasScroll = false;
27
- // Current sort column/direction — cycled by clicking the ml-header columns.
28
- // "date desc" is the default (newest first). Clicking a column flips direction
29
- // if it's already active, or switches to that column with its own default dir
30
- // (text columns default asc, date defaults desc).
31
- let currentSort = "date";
32
- let currentSortDir: "asc" | "desc" = "desc";
33
-
34
- /** S56 refactor slice — per-row focus/unfocus.
35
- *
36
- * Every rendered message row has an associated Row record tracking the DOM
37
- * element + account/msg pair. `focusRow` runs the atomic transition:
38
- *
39
- * 1. unfocus the previously-focused row (clears `.selected` class)
40
- * 2. mark the new row `.selected`
41
- * 3. update shared state.selected + notify viewer
42
- *
43
- * The viewer's `showMessageGeneration` token still cancels stale body
44
- * fetches during the transition — that's the "async cancellation" piece of
45
- * S56. The "atomic DOM transition" piece is now owned here.
46
- *
47
- * Full abort-signal plumbing through getMessage → fetchMessageBody is a
48
- * separate follow-up; for now the gen token does the job. */
49
- let currentFocusedRow: HTMLElement | null = null;
50
-
51
- function focusRow(row: HTMLElement, accountId: string, msg: any): void {
52
- if (currentFocusedRow && currentFocusedRow !== row) {
53
- // Unfocus the previous atomically — clearing .selected AND
54
- // triggering the viewer's stale-fetch cancel (via the bump inside
55
- // the next showMessage() call).
56
- currentFocusedRow.classList.remove("selected");
57
- }
58
- if (!row.classList.contains("selected")) row.classList.add("selected");
59
- currentFocusedRow = row;
60
- state.select(msg);
61
- onMessageSelect(accountId, msg.uid, msg.folderId);
62
- }
63
-
64
- /** Back-compat shim — some call sites still use `focusMessage(accountId, msg)`
65
- * without a DOM row (e.g. thread-popup click). Fall back to the previous
66
- * behavior (state + viewer only) so nothing regresses. */
67
- function focusMessage(accountId: string, msg: any): void {
68
- state.select(msg);
69
- onMessageSelect(accountId, msg.uid, msg.folderId);
70
- // Clear the focused-row reference since we don't have a DOM row here
71
- // — the next row click will unfocus whatever was selected anyway.
72
- if (currentFocusedRow) currentFocusedRow.classList.remove("selected");
73
- currentFocusedRow = null;
74
- }
75
-
76
- /** Flip the "not-downloaded" indicator off for rows whose bodies just cached.
77
- * Called from the bodyCached service event — covers both background prefetch
78
- * and on-demand fetch. No-op for rows not currently rendered. */
79
- export function markBodiesCached(items: { accountId: string; uid: number }[]): void {
80
- const body = document.getElementById("ml-body");
81
- if (!body || items.length === 0) return;
82
- for (const { accountId, uid } of items) {
83
- const row = body.querySelector(`.ml-row[data-uid="${uid}"][data-account-id="${CSS.escape(accountId)}"]`)
84
- || body.querySelector(`.ml-row[data-uid="${uid}"]`);
85
- if (row) row.classList.remove("not-downloaded");
86
- }
87
- }
88
-
89
- /** Get all selected message rows */
90
- export function getSelectedMessages(): { accountId: string; uid: number; folderId: number }[] {
91
- const body = document.getElementById("ml-body");
92
- if (!body) return [];
93
- const rows = body.querySelectorAll(".ml-row.selected");
94
- return Array.from(rows).map(r => ({
95
- accountId: (r as HTMLElement).dataset.accountId || "",
96
- uid: Number((r as HTMLElement).dataset.uid),
97
- folderId: Number((r as HTMLElement).dataset.folderId),
98
- }));
99
- }
100
-
101
- function clearSelection(): void {
102
- const body = document.getElementById("ml-body");
103
- if (body) body.querySelectorAll(".ml-row.selected").forEach(r => r.classList.remove("selected"));
104
- // S56 seam: the focused-row invariant is "the Row whose .selected is
105
- // currently mine". clearSelection wipes all .selected, so the invariant
106
- // would break if we kept a stale pointer.
107
- currentFocusedRow = null;
108
- }
109
-
110
- /** Deterministic sender-avatar color from a seed string (typically the
111
- * email address). Hash → hue at 12 evenly-spaced positions on the wheel.
112
- * Saturation + lightness fixed so all colors carry the same visual weight
113
- * regardless of hue, and so light/dark themes don't have to override. */
114
- function senderColor(seed: string): string {
115
- let h = 0;
116
- for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) | 0;
117
- const hue = ((Math.abs(h) % 12) * 30) + 15; // 15, 45, 75, …, 345
118
- return `oklch(0.62 0.14 ${hue})`;
119
- }
120
-
121
- /** Exit multi-select mode (entered via touch long-press). Clears selection
122
- * and the sticky body flag so subsequent taps open messages again. */
123
- function exitMultiSelect(): void {
124
- const body = document.getElementById("ml-body");
125
- if (!body?.classList.contains("multi-select-on")) return;
126
- body.classList.remove("multi-select-on");
127
- clearSelection();
128
- updateBulkBar();
129
- }
130
-
131
- /** Bulk-actions bar retired 2026-04-24 — trash + spam live on the main
132
- * toolbar now and every other bulk op (mark-read, flag, move) is one
133
- * right-click away. Kept as a no-op stub so existing call sites
134
- * (avatar tap, row click, long-press) don't need to be touched. */
135
- function updateBulkBar(): void { /* bar removed; nothing to render */ }
136
-
137
- // Escape key + click-outside-list exit multi-select mode. Attached once
138
- // (idempotent because document only has one listener scope per handler).
139
- if (!(window as any).__mailxMultiSelectWired) {
140
- (window as any).__mailxMultiSelectWired = true;
141
- document.addEventListener("keydown", (e) => {
142
- if (e.key === "Escape") exitMultiSelect();
143
- });
144
- document.addEventListener("pointerdown", (e) => {
145
- const body = document.getElementById("ml-body");
146
- if (!body?.classList.contains("multi-select-on")) return;
147
- const target = e.target as HTMLElement;
148
- // A tap on a row is handled by the row's own click listener.
149
- // The toolbar must also be exempt: its trash / spam / etc.
150
- // buttons operate ON the current multi-selection, so a tap on
151
- // them should NOT clear selection before the button's click
152
- // handler runs (otherwise getSelectedMessages returns empty
153
- // and the action no-ops — Android-reported 2026-04-30: "press
154
- // multiple circles, press trashcan, checks vanish, nothing
155
- // deleted"). Same logic for the folder-tree (drop targets,
156
- // future: bulk move). Exit only on a tap to genuine neutral
157
- // ground.
158
- // Exempt: rows (handled by their own listener), toolbar buttons
159
- // (delete/spam/etc. operate ON the selection — clearing it here
160
- // empties the selection before the click runs), folder-tree
161
- // (drop targets / future bulk move), and the context menu
162
- // (right-click → "mark read" / "move to" / etc. all need the
163
- // selection intact when the menu item runs).
164
- if (target.closest(".ml-row, .toolbar, .folder-tree, .ctx-menu, #btn-tb-delete, #btn-tb-spam")) return;
165
- exitMultiSelect();
166
- }, true);
167
- }
168
-
169
- function selectRange(from: HTMLElement, to: HTMLElement): void {
170
- const body = document.getElementById("ml-body");
171
- if (!body) return;
172
- const rows = Array.from(body.querySelectorAll(".ml-row"));
173
- const fromIdx = rows.indexOf(from);
174
- const toIdx = rows.indexOf(to);
175
- if (fromIdx < 0 || toIdx < 0) return;
176
- const lo = Math.min(fromIdx, toIdx);
177
- const hi = Math.max(fromIdx, toIdx);
178
- for (let i = lo; i <= hi; i++) rows[i].classList.add("selected");
179
- }
180
-
181
- /** The row to anchor a shift-click range against. `lastClickedRow` is the
182
- * primary anchor, but it can become a detached DOM node after a list
183
- * re-render (folder switch, sort, search reload, paging) — `selectRange`
184
- * would then no-op. Fall back to whichever live row is `.selected` (the
185
- * one in the viewer) before giving up. */
186
- function resolveShiftAnchor(): HTMLElement | null {
187
- if (lastClickedRow?.isConnected) return lastClickedRow;
188
- const body = document.getElementById("ml-body");
189
- if (!body) return null;
190
- return body.querySelector(".ml-row.selected") as HTMLElement | null;
191
- }
192
-
193
- const timeFmt: Intl.DateTimeFormatOptions = { hour: "2-digit", minute: "2-digit", hour12: false };
194
- const dateFmt: Intl.DateTimeFormatOptions = { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false };
195
- const dateFmtSameYear: Intl.DateTimeFormatOptions = { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false };
196
-
197
- export function initMessageList(handler: MessageSelectHandler): void {
198
- onMessageSelect = handler;
199
-
200
- // Infinite scroll
201
- const body = document.getElementById("ml-body");
202
- if (body) {
203
- // Touch scroll vs tap: the WebView occasionally synthesizes a click on
204
- // touchend even when the user clearly scrolled, which opened a message
205
- // just from swiping the list. Multi-signal detection so a scroll is
206
- // reliably classified:
207
- // 1. touchmove movement ≥ TAP_SLOP — the primary signal
208
- // 2. actual scrollTop change between touchstart and touchend — always
209
- // set the flag when the container moved, even if touchmove never
210
- // fired (some Android builds coalesce events under momentum)
211
- // 3. longer TAP_SLOP (15px) — fingers are wide; 10px was too twitchy
212
- let touchStartY = 0;
213
- let touchStartX = 0;
214
- let touchStartScrollTop = 0;
215
- const TAP_SLOP = 15;
216
- body.addEventListener("touchstart", (e) => {
217
- const t = e.touches[0];
218
- touchStartY = t.clientY;
219
- touchStartX = t.clientX;
220
- touchStartScrollTop = body.scrollTop;
221
- touchWasScroll = false;
222
- }, { passive: true });
223
- body.addEventListener("touchmove", (e) => {
224
- const t = e.touches[0];
225
- if (Math.abs(t.clientY - touchStartY) > TAP_SLOP || Math.abs(t.clientX - touchStartX) > TAP_SLOP) {
226
- touchWasScroll = true;
227
- }
228
- }, { passive: true });
229
- body.addEventListener("touchend", () => {
230
- // If the container actually scrolled during this touch, the user
231
- // was scrolling regardless of how small their finger movement was.
232
- if (body.scrollTop !== touchStartScrollTop) {
233
- touchWasScroll = true;
234
- }
235
- }, { passive: true });
236
-
237
- body.addEventListener("scroll", () => {
238
- if (loading) return;
239
- const nearBottom = body.scrollHeight - body.scrollTop - body.clientHeight < 200;
240
- if (nearBottom && currentPage * 50 < totalMessages) {
241
- loadMoreMessages();
242
- }
243
- });
244
- }
245
-
246
- // Subscribe to state changes — react to removeMessages (move/delete)
247
- state.subscribe((change) => {
248
- if (change === "removed") {
249
- syncDomToState();
250
- }
251
- });
252
-
253
- // Sort column headers — click to cycle. Date defaults desc (newest first);
254
- // From/Subject default asc on first click so alphabetical order reads
255
- // naturally. Clicking the currently-active column flips direction.
256
- const header = document.getElementById("ml-header");
257
- if (header) {
258
- header.addEventListener("click", (e) => {
259
- const col = (e.target as HTMLElement).closest<HTMLElement>(".ml-col-sortable");
260
- if (!col) return;
261
- const key = col.dataset.sort;
262
- if (!key) return;
263
- if (currentSort === key) {
264
- currentSortDir = currentSortDir === "asc" ? "desc" : "asc";
265
- } else {
266
- currentSort = key;
267
- currentSortDir = key === "date" ? "desc" : "asc";
268
- }
269
- // Only per-folder lists support server-side sort today; unified and
270
- // search paths sort client-side on the fetched page. Reload if we
271
- // have an active per-folder context.
272
- if (!searchMode && !unifiedMode && currentAccountId && currentFolderId) {
273
- loadMessages(currentAccountId, currentFolderId, 1, "", false);
274
- } else {
275
- applyClientSideSort();
276
- updateSortIndicators();
277
- }
278
- });
279
- }
280
- updateSortIndicators();
281
- }
282
-
283
- /** Reorder currently-loaded state messages in-place by currentSort/currentSortDir.
284
- * Used for unified-inbox and search results where the server can't re-sort
285
- * a single page on our behalf. */
286
- function applyClientSideSort(): void {
287
- const items = [...state.getMessages()];
288
- const sign = currentSortDir === "asc" ? 1 : -1;
289
- items.sort((a: any, b: any) => {
290
- if (currentSort === "from") {
291
- const av = (a.from?.name || a.from?.address || "").toLowerCase();
292
- const bv = (b.from?.name || b.from?.address || "").toLowerCase();
293
- return av < bv ? -sign : av > bv ? sign : 0;
294
- }
295
- if (currentSort === "subject") {
296
- const av = (a.subject || "").replace(/^(re:|fwd:|fw:)\s*/i, "").toLowerCase();
297
- const bv = (b.subject || "").replace(/^(re:|fwd:|fw:)\s*/i, "").toLowerCase();
298
- return av < bv ? -sign : av > bv ? sign : 0;
299
- }
300
- // date
301
- return ((a.date || 0) - (b.date || 0)) * sign;
302
- });
303
- state.setMessages(items as any);
304
- }
305
-
306
- function updateSortIndicators(): void {
307
- const header = document.getElementById("ml-header");
308
- if (!header) return;
309
- header.querySelectorAll<HTMLElement>(".ml-col-sortable").forEach(c => {
310
- c.classList.remove("ml-col-sort-asc", "ml-col-sort-desc");
311
- if (c.dataset.sort === currentSort) {
312
- c.classList.add(currentSortDir === "asc" ? "ml-col-sort-asc" : "ml-col-sort-desc");
313
- }
314
- });
315
- }
316
-
317
- /**
318
- * Sync DOM rows to current state after messages are removed.
319
- * Removes DOM rows that are no longer in state, updates selection.
320
- */
321
- function syncDomToState(): void {
322
- const body = document.getElementById("ml-body");
323
- if (!body) return;
324
-
325
- // Build set of UIDs still in state
326
- const stateUids = new Set(state.getMessages().map(m => `${m.accountId}:${m.uid}`));
327
-
328
- // Remove rows not in state
329
- for (const row of Array.from(body.querySelectorAll(".ml-row"))) {
330
- const el = row as HTMLElement;
331
- const key = `${el.dataset.accountId}:${el.dataset.uid}`;
332
- if (!stateUids.has(key)) {
333
- el.remove();
334
- }
335
- }
336
-
337
- // Update selection to match state
338
- clearSelection();
339
- const sel = state.getSelected();
340
- if (sel) {
341
- const row = body.querySelector(`.ml-row[data-uid="${sel.uid}"][data-account-id="${sel.accountId}"]`) as HTMLElement
342
- || body.querySelector(`.ml-row[data-uid="${sel.uid}"]`) as HTMLElement;
343
- if (row) {
344
- row.classList.add("selected");
345
- lastClickedRow = row;
346
- // Trigger viewer update
347
- onMessageSelect(sel.accountId, sel.uid, sel.folderId);
348
- }
349
- }
350
-
351
- // If no messages left, show empty
352
- if (state.getMessages().length === 0) {
353
- body.innerHTML = `<div class="ml-empty">No messages</div>`;
354
- }
355
- }
356
-
357
- /** Reload the currently displayed folder (preserves current selection) */
358
- export function reloadCurrentFolder(): void {
359
- if (searchMode) {
360
- loadSearchResults(currentSearchQuery);
361
- } else if (unifiedMode) {
362
- loadUnifiedInbox(false);
363
- } else if (currentAccountId && currentFolderId) {
364
- loadMessages(currentAccountId, currentFolderId, 1, "", false);
365
- }
366
- }
367
-
368
- /** Exit search mode without triggering a reload — caller decides what to load
369
- * next. Used by the search-input "cleared to empty" handler so the next
370
- * reloadCurrentFolder() call doesn't re-run the stale search query. */
371
- export function clearSearchMode(): void {
372
- searchMode = false;
373
- currentSearchQuery = "";
374
- }
375
-
376
- /** Load unified inbox (all accounts) */
377
- export async function loadUnifiedInbox(autoSelect = true): Promise<void> {
378
- unifiedMode = true;
379
- searchMode = false;
380
- currentSpecialUse = "";
381
- showToInsteadOfFrom = false; // Unified inbox always shows From, not To
382
- currentPage = 1;
383
- totalMessages = 0;
384
-
385
- const body = document.getElementById("ml-body");
386
- if (!body) return;
387
- const fromHeader = document.querySelector(".ml-col-from");
388
- if (fromHeader) fromHeader.textContent = "From";
389
-
390
- const savedScroll = !autoSelect ? body.scrollTop : 0;
391
- const savedUid = !autoSelect ? body.querySelector(".ml-row.selected")?.getAttribute("data-uid") : null;
392
-
393
- if (autoSelect) {
394
- body.innerHTML = `<div class="ml-empty">Loading...</div>`;
395
- }
396
-
397
- try {
398
- const result = await apiGetUnifiedInbox(1);
399
- totalMessages = result.total;
400
-
401
- if (result.items.length === 0) {
402
- state.setMessages([]);
403
- body.innerHTML = `<div class="ml-empty">${result.total > 0 ? `${result.total} messages syncing...` : "Syncing — messages will appear shortly"}</div>`;
404
- return;
405
- }
406
-
407
- state.setMessages(result.items);
408
- renderMessages(body, "", result.items);
409
-
410
- if (autoSelect) {
411
- selectFirst(body);
412
- } else {
413
- body.scrollTop = savedScroll;
414
- restoreSelection(body, savedUid);
415
- }
416
- } catch (e: any) {
417
- if (e.name === "AbortError") return;
418
- body.innerHTML = `<div class="ml-empty">Error: ${e.message}</div>`;
419
- }
420
- }
421
-
422
- /** Load search results */
423
- export async function loadSearchResults(query: string, scope = "all", accountId = "", folderId = 0): Promise<void> {
424
- searchMode = true;
425
- unifiedMode = false;
426
- currentSearchQuery = query;
427
- currentPage = 1;
428
- totalMessages = 0;
429
-
430
- const body = document.getElementById("ml-body");
431
- if (!body) return;
432
-
433
- // Clear the preview pane — old preview from the prior selection lingers
434
- // until the user clicks a search result. Atomic clear-on-list-mutation
435
- // is what Slice D's row-objects-own-preview will do generally; this is
436
- // the band-aid for the search-specific case until that lands.
437
- document.dispatchEvent(new CustomEvent("mailx-clear-viewer"));
438
-
439
- body.innerHTML = `<div class="ml-empty">Searching...</div>`;
440
-
441
- try {
442
- // Regex search: filter client-side
443
- if (query.startsWith("/") && query.endsWith("/") && query.length > 2) {
444
- const pattern = query.slice(1, -1);
445
- let regex: RegExp;
446
- try { regex = new RegExp(pattern, "i"); } catch { body.innerHTML = `<div class="ml-empty">Invalid regex</div>`; return; }
447
-
448
- const source = scope === "current" && accountId
449
- ? await apiGetMessages(accountId, folderId, 1, 10000)
450
- : await apiGetUnifiedInbox(1, 10000);
451
- const matches = source.items.filter((m: any) =>
452
- regex.test(m.subject || "") || regex.test(m.from?.name || "") || regex.test(m.from?.address || "") || regex.test(m.preview || "")
453
- );
454
- totalMessages = matches.length;
455
- state.setMessages(matches);
456
- if (matches.length === 0) { body.innerHTML = `<div class="ml-empty">No regex matches</div>`; return; }
457
- body.innerHTML = "";
458
- appendMessages(body, "", matches);
459
- selectFirst(body);
460
- return;
461
- }
462
-
463
- const result = await searchMessages(query, 1, 50, scope, accountId, folderId);
464
- totalMessages = result.total;
465
-
466
- if (result.items.length === 0) {
467
- state.setMessages([]);
468
- body.innerHTML = `<div class="ml-empty">No results for "${query}"</div>`;
469
- return;
470
- }
471
-
472
- state.setMessages(result.items);
473
- body.innerHTML = "";
474
- appendMessages(body, "", result.items);
475
- } catch (e: any) {
476
- body.innerHTML = `<div class="ml-empty">Search error: ${e.message}</div>`;
477
- }
478
- }
479
-
480
- export async function loadMessages(accountId: string, folderId: number, page = 1, specialUse = "", autoSelect = true): Promise<void> {
481
- searchMode = false;
482
- unifiedMode = false;
483
- // Folder switch clears any in-progress multi-select — carrying a "3
484
- // selected" state across folders would lie about what rows the bulk
485
- // buttons would act on.
486
- exitMultiSelect();
487
- // specialUse is either the DB tag ("sent"/"drafts"/"outbox") or the
488
- // folder path lowercased (folder-tree fallback when tag is missing — common
489
- // on Dovecot which doesn't advertise \Sent). Match both cases.
490
- // Empty specialUse on reload means "keep what we had" — otherwise a
491
- // folderCountsChanged event or sort-header click resets Sent/Drafts/Outbox
492
- // back to showing From (user-reported regression 2026-04-24).
493
- if (specialUse) currentSpecialUse = specialUse;
494
- const su = currentSpecialUse.toLowerCase();
495
- showToInsteadOfFrom = su === "sent" || su === "drafts" || su === "outbox"
496
- || su.endsWith("sent") || su.endsWith("drafts") || su.endsWith("outbox")
497
- || su === "sent items" || su === "sent mail" || su.endsWith("/sent items") || su.endsWith(".sent items");
498
- currentAccountId = accountId;
499
- currentFolderId = folderId;
500
- currentPage = 1;
501
- totalMessages = 0;
502
-
503
- const body = document.getElementById("ml-body");
504
- if (!body) return;
505
-
506
- // Update header label
507
- const fromHeader = document.querySelector(".ml-col-from");
508
- if (fromHeader) fromHeader.textContent = showToInsteadOfFrom ? "To" : "From";
509
-
510
- const savedScroll = !autoSelect ? body.scrollTop : 0;
511
- const savedUid = !autoSelect ? body.querySelector(".ml-row.selected")?.getAttribute("data-uid") : null;
512
-
513
- if (autoSelect) {
514
- body.innerHTML = `<div class="ml-empty">Loading...</div>`;
515
- }
516
-
517
- try {
518
- const flaggedOnly = document.getElementById("ml-body")?.classList.contains("flagged-only") || false;
519
- const result = await apiGetMessages(accountId, folderId, 1, 50, flaggedOnly, currentSort, currentSortDir);
520
- totalMessages = result.total;
521
- updateSortIndicators();
522
-
523
- if (result.items.length === 0) {
524
- state.setMessages([]);
525
- body.innerHTML = `<div class="ml-empty">${flaggedOnly ? "No flagged messages" : "No messages"}</div>`;
526
- return;
527
- }
528
-
529
- state.setMessages(result.items);
530
- renderMessages(body, accountId, result.items);
531
-
532
- if (autoSelect) {
533
- selectFirst(body);
534
- } else {
535
- requestAnimationFrame(() => {
536
- body.scrollTop = savedScroll;
537
- restoreSelection(body, savedUid);
538
- });
539
- }
540
- } catch (e: any) {
541
- if (e.name === "AbortError") return;
542
- body.innerHTML = `<div class="ml-empty">Error: ${e.message}</div>`;
543
- }
544
- }
545
-
546
- async function loadMoreMessages(): Promise<void> {
547
- const body = document.getElementById("ml-body");
548
- if (!body) return;
549
-
550
- loading = true;
551
- currentPage++;
552
-
553
- try {
554
- const flaggedOnly = body.classList.contains("flagged-only");
555
- const result = searchMode
556
- ? await searchMessages(currentSearchQuery, currentPage)
557
- : unifiedMode
558
- ? await apiGetUnifiedInbox(currentPage)
559
- : await apiGetMessages(currentAccountId, currentFolderId, currentPage, 50, flaggedOnly);
560
- // Append to state
561
- const current = state.getMessages();
562
- state.setMessages([...current, ...result.items]);
563
- appendMessages(body, unifiedMode ? "" : currentAccountId, result.items);
564
- } catch (e: any) {
565
- console.error(`Load more error: ${e.message}`);
566
- } finally {
567
- loading = false;
568
- }
569
- }
570
-
571
- /** Replace body contents with rendered rows */
572
- function renderMessages(body: HTMLElement, accountId: string, items: any[]): void {
573
- const fragment = document.createDocumentFragment();
574
- const tempDiv = document.createElement("div");
575
- appendMessages(tempDiv, accountId, items);
576
- while (tempDiv.firstChild) fragment.appendChild(tempDiv.firstChild);
577
- body.replaceChildren(fragment);
578
- }
579
-
580
- function selectFirst(body: HTMLElement): void {
581
- // Narrow viewports (Android, phone-sized): don't auto-select. The
582
- // click handler in app.ts switches the layout to "narrow-active" on
583
- // any list-row click, which on a phone means the message viewer takes
584
- // over the screen and hides the list. Auto-selecting at startup
585
- // therefore lands the user in the LAST letter they read instead of
586
- // the inbox summary they wanted. Desktop unchanged — auto-select
587
- // remains useful when the list and viewer are side-by-side.
588
- if (window.innerWidth <= 768) return;
589
- const firstRow = body.querySelector(".ml-row") as HTMLElement;
590
- if (firstRow) firstRow.click();
591
- }
592
-
593
- function restoreSelection(body: HTMLElement, savedUid: string | null | undefined): void {
594
- if (savedUid) {
595
- const row = body.querySelector(`.ml-row[data-uid="${savedUid}"]`) as HTMLElement;
596
- if (row) row.classList.add("selected");
597
- }
598
- }
599
-
600
- /** Show a floating list of all messages in a thread when the pill is clicked.
601
- * Each entry in the popup selects that message in the viewer when clicked.
602
- * This is simpler than inline expansion and avoids duplicating the row builder. */
603
- export async function showThreadPopup(pillEl: HTMLElement, headMsg: any): Promise<void> {
604
- // Remove any existing popup
605
- document.querySelectorAll(".ml-thread-popup").forEach(el => el.remove());
606
- let thread: any[] = [];
607
- try { thread = await getThreadMessages(headMsg.accountId, headMsg.threadId); } catch { /* ignore */ }
608
- if (!thread || thread.length === 0) return;
609
- thread.sort((a, b) => (a.date || 0) - (b.date || 0));
610
- const popup = document.createElement("div");
611
- popup.className = "ml-thread-popup";
612
- for (const msg of thread) {
613
- const item = document.createElement("div");
614
- item.className = "ml-thread-popup-item";
615
- if (!msg.flags.includes("\\Seen")) item.classList.add("unread");
616
- const from = document.createElement("span");
617
- from.className = "ml-thread-popup-from";
618
- from.textContent = msg.from?.name || msg.from?.address || "?";
619
- const date = document.createElement("span");
620
- date.className = "ml-thread-popup-date";
621
- date.textContent = formatDate(msg.date);
622
- const subject = document.createElement("span");
623
- subject.className = "ml-thread-popup-subject";
624
- subject.textContent = msg.subject || "(no subject)";
625
- item.appendChild(from);
626
- item.appendChild(date);
627
- item.appendChild(subject);
628
- item.addEventListener("click", async () => {
629
- focusMessage(msg.accountId, { accountId: msg.accountId, uid: msg.uid, folderId: msg.folderId, subject: msg.subject, from: msg.from, to: msg.to, cc: msg.cc, date: msg.date, flags: msg.flags, size: msg.size, preview: msg.preview, hasAttachments: msg.hasAttachments });
630
- popup.remove();
631
- });
632
- popup.appendChild(item);
633
- }
634
- document.body.appendChild(popup);
635
- const rect = pillEl.getBoundingClientRect();
636
- popup.style.left = `${rect.left}px`;
637
- popup.style.top = `${rect.bottom + 4}px`;
638
- // Dismiss on outside click
639
- setTimeout(() => {
640
- const dismiss = (e: MouseEvent) => {
641
- if (!popup.contains(e.target as Node)) {
642
- popup.remove();
643
- document.removeEventListener("mousedown", dismiss, true);
644
- }
645
- };
646
- document.addEventListener("mousedown", dismiss, true);
647
- }, 0);
648
- }
649
-
650
- function appendMessages(body: HTMLElement, accountId: string, items: any[]): void {
651
- // Thread grouping: when the list has the "threaded" class, collapse messages
652
- // sharing the same threadId to a single row showing the most recent message,
653
- // with a small pill indicating the thread size. Pre-threading messages have
654
- // no threadId — those are treated as singletons keyed by their own uid.
655
- const threaded = body.classList.contains("threaded");
656
- let rowsToRender: any[] = items;
657
- let threadSize: Map<any, number> | null = null;
658
- if (threaded) {
659
- const threadMap = new Map<string, any>(); // threadId → newest msg
660
- threadSize = new Map<any, number>();
661
- for (const msg of items) {
662
- const key = msg.threadId || `_msg_${msg.accountId || accountId}_${msg.uid}`;
663
- const existing = threadMap.get(key);
664
- if (!existing || (msg.date || 0) > (existing.date || 0)) {
665
- threadMap.set(key, msg);
666
- }
667
- }
668
- // Count messages per thread
669
- for (const msg of items) {
670
- const key = msg.threadId || `_msg_${msg.accountId || accountId}_${msg.uid}`;
671
- const head = threadMap.get(key);
672
- if (head) threadSize.set(head, (threadSize.get(head) || 0) + 1);
673
- }
674
- rowsToRender = Array.from(threadMap.values()).sort((a, b) => (b.date || 0) - (a.date || 0));
675
- }
676
- for (const msg of rowsToRender) {
677
- const msgAccountId = msg.accountId || accountId;
678
- const row = document.createElement("div");
679
- row.className = "ml-row";
680
- row.draggable = true;
681
- if (!msg.flags.includes("\\Seen")) row.classList.add("unread");
682
- if (msg.flags.includes("\\Flagged")) row.classList.add("flagged");
683
- if (!msg.bodyPath) row.classList.add("not-downloaded");
684
- // Pink-row visible reconciliation state (S1 slice C): a queued local
685
- // action (move/flag/delete) hasn't been ACK'd by the server yet.
686
- if (msg.pending) row.classList.add("pending-reconcile");
687
- // Reply-row marker: messages with In-Reply-To are replies. Shows a
688
- // subtle left-border accent so the eye can pick out threaded replies
689
- // without enabling full thread grouping.
690
- if (msg.inReplyTo) row.classList.add("is-reply");
691
- row.dataset.uid = String(msg.uid);
692
- row.dataset.accountId = msgAccountId;
693
- row.dataset.folderId = String(msg.folderId);
694
- if (msg.threadId) row.dataset.threadId = msg.threadId;
695
-
696
- // Sender avatar \u2014 Thunderbird-style colored circle with the first
697
- // initial of the sender's display name. Doubles as the multi-select
698
- // affordance: in `multi-select-on` mode, the avatar swaps to a
699
- // checkmark via CSS. Color is derived deterministically from the
700
- // address so the same sender keeps the same color across rows.
701
- const fromName = (showToInsteadOfFrom && msg.to?.length)
702
- ? (msg.to[0].name || msg.to[0].address || "?")
703
- : (msg.from?.name || msg.from?.address || "?");
704
- const seedAddr = (msg.from?.address || msg.from?.name || "?").toLowerCase();
705
- const initial = (fromName.replace(/^[\W_]+/, "") || "?").charAt(0).toUpperCase();
706
- const avatar = document.createElement("span");
707
- avatar.className = "ml-avatar";
708
- avatar.textContent = initial;
709
- avatar.style.background = senderColor(seedAddr);
710
- avatar.title = msg.from?.address || "";
711
- // Tapping the avatar enters multi-select mode (or toggles in it,
712
- // mirroring Thunderbird/Gmail). Click bubbles to the row otherwise,
713
- // which would open the message — stopPropagation here keeps the
714
- // avatar a dedicated selection affordance.
715
- avatar.addEventListener("click", (e) => {
716
- e.stopPropagation();
717
- const body = document.getElementById("ml-body");
718
- if (!body) return;
719
- if (body.classList.contains("multi-select-on")) {
720
- row.classList.toggle("selected");
721
- } else {
722
- clearSelection();
723
- row.classList.add("selected");
724
- body.classList.add("multi-select-on");
725
- }
726
- lastClickedRow = row;
727
- updateBulkBar();
728
- });
729
-
730
- // Right-click (or long-press) on the avatar → bulk-selection menu.
731
- // Putting it on the avatar is contextually right: the avatar is the
732
- // "select" affordance, so its menu owns operations on the selection
733
- // set. "Select all visible" is the load-bearing item — there's no
734
- // Ctrl-A equivalent on touch and the scope-after-search use case
735
- // demands it.
736
- avatar.addEventListener("contextmenu", async (e) => {
737
- e.preventDefault();
738
- e.stopPropagation();
739
- const { showContextMenu } = await import("./context-menu.js");
740
- const body = document.getElementById("ml-body");
741
- const visibleRows = body
742
- ? Array.from(body.querySelectorAll<HTMLElement>(".ml-row:not(.filter-hidden)"))
743
- : [];
744
- const selectedCount = body
745
- ? body.querySelectorAll(".ml-row.selected").length
746
- : 0;
747
- showContextMenu(e.clientX, e.clientY, [
748
- {
749
- label: `Select all (${visibleRows.length})`,
750
- action: () => {
751
- if (!body) return;
752
- body.classList.add("multi-select-on");
753
- for (const r of visibleRows) r.classList.add("selected");
754
- lastClickedRow = visibleRows[visibleRows.length - 1] || null;
755
- updateBulkBar();
756
- },
757
- disabled: visibleRows.length === 0,
758
- },
759
- {
760
- label: `Clear selection${selectedCount ? ` (${selectedCount})` : ""}`,
761
- action: () => exitMultiSelect(),
762
- disabled: selectedCount === 0,
763
- },
764
- {
765
- label: "Invert selection",
766
- action: () => {
767
- if (!body) return;
768
- body.classList.add("multi-select-on");
769
- for (const r of visibleRows) r.classList.toggle("selected");
770
- lastClickedRow = visibleRows[visibleRows.length - 1] || null;
771
- updateBulkBar();
772
- },
773
- disabled: visibleRows.length === 0,
774
- },
775
- ]);
776
- });
777
-
778
- const flag = document.createElement("span");
779
- flag.className = "ml-flag";
780
- flag.textContent = msg.flags.includes("\\Flagged") ? "\u2605" : "\u2606";
781
- flag.title = "Toggle flag";
782
-
783
- const from = document.createElement("span");
784
- from.className = "ml-from";
785
- if (showToInsteadOfFrom && msg.to?.length) {
786
- from.textContent = msg.to.map((a: any) => a.name || a.address).join(", ");
787
- } else {
788
- from.textContent = msg.from.name || msg.from.address;
789
- }
790
- if (!accountId && msgAccountId) {
791
- const tag = document.createElement("span");
792
- tag.className = "ml-account-tag";
793
- tag.textContent = msgAccountId.charAt(0).toUpperCase();
794
- tag.title = msgAccountId;
795
- from.prepend(tag);
796
- }
797
- // Search/cross-folder results carry folderName — show a tag so the user
798
- // can tell which folder each hit lives in.
799
- if (msg.folderName) {
800
- const folderTag = document.createElement("span");
801
- folderTag.className = "ml-folder-tag";
802
- folderTag.textContent = msg.folderName;
803
- folderTag.title = `In folder: ${msg.folderName}`;
804
- from.prepend(folderTag);
805
- }
806
- // Unified inbox: same Message-ID exists under >=2 accounts → ⇆ badge.
807
- // Tooltip names the count so the user knows "this appears on N".
808
- if ((msg.dupeCount as number) >= 2) {
809
- const dupe = document.createElement("span");
810
- dupe.className = "ml-dupe-tag";
811
- dupe.textContent = "⇆";
812
- dupe.title = `Same message on ${msg.dupeCount} accounts`;
813
- from.prepend(dupe);
814
- }
815
-
816
- const subject = document.createElement("span");
817
- subject.className = "ml-subject";
818
- subject.innerHTML = escapeHtml(msg.subject);
819
- // Thread size pill: click to show a popup list of the thread's messages.
820
- if (threadSize) {
821
- const n = threadSize.get(msg) || 1;
822
- if (n > 1 && msg.threadId) {
823
- row.classList.add("thread-head");
824
- row.dataset.threadId = msg.threadId;
825
- const threadPill = document.createElement("span");
826
- threadPill.className = "ml-thread-pill";
827
- threadPill.textContent = String(n);
828
- threadPill.title = `${n} messages in this thread — click to see list`;
829
- threadPill.addEventListener("click", async (e) => {
830
- e.stopPropagation();
831
- await showThreadPopup(threadPill, msg);
832
- });
833
- subject.prepend(threadPill);
834
- }
835
- }
836
- if (msg.preview) {
837
- const preview = document.createElement("span");
838
- preview.className = "ml-preview";
839
- preview.textContent = ` \u2014 ${msg.preview}`;
840
- subject.appendChild(preview);
841
- }
842
-
843
- const date = document.createElement("span");
844
- date.className = "ml-date";
845
- date.textContent = formatDate(msg.date);
846
-
847
- flag.addEventListener("click", async (e) => {
848
- e.stopPropagation();
849
- const isFlagged = row.classList.contains("flagged");
850
- const currentFlags: string[] = msg.flags || [];
851
- const newFlags = isFlagged
852
- ? currentFlags.filter((f: string) => f !== "\\Flagged")
853
- : [...currentFlags, "\\Flagged"];
854
- try {
855
- await updateFlags(msgAccountId, msg.uid, newFlags);
856
- msg.flags = newFlags;
857
- row.classList.toggle("flagged");
858
- flag.textContent = row.classList.contains("flagged") ? "\u2605" : "\u2606";
859
- } catch { /* ignore */ }
860
- });
861
-
862
- row.appendChild(avatar);
863
- row.appendChild(flag);
864
- row.appendChild(from);
865
- row.appendChild(date);
866
- row.appendChild(subject);
867
-
868
- row.addEventListener("click", (e) => {
869
- if (touchWasScroll) { touchWasScroll = false; return; }
870
- // Multi-select mode (entered via long-press on touch): taps toggle
871
- // rows instead of opening messages. Exit mode when the user taps
872
- // outside any row or presses Escape (handled at the body level).
873
- const body = row.parentElement as HTMLElement | null;
874
- if (body?.classList.contains("multi-select-on")) {
875
- row.classList.toggle("selected");
876
- lastClickedRow = row;
877
- updateBulkBar();
878
- return;
879
- }
880
- if (e.shiftKey) {
881
- const anchor = resolveShiftAnchor();
882
- if (anchor) {
883
- clearSelection();
884
- selectRange(anchor, row);
885
- lastClickedRow = row;
886
- row.classList.remove("unread");
887
- focusMessage(msgAccountId, msg);
888
- } else {
889
- // No anchor available (first click of the session, or list
890
- // was just rebuilt with no selection). Treat as a plain
891
- // click so the user gets visible feedback rather than a
892
- // silent no-op.
893
- clearSelection();
894
- focusRow(row, msgAccountId, msg);
895
- lastClickedRow = row;
896
- row.classList.remove("unread");
897
- }
898
- } else if (e.ctrlKey || e.metaKey) {
899
- row.classList.toggle("selected");
900
- lastClickedRow = row;
901
- } else {
902
- // Atomic unfocus-previous + focus-this.
903
- clearSelection();
904
- focusRow(row, msgAccountId, msg);
905
- lastClickedRow = row;
906
- row.classList.remove("unread");
907
- }
908
- updateBulkBar();
909
- });
910
-
911
- // Q64: double-click → pop out the message in a floating overlay so
912
- // the user can read it without losing the selected list context.
913
- row.addEventListener("dblclick", (e) => {
914
- e.preventDefault();
915
- e.stopPropagation();
916
- document.dispatchEvent(new CustomEvent("mailx-popout-message", {
917
- detail: { accountId: msgAccountId, uid: msg.uid, folderId: msg.folderId, subject: msg.subject }
918
- }));
919
- });
920
-
921
- row.addEventListener("dragstart", (e) => {
922
- if (!row.classList.contains("selected")) {
923
- clearSelection();
924
- row.classList.add("selected");
925
- lastClickedRow = row;
926
- }
927
- const selected = getSelectedMessages();
928
- e.dataTransfer!.setData("application/x-mailx-messages", JSON.stringify(selected));
929
- e.dataTransfer!.setData("application/x-mailx-message", JSON.stringify({
930
- accountId: msgAccountId,
931
- uid: msg.uid,
932
- folderId: msg.folderId,
933
- subject: msg.subject,
934
- }));
935
- e.dataTransfer!.effectAllowed = "copyMove";
936
- row.classList.add("dragging");
937
- if (selected.length > 1) {
938
- const badge = document.createElement("div");
939
- badge.textContent = `${selected.length} messages`;
940
- badge.style.cssText = "position:absolute;top:-1000px;background:#333;color:white;padding:4px 8px;border-radius:4px;font-size:12px";
941
- document.body.appendChild(badge);
942
- e.dataTransfer!.setDragImage(badge, 0, 0);
943
- setTimeout(() => badge.remove(), 0);
944
- }
945
- });
946
- row.addEventListener("dragend", () => row.classList.remove("dragging"));
947
-
948
- // ── Q66: long-press on touch → context menu ──
949
- // Mirrors right-click on the phone where right-click isn't a thing.
950
- // Cancelled by any touchmove or touchend before the threshold.
951
- let longPressTimer: ReturnType<typeof setTimeout> | null = null;
952
- const LONG_PRESS_MS = 550;
953
- row.addEventListener("touchstart", (e: TouchEvent) => {
954
- const t = e.touches[0];
955
- if (!t) return;
956
- const cx = t.clientX, cy = t.clientY;
957
- if (longPressTimer) clearTimeout(longPressTimer);
958
- longPressTimer = setTimeout(() => {
959
- longPressTimer = null;
960
- // Long-press semantics:
961
- // - If the list is already in multi-select mode, toggle this
962
- // row's selected state (so the user can extend a selection
963
- // without needing a second long-press-and-menu dance).
964
- // - Otherwise enter multi-select mode: mark THIS row selected
965
- // and add a sticky class on the body so future taps toggle
966
- // instead of opening messages. Tap elsewhere or press
967
- // Escape to exit.
968
- const body = row.parentElement as HTMLElement | null;
969
- const alreadyMulti = body?.classList.contains("multi-select-on");
970
- if (alreadyMulti) {
971
- row.classList.toggle("selected");
972
- } else {
973
- clearSelection();
974
- row.classList.add("selected");
975
- body?.classList.add("multi-select-on");
976
- }
977
- lastClickedRow = row;
978
- updateBulkBar();
979
- // Haptic hint if the platform supports it (Android WebView does).
980
- try { (navigator as any).vibrate?.(20); } catch { /* */ }
981
- }, LONG_PRESS_MS);
982
- }, { passive: true });
983
- const cancelLongPress = () => {
984
- if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; }
985
- };
986
- row.addEventListener("touchmove", cancelLongPress, { passive: true });
987
- row.addEventListener("touchend", cancelLongPress, { passive: true });
988
- row.addEventListener("touchcancel", cancelLongPress, { passive: true });
989
-
990
- // ── Right-click context menu ──
991
- row.addEventListener("contextmenu", (e) => {
992
- e.preventDefault();
993
- // Selection-on-context-click semantics:
994
- // - If the right-clicked row is already selected → keep the
995
- // existing selection (single or multi). The menu acts on
996
- // whatever is selected.
997
- // - If it's NOT selected and we're in multi-select mode →
998
- // ADD this row to the selection (don't clear). Long-press
999
- // to open a context menu used to wipe the multi-select set,
1000
- // which was the user-reported "annoying" behavior.
1001
- // - If it's NOT selected and we're NOT in multi-select →
1002
- // single-select this row (replace prior selection).
1003
- const body = row.parentElement as HTMLElement | null;
1004
- const inMulti = !!body?.classList.contains("multi-select-on");
1005
- if (!row.classList.contains("selected")) {
1006
- if (inMulti) {
1007
- row.classList.add("selected");
1008
- lastClickedRow = row;
1009
- } else {
1010
- clearSelection();
1011
- row.classList.add("selected");
1012
- lastClickedRow = row;
1013
- focusMessage(msgAccountId, msg);
1014
- }
1015
- }
1016
-
1017
- const isSeen = msg.flags.includes("\\Seen");
1018
- const isFlagged = msg.flags.includes("\\Flagged");
1019
-
1020
- const items: MenuItem[] = [
1021
- {
1022
- label: isSeen ? "Mark unread" : "Mark read",
1023
- action: async () => {
1024
- const newFlags = isSeen
1025
- ? msg.flags.filter((f: string) => f !== "\\Seen")
1026
- : [...msg.flags, "\\Seen"];
1027
- try {
1028
- await updateFlags(msgAccountId, msg.uid, newFlags);
1029
- msg.flags = newFlags;
1030
- state.updateMessageFlags(msgAccountId, msg.uid, newFlags);
1031
- row.classList.toggle("unread", !newFlags.includes("\\Seen"));
1032
- } catch { /* ignore */ }
1033
- },
1034
- },
1035
- {
1036
- label: isFlagged ? "Unflag" : "Flag",
1037
- action: async () => {
1038
- const newFlags = isFlagged
1039
- ? msg.flags.filter((f: string) => f !== "\\Flagged")
1040
- : [...msg.flags, "\\Flagged"];
1041
- try {
1042
- await updateFlags(msgAccountId, msg.uid, newFlags);
1043
- msg.flags = newFlags;
1044
- row.classList.toggle("flagged");
1045
- flag.textContent = row.classList.contains("flagged") ? "\u2605" : "\u2606";
1046
- } catch { /* ignore */ }
1047
- },
1048
- },
1049
- { label: "", action: () => {}, separator: true },
1050
- {
1051
- label: "Reply",
1052
- action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "reply" } })),
1053
- },
1054
- {
1055
- label: "Reply All",
1056
- action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "replyAll" } })),
1057
- },
1058
- {
1059
- label: "Forward",
1060
- action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "forward" } })),
1061
- },
1062
- { label: "", action: () => {}, separator: true },
1063
- {
1064
- label: "Move to folder…",
1065
- action: async () => {
1066
- // Move all currently-selected rows (or just this one if it's the only selection)
1067
- const selectedRows = Array.from(document.querySelectorAll(".ml-row.selected"));
1068
- const uids = selectedRows.length > 0
1069
- ? selectedRows.map((r: Element) => Number((r as HTMLElement).dataset.uid)).filter(u => !isNaN(u))
1070
- : [msg.uid];
1071
- const pick = await pickFolder(msgAccountId, { excludeFolderIds: [msg.folderId] });
1072
- if (!pick) return;
1073
- try {
1074
- await apiMoveMessages(msgAccountId, uids, pick.folderId);
1075
- // Remove from local state — reconciler handles server sync.
1076
- state.removeMessages(uids.map(u => ({ accountId: msgAccountId, uid: u })));
1077
- } catch (err: any) {
1078
- alert(`Move failed: ${err.message}`);
1079
- }
1080
- },
1081
- },
1082
- {
1083
- label: "Delete",
1084
- action: () => document.dispatchEvent(new CustomEvent("mailx-delete")),
1085
- },
1086
- { label: "", action: () => {}, separator: true },
1087
- {
1088
- label: "⚠ Mark as spam",
1089
- action: () => document.getElementById("btn-spam")?.click(),
1090
- },
1091
- { label: "", action: () => {}, separator: true },
1092
- {
1093
- label: "Copy Message-ID",
1094
- action: async () => {
1095
- // Useful when asking "where did my letter go?" — pair the
1096
- // Message-ID with the reconcile-delete log line.
1097
- if (!msg.messageId) { alert("No Message-ID on this row."); return; }
1098
- try { await navigator.clipboard.writeText(msg.messageId); } catch { /* */ }
1099
- },
1100
- },
1101
- ];
1102
-
1103
- showContextMenu(e.clientX, e.clientY, items);
1104
- });
1105
-
1106
- body.appendChild(row);
1107
- }
1108
- }
1109
-
1110
- function formatDate(epochMs: number): string {
1111
- const d = new Date(epochMs);
1112
- const now = new Date();
1113
- const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
1114
- const msgDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
1115
-
1116
- if (msgDay.getTime() === today.getTime())
1117
- return d.toLocaleTimeString(undefined, timeFmt);
1118
-
1119
- if (d.getFullYear() === now.getFullYear())
1120
- return d.toLocaleString(undefined, dateFmtSameYear);
1121
-
1122
- return d.toLocaleString(undefined, dateFmt);
1123
- }
1124
-
1125
- function escapeHtml(s: string): string {
1126
- const div = document.createElement("div");
1127
- div.textContent = s;
1128
- return div.innerHTML;
1129
- }