@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
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { getMessages as apiGetMessages, getUnifiedInbox as apiGetUnifiedInbox, searchMessages, updateFlags, getThreadMessages, moveMessages as apiMoveMessages } from "../lib/api-client.js";
6
6
  import * as state from "../lib/message-state.js";
7
+ import { showMessage as viewerShow, clearViewer as viewerClear } from "./message-viewer.js";
7
8
  import { showContextMenu } from "./context-menu.js";
8
9
  import { pickFolder } from "./folder-picker.js";
9
10
  let onMessageSelect;
@@ -25,46 +26,60 @@ let touchWasScroll = false;
25
26
  // (text columns default asc, date defaults desc).
26
27
  let currentSort = "date";
27
28
  let currentSortDir = "desc";
28
- /** S56 refactor slice per-row focus/unfocus.
29
+ /** Single source of truth for "which row is focused" in the list.
29
30
  *
30
- * Every rendered message row has an associated Row record tracking the DOM
31
- * element + account/msg pair. `focusRow` runs the atomic transition:
31
+ * Each rendered row is a `MessageRow` instance owning its DOM element,
32
+ * its message envelope, and its event handlers. `focusRow(row)` runs
33
+ * the atomic transition: unfocus the previous row, mark this one
34
+ * `.selected`, drive the viewer with the row's envelope, dispatch
35
+ * `mailx-focus-changed`. There is no "select state" anywhere else; the
36
+ * viewer has no subscriptions. If `focusRow` isn't called, the preview
37
+ * pane doesn't update — drift between the highlighted row and the
38
+ * preview is structurally impossible.
32
39
  *
33
- * 1. unfocus the previously-focused row (clears `.selected` class)
34
- * 2. mark the new row `.selected`
35
- * 3. update shared state.selected + notify viewer
36
- *
37
- * The viewer's `showMessageGeneration` token still cancels stale body
38
- * fetches during the transition — that's the "async cancellation" piece of
39
- * S56. The "atomic DOM transition" piece is now owned here.
40
- *
41
- * Full abort-signal plumbing through getMessage → fetchMessageBody is a
42
- * separate follow-up; for now the gen token does the job. */
43
- let currentFocusedRow = null;
44
- function focusRow(row, accountId, msg) {
45
- if (currentFocusedRow && currentFocusedRow !== row) {
46
- // Unfocus the previous atomically — clearing .selected AND
47
- // triggering the viewer's stale-fetch cancel (via the bump inside
48
- // the next showMessage() call).
49
- currentFocusedRow.classList.remove("selected");
50
- }
51
- if (!row.classList.contains("selected"))
52
- row.classList.add("selected");
53
- currentFocusedRow = row;
54
- state.select(msg);
55
- onMessageSelect(accountId, msg.uid, msg.folderId);
40
+ * When the focused row's data leaves the list (delete, move, search
41
+ * reload, folder switch), the controller hands focus to a survivor
42
+ * via `focusByIdentity` or, if no survivor exists, calls
43
+ * `releaseFocus()` which clears highlight + viewer in the same call. */
44
+ let focusedRow = null;
45
+ const rowByKey = new Map();
46
+ function rowKey(accountId, uid) {
47
+ return `${accountId}:${uid}`;
48
+ }
49
+ function focusRow(row) {
50
+ if (focusedRow && focusedRow !== row)
51
+ focusedRow.setSelected(false);
52
+ row.setSelected(true);
53
+ focusedRow = row;
54
+ // Drive the viewer with the row's own envelope. Single call site;
55
+ // the viewer paints headers immediately, fetches body in background.
56
+ viewerShow(row.accountId, row.msg.uid, row.msg.folderId, undefined, false, row.msg);
57
+ onMessageSelect(row.accountId, row.msg.uid, row.msg.folderId);
58
+ document.dispatchEvent(new CustomEvent("mailx-focus-changed", { detail: row.msg }));
59
+ }
60
+ /** Read the currently-focused message envelope. Used by app-level
61
+ * features (flag toggle, mark unread, status bar) that need to know
62
+ * what's open in the viewer. */
63
+ export function getCurrentFocused() {
64
+ return focusedRow ? focusedRow.msg : null;
56
65
  }
57
- /** Back-compat shim some call sites still use `focusMessage(accountId, msg)`
58
- * without a DOM row (e.g. thread-popup click). Fall back to the previous
59
- * behavior (state + viewer only) so nothing regresses. */
60
- function focusMessage(accountId, msg) {
61
- state.select(msg);
62
- onMessageSelect(accountId, msg.uid, msg.folderId);
63
- // Clear the focused-row reference since we don't have a DOM row here
64
- // — the next row click will unfocus whatever was selected anyway.
65
- if (currentFocusedRow)
66
- currentFocusedRow.classList.remove("selected");
67
- currentFocusedRow = null;
66
+ /** Programmatic focus by identity. Used for thread-popup clicks,
67
+ * keyboard nav, post-delete handoff. Returns true if a row was found
68
+ * and focused. */
69
+ function focusByIdentity(accountId, uid) {
70
+ const row = rowByKey.get(rowKey(accountId, uid));
71
+ if (!row)
72
+ return false;
73
+ focusRow(row);
74
+ return true;
75
+ }
76
+ /** Release the focus slot and clear the preview pane in one call. */
77
+ export function releaseFocus() {
78
+ if (focusedRow)
79
+ focusedRow.setSelected(false);
80
+ focusedRow = null;
81
+ viewerClear();
82
+ document.dispatchEvent(new CustomEvent("mailx-focus-changed", { detail: null }));
68
83
  }
69
84
  /** Flip the "not-downloaded" indicator off for rows whose bodies just cached.
70
85
  * Called from the bodyCached service event — covers both background prefetch
@@ -96,10 +111,14 @@ function clearSelection() {
96
111
  const body = document.getElementById("ml-body");
97
112
  if (body)
98
113
  body.querySelectorAll(".ml-row.selected").forEach(r => r.classList.remove("selected"));
99
- // S56 seam: the focused-row invariant is "the Row whose .selected is
100
- // currently mine". clearSelection wipes all .selected, so the invariant
101
- // would break if we kept a stale pointer.
102
- currentFocusedRow = null;
114
+ // The focused-row invariant is "the Row whose .selected is currently
115
+ // mine". clearSelection wipes all .selected, so the invariant breaks
116
+ // unless we drop the focused-row reference too.
117
+ if (focusedRow) {
118
+ focusedRow = null;
119
+ viewerClear();
120
+ document.dispatchEvent(new CustomEvent("mailx-focus-changed", { detail: null }));
121
+ }
103
122
  }
104
123
  /** Deterministic sender-avatar color from a seed string (typically the
105
124
  * email address). Hash → hue at 12 evenly-spaced positions on the wheel.
@@ -238,10 +257,14 @@ export function initMessageList(handler) {
238
257
  }
239
258
  });
240
259
  }
241
- // Subscribe to state changes react to removeMessages (move/delete)
242
- state.subscribe((change) => {
243
- if (change === "removed") {
244
- syncDomToState();
260
+ // Viewer signals "this row is gone server-side" via mailx-remove-stale.
261
+ // The list owns row lifecycle, so it runs the removal here — filtering
262
+ // state, removing the DOM row, and handing focus to a survivor (or
263
+ // clearing the pane) in one transaction.
264
+ document.addEventListener("mailx-remove-stale", (e) => {
265
+ const { accountId, uid } = e.detail || {};
266
+ if (typeof uid === "number" && typeof accountId === "string") {
267
+ removeMessagesAndReconcile([{ accountId, uid }]);
245
268
  }
246
269
  });
247
270
  // Sort column headers — click to cycle. Date defaults desc (newest first);
@@ -311,39 +334,46 @@ function updateSortIndicators() {
311
334
  });
312
335
  }
313
336
  /**
314
- * Sync DOM rows to current state after messages are removed.
315
- * Removes DOM rows that are no longer in state, updates selection.
337
+ * Remove the named messages from the list and reconcile DOM + focus.
338
+ *
339
+ * Single-transaction transition: filters the underlying state, deletes
340
+ * the DOM rows, and either re-focuses a surviving row or releases focus
341
+ * (clearing the viewer). Replaces the old subscribe-and-sync model where
342
+ * state.removeMessages broadcast a "removed" event to two independent
343
+ * subscribers — that path could leave the highlight and preview out of
344
+ * sync if the list and viewer noticed the change in different orders.
345
+ *
346
+ * Call this whenever local rows need to disappear (delete, move,
347
+ * server-side stale removal, undo). Pass identities; the function
348
+ * decides what to focus next.
316
349
  */
317
- function syncDomToState() {
350
+ export function removeMessagesAndReconcile(uids) {
351
+ const focusedIdent = focusedRow
352
+ ? { accountId: focusedRow.accountId, uid: focusedRow.msg.uid }
353
+ : null;
354
+ const outcome = state.removeMessages(uids, focusedIdent);
318
355
  const body = document.getElementById("ml-body");
319
- if (!body)
320
- return;
321
- // Build set of UIDs still in state
322
- const stateUids = new Set(state.getMessages().map(m => `${m.accountId}:${m.uid}`));
323
- // Remove rows not in state
324
- for (const row of Array.from(body.querySelectorAll(".ml-row"))) {
325
- const el = row;
326
- const key = `${el.dataset.accountId}:${el.dataset.uid}`;
327
- if (!stateUids.has(key)) {
328
- el.remove();
356
+ if (body) {
357
+ const stateUids = new Set(state.getMessages().map(m => `${m.accountId}:${m.uid}`));
358
+ for (const row of Array.from(body.querySelectorAll(".ml-row"))) {
359
+ const el = row;
360
+ const key = `${el.dataset.accountId}:${el.dataset.uid}`;
361
+ if (!stateUids.has(key))
362
+ el.remove();
329
363
  }
330
- }
331
- // Update selection to match state
332
- clearSelection();
333
- const sel = state.getSelected();
334
- if (sel) {
335
- const row = body.querySelector(`.ml-row[data-uid="${sel.uid}"][data-account-id="${sel.accountId}"]`)
336
- || body.querySelector(`.ml-row[data-uid="${sel.uid}"]`);
337
- if (row) {
338
- row.classList.add("selected");
339
- lastClickedRow = row;
340
- // Trigger viewer update
341
- onMessageSelect(sel.accountId, sel.uid, sel.folderId);
364
+ if (state.getMessages().length === 0) {
365
+ body.innerHTML = `<div class="ml-empty">No messages</div>`;
342
366
  }
343
367
  }
344
- // If no messages left, show empty
345
- if (state.getMessages().length === 0) {
346
- body.innerHTML = `<div class="ml-empty">No messages</div>`;
368
+ if (outcome.focusedWasRemoved) {
369
+ if (outcome.nextSurvivor && focusByIdentity(outcome.nextSurvivor.accountId, outcome.nextSurvivor.uid)) {
370
+ // focusByIdentity handled the transition (DOM class + viewer).
371
+ }
372
+ else {
373
+ // No survivor (or its row didn't render) — release focus and
374
+ // clear the pane so there's no orphaned preview.
375
+ releaseFocus();
376
+ }
347
377
  }
348
378
  }
349
379
  /** Reload the currently displayed folder (preserves current selection) */
@@ -418,11 +448,11 @@ export async function loadSearchResults(query, scope = "all", accountId = "", fo
418
448
  const body = document.getElementById("ml-body");
419
449
  if (!body)
420
450
  return;
421
- // Clear the preview pane old preview from the prior selection lingers
422
- // until the user clicks a search result. Atomic clear-on-list-mutation
423
- // is what Slice D's row-objects-own-preview will do generally; this is
424
- // the band-aid for the search-specific case until that lands.
425
- document.dispatchEvent(new CustomEvent("mailx-clear-viewer"));
451
+ // Search reload tears down the current row set focus must release
452
+ // along with the rows. releaseFocus clears the preview pane in the
453
+ // same call frame, so no orphan preview lingers behind a list that
454
+ // no longer contains the previously-shown row.
455
+ releaseFocus();
426
456
  body.innerHTML = `<div class="ml-empty">Searching...</div>`;
427
457
  try {
428
458
  // Regex search: filter client-side
@@ -618,7 +648,26 @@ export async function showThreadPopup(pillEl, headMsg) {
618
648
  item.appendChild(date);
619
649
  item.appendChild(subject);
620
650
  item.addEventListener("click", async () => {
621
- 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 });
651
+ // Thread popup viewer-only update. The list keeps showing the
652
+ // thread head highlighted (it's the row in the actual list);
653
+ // the viewer pivots to the clicked thread member. Single path
654
+ // in: the viewer's show() call. No state, no event, no drift.
655
+ const envelope = {
656
+ accountId: msg.accountId,
657
+ uid: msg.uid,
658
+ folderId: msg.folderId,
659
+ subject: msg.subject,
660
+ from: msg.from,
661
+ to: msg.to,
662
+ cc: msg.cc,
663
+ date: msg.date,
664
+ flags: msg.flags,
665
+ size: msg.size,
666
+ preview: msg.preview,
667
+ hasAttachments: msg.hasAttachments,
668
+ };
669
+ viewerShow(msg.accountId, msg.uid, msg.folderId, undefined, false, envelope);
670
+ onMessageSelect(msg.accountId, msg.uid, msg.folderId);
622
671
  popup.remove();
623
672
  });
624
673
  popup.appendChild(item);
@@ -638,36 +687,35 @@ export async function showThreadPopup(pillEl, headMsg) {
638
687
  document.addEventListener("mousedown", dismiss, true);
639
688
  }, 0);
640
689
  }
641
- function appendMessages(body, accountId, items) {
642
- // Thread grouping: when the list has the "threaded" class, collapse messages
643
- // sharing the same threadId to a single row showing the most recent message,
644
- // with a small pill indicating the thread size. Pre-threading messages have
645
- // no threadId those are treated as singletons keyed by their own uid.
646
- const threaded = body.classList.contains("threaded");
647
- let rowsToRender = items;
648
- let threadSize = null;
649
- if (threaded) {
650
- const threadMap = new Map(); // threadId → newest msg
651
- threadSize = new Map();
652
- for (const msg of items) {
653
- const key = msg.threadId || `_msg_${msg.accountId || accountId}_${msg.uid}`;
654
- const existing = threadMap.get(key);
655
- if (!existing || (msg.date || 0) > (existing.date || 0)) {
656
- threadMap.set(key, msg);
657
- }
658
- }
659
- // Count messages per thread
660
- for (const msg of items) {
661
- const key = msg.threadId || `_msg_${msg.accountId || accountId}_${msg.uid}`;
662
- const head = threadMap.get(key);
663
- if (head)
664
- threadSize.set(head, (threadSize.get(head) || 0) + 1);
665
- }
666
- rowsToRender = Array.from(threadMap.values()).sort((a, b) => (b.date || 0) - (a.date || 0));
667
- }
668
- for (const msg of rowsToRender) {
669
- const msgAccountId = msg.accountId || accountId;
690
+ /** A rendered row in the message list.
691
+ *
692
+ * Owns its DOM element, the message envelope it represents, and all of
693
+ * its event handlers (click, dblclick, drag, contextmenu, touch
694
+ * long-press, plus the avatar / flag / thread-pill child handlers).
695
+ * State changes that affect appearance (selected, unread, flagged,
696
+ * body-cached) go through methods so the OO layer is the single point
697
+ * of mutation — no `el.classList.toggle("selected")` scattered through
698
+ * the codebase.
699
+ *
700
+ * Lifecycle: constructor builds DOM and wires handlers; `attach(body)`
701
+ * inserts into the list; `detach()` removes from DOM and from the
702
+ * module-level rowByKey map. The list controller diff-updates rows on
703
+ * list mutations.
704
+ *
705
+ * Multi-select still uses `.selected` class queries — that's a single
706
+ * DOM-level concept where drift isn't an issue (no separate render
707
+ * surface to drift against). The Row class wraps the *focus* concept
708
+ * (which couples to the viewer) as a fate-shared unit. */
709
+ class MessageRow {
710
+ msg;
711
+ accountId;
712
+ el;
713
+ flagEl;
714
+ constructor(msg, accountId, threadHead, threadCount, showAccountTag) {
715
+ this.msg = msg;
716
+ this.accountId = accountId;
670
717
  const row = document.createElement("div");
718
+ this.el = row;
671
719
  row.className = "ml-row";
672
720
  row.draggable = true;
673
721
  if (!msg.flags.includes("\\Seen"))
@@ -676,25 +724,18 @@ function appendMessages(body, accountId, items) {
676
724
  row.classList.add("flagged");
677
725
  if (!msg.bodyPath)
678
726
  row.classList.add("not-downloaded");
679
- // Pink-row visible reconciliation state (S1 slice C): a queued local
680
- // action (move/flag/delete) hasn't been ACK'd by the server yet.
681
727
  if (msg.pending)
682
728
  row.classList.add("pending-reconcile");
683
- // Reply-row marker: messages with In-Reply-To are replies. Shows a
684
- // subtle left-border accent so the eye can pick out threaded replies
685
- // without enabling full thread grouping.
686
729
  if (msg.inReplyTo)
687
730
  row.classList.add("is-reply");
688
731
  row.dataset.uid = String(msg.uid);
689
- row.dataset.accountId = msgAccountId;
732
+ row.dataset.accountId = accountId;
690
733
  row.dataset.folderId = String(msg.folderId);
691
734
  if (msg.threadId)
692
735
  row.dataset.threadId = msg.threadId;
693
- // Sender avatar \u2014 Thunderbird-style colored circle with the first
694
- // initial of the sender's display name. Doubles as the multi-select
695
- // affordance: in `multi-select-on` mode, the avatar swaps to a
696
- // checkmark via CSS. Color is derived deterministically from the
697
- // address so the same sender keeps the same color across rows.
736
+ if (threadHead)
737
+ row.classList.add("thread-head");
738
+ // ── Avatar (sender circle, doubles as multi-select affordance) ──
698
739
  const fromName = (showToInsteadOfFrom && msg.to?.length)
699
740
  ? (msg.to[0].name || msg.to[0].address || "?")
700
741
  : (msg.from?.name || msg.from?.address || "?");
@@ -705,81 +746,16 @@ function appendMessages(body, accountId, items) {
705
746
  avatar.textContent = initial;
706
747
  avatar.style.background = senderColor(seedAddr);
707
748
  avatar.title = msg.from?.address || "";
708
- // Tapping the avatar enters multi-select mode (or toggles in it,
709
- // mirroring Thunderbird/Gmail). Click bubbles to the row otherwise,
710
- // which would open the message stopPropagation here keeps the
711
- // avatar a dedicated selection affordance.
712
- avatar.addEventListener("click", (e) => {
713
- e.stopPropagation();
714
- const body = document.getElementById("ml-body");
715
- if (!body)
716
- return;
717
- if (body.classList.contains("multi-select-on")) {
718
- row.classList.toggle("selected");
719
- }
720
- else {
721
- clearSelection();
722
- row.classList.add("selected");
723
- body.classList.add("multi-select-on");
724
- }
725
- lastClickedRow = row;
726
- updateBulkBar();
727
- });
728
- // Right-click (or long-press) on the avatar → bulk-selection menu.
729
- // Putting it on the avatar is contextually right: the avatar is the
730
- // "select" affordance, so its menu owns operations on the selection
731
- // set. "Select all visible" is the load-bearing item — there's no
732
- // Ctrl-A equivalent on touch and the scope-after-search use case
733
- // demands it.
734
- avatar.addEventListener("contextmenu", async (e) => {
735
- e.preventDefault();
736
- e.stopPropagation();
737
- const { showContextMenu } = await import("./context-menu.js");
738
- const body = document.getElementById("ml-body");
739
- const visibleRows = body
740
- ? Array.from(body.querySelectorAll(".ml-row:not(.filter-hidden)"))
741
- : [];
742
- const selectedCount = body
743
- ? body.querySelectorAll(".ml-row.selected").length
744
- : 0;
745
- showContextMenu(e.clientX, e.clientY, [
746
- {
747
- label: `Select all (${visibleRows.length})`,
748
- action: () => {
749
- if (!body)
750
- return;
751
- body.classList.add("multi-select-on");
752
- for (const r of visibleRows)
753
- 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)
768
- return;
769
- body.classList.add("multi-select-on");
770
- for (const r of visibleRows)
771
- r.classList.toggle("selected");
772
- lastClickedRow = visibleRows[visibleRows.length - 1] || null;
773
- updateBulkBar();
774
- },
775
- disabled: visibleRows.length === 0,
776
- },
777
- ]);
778
- });
749
+ avatar.addEventListener("click", (e) => this.onAvatarClick(e));
750
+ avatar.addEventListener("contextmenu", (e) => this.onAvatarContextMenu(e));
751
+ // ── Flag star (toggle on click) ──
779
752
  const flag = document.createElement("span");
780
753
  flag.className = "ml-flag";
781
- flag.textContent = msg.flags.includes("\\Flagged") ? "\u2605" : "\u2606";
754
+ flag.textContent = msg.flags.includes("\\Flagged") ? "" : "";
782
755
  flag.title = "Toggle flag";
756
+ flag.addEventListener("click", (e) => this.onFlagClick(e));
757
+ this.flagEl = flag;
758
+ // ── From column ──
783
759
  const from = document.createElement("span");
784
760
  from.className = "ml-from";
785
761
  if (showToInsteadOfFrom && msg.to?.length) {
@@ -788,15 +764,13 @@ function appendMessages(body, accountId, items) {
788
764
  else {
789
765
  from.textContent = msg.from.name || msg.from.address;
790
766
  }
791
- if (!accountId && msgAccountId) {
767
+ if (showAccountTag && accountId) {
792
768
  const tag = document.createElement("span");
793
769
  tag.className = "ml-account-tag";
794
- tag.textContent = msgAccountId.charAt(0).toUpperCase();
795
- tag.title = msgAccountId;
770
+ tag.textContent = accountId.charAt(0).toUpperCase();
771
+ tag.title = accountId;
796
772
  from.prepend(tag);
797
773
  }
798
- // Search/cross-folder results carry folderName — show a tag so the user
799
- // can tell which folder each hit lives in.
800
774
  if (msg.folderName) {
801
775
  const folderTag = document.createElement("span");
802
776
  folderTag.className = "ml-folder-tag";
@@ -804,8 +778,6 @@ function appendMessages(body, accountId, items) {
804
778
  folderTag.title = `In folder: ${msg.folderName}`;
805
779
  from.prepend(folderTag);
806
780
  }
807
- // Unified inbox: same Message-ID exists under >=2 accounts → ⇆ badge.
808
- // Tooltip names the count so the user knows "this appears on N".
809
781
  if (msg.dupeCount >= 2) {
810
782
  const dupe = document.createElement("span");
811
783
  dupe.className = "ml-dupe-tag";
@@ -813,173 +785,231 @@ function appendMessages(body, accountId, items) {
813
785
  dupe.title = `Same message on ${msg.dupeCount} accounts`;
814
786
  from.prepend(dupe);
815
787
  }
788
+ // ── Subject (with optional thread pill + preview snippet) ──
816
789
  const subject = document.createElement("span");
817
790
  subject.className = "ml-subject";
818
791
  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
- }
792
+ if (threadHead && threadCount > 1 && msg.threadId) {
793
+ const threadPill = document.createElement("span");
794
+ threadPill.className = "ml-thread-pill";
795
+ threadPill.textContent = String(threadCount);
796
+ threadPill.title = `${threadCount} messages in this thread — click to see list`;
797
+ threadPill.addEventListener("click", async (e) => {
798
+ e.stopPropagation();
799
+ await showThreadPopup(threadPill, msg);
800
+ });
801
+ subject.prepend(threadPill);
835
802
  }
836
803
  if (msg.preview) {
837
804
  const preview = document.createElement("span");
838
805
  preview.className = "ml-preview";
839
- preview.textContent = ` \u2014 ${msg.preview}`;
806
+ preview.textContent = ` ${msg.preview}`;
840
807
  subject.appendChild(preview);
841
808
  }
809
+ // ── Date column ──
842
810
  const date = document.createElement("span");
843
811
  date.className = "ml-date";
844
812
  date.textContent = formatDate(msg.date);
845
- flag.addEventListener("click", async (e) => {
846
- e.stopPropagation();
847
- const isFlagged = row.classList.contains("flagged");
848
- const currentFlags = msg.flags || [];
849
- const newFlags = isFlagged
850
- ? currentFlags.filter((f) => f !== "\\Flagged")
851
- : [...currentFlags, "\\Flagged"];
852
- try {
853
- await updateFlags(msgAccountId, msg.uid, newFlags);
854
- msg.flags = newFlags;
855
- row.classList.toggle("flagged");
856
- flag.textContent = row.classList.contains("flagged") ? "\u2605" : "\u2606";
857
- }
858
- catch { /* ignore */ }
859
- });
860
813
  row.appendChild(avatar);
861
814
  row.appendChild(flag);
862
815
  row.appendChild(from);
863
816
  row.appendChild(date);
864
817
  row.appendChild(subject);
865
- row.addEventListener("click", (e) => {
866
- if (touchWasScroll) {
867
- touchWasScroll = false;
868
- return;
869
- }
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;
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
- }
889
- else {
890
- // No anchor available (first click of the session, or list
891
- // was just rebuilt with no selection). Treat as a plain
892
- // click so the user gets visible feedback rather than a
893
- // silent no-op.
894
- clearSelection();
895
- focusRow(row, msgAccountId, msg);
896
- lastClickedRow = row;
897
- row.classList.remove("unread");
898
- }
899
- }
900
- else if (e.ctrlKey || e.metaKey) {
901
- row.classList.toggle("selected");
902
- lastClickedRow = row;
903
- }
904
- else {
905
- // Atomic unfocus-previous + focus-this.
906
- clearSelection();
907
- focusRow(row, msgAccountId, msg);
908
- lastClickedRow = row;
909
- row.classList.remove("unread");
910
- }
818
+ row.addEventListener("click", (e) => this.onRowClick(e));
819
+ row.addEventListener("dblclick", (e) => this.onRowDoubleClick(e));
820
+ row.addEventListener("dragstart", (e) => this.onDragStart(e));
821
+ row.addEventListener("dragend", () => row.classList.remove("dragging"));
822
+ row.addEventListener("contextmenu", (e) => this.onRowContextMenu(e));
823
+ this.wireLongPress(row);
824
+ }
825
+ /** Insert this row into a list body and register it in the lookup map. */
826
+ attach(body) {
827
+ body.appendChild(this.el);
828
+ rowByKey.set(rowKey(this.accountId, this.msg.uid), this);
829
+ }
830
+ /** Remove this row from the DOM and the lookup map. If it was the
831
+ * focused row, the controller is responsible for releasing focus
832
+ * (this method doesn't auto-clear the viewer because the controller
833
+ * may want to hand focus to a sibling instead). */
834
+ detach() {
835
+ this.el.remove();
836
+ rowByKey.delete(rowKey(this.accountId, this.msg.uid));
837
+ }
838
+ setSelected(yes) { this.el.classList.toggle("selected", yes); }
839
+ get isSelected() { return this.el.classList.contains("selected"); }
840
+ setUnreadClass(yes) { this.el.classList.toggle("unread", yes); }
841
+ setFlaggedClass(yes) {
842
+ this.el.classList.toggle("flagged", yes);
843
+ this.flagEl.textContent = yes ? "★" : "☆";
844
+ }
845
+ markBodyCached() { this.el.classList.remove("not-downloaded"); }
846
+ onAvatarClick(e) {
847
+ e.stopPropagation();
848
+ const body = document.getElementById("ml-body");
849
+ if (!body)
850
+ return;
851
+ if (body.classList.contains("multi-select-on")) {
852
+ this.setSelected(!this.isSelected);
853
+ }
854
+ else {
855
+ clearSelection();
856
+ this.setSelected(true);
857
+ body.classList.add("multi-select-on");
858
+ }
859
+ lastClickedRow = this.el;
860
+ updateBulkBar();
861
+ }
862
+ async onAvatarContextMenu(e) {
863
+ e.preventDefault();
864
+ e.stopPropagation();
865
+ const { showContextMenu: showMenu } = await import("./context-menu.js");
866
+ const body = document.getElementById("ml-body");
867
+ const visibleRows = body
868
+ ? Array.from(body.querySelectorAll(".ml-row:not(.filter-hidden)"))
869
+ : [];
870
+ const selectedCount = body
871
+ ? body.querySelectorAll(".ml-row.selected").length
872
+ : 0;
873
+ showMenu(e.clientX, e.clientY, [
874
+ {
875
+ label: `Select all (${visibleRows.length})`,
876
+ action: () => {
877
+ if (!body)
878
+ return;
879
+ body.classList.add("multi-select-on");
880
+ for (const r of visibleRows)
881
+ r.classList.add("selected");
882
+ lastClickedRow = visibleRows[visibleRows.length - 1] || null;
883
+ updateBulkBar();
884
+ },
885
+ disabled: visibleRows.length === 0,
886
+ },
887
+ {
888
+ label: `Clear selection${selectedCount ? ` (${selectedCount})` : ""}`,
889
+ action: () => exitMultiSelect(),
890
+ disabled: selectedCount === 0,
891
+ },
892
+ {
893
+ label: "Invert selection",
894
+ action: () => {
895
+ if (!body)
896
+ return;
897
+ body.classList.add("multi-select-on");
898
+ for (const r of visibleRows)
899
+ r.classList.toggle("selected");
900
+ lastClickedRow = visibleRows[visibleRows.length - 1] || null;
901
+ updateBulkBar();
902
+ },
903
+ disabled: visibleRows.length === 0,
904
+ },
905
+ ]);
906
+ }
907
+ async onFlagClick(e) {
908
+ e.stopPropagation();
909
+ const isFlagged = this.el.classList.contains("flagged");
910
+ const currentFlags = this.msg.flags || [];
911
+ const newFlags = isFlagged
912
+ ? currentFlags.filter((f) => f !== "\\Flagged")
913
+ : [...currentFlags, "\\Flagged"];
914
+ try {
915
+ await updateFlags(this.accountId, this.msg.uid, newFlags);
916
+ this.msg.flags = newFlags;
917
+ this.setFlaggedClass(!isFlagged);
918
+ }
919
+ catch { /* ignore */ }
920
+ }
921
+ onRowClick(e) {
922
+ if (touchWasScroll) {
923
+ touchWasScroll = false;
924
+ return;
925
+ }
926
+ const body = this.el.parentElement;
927
+ if (body?.classList.contains("multi-select-on")) {
928
+ this.setSelected(!this.isSelected);
929
+ lastClickedRow = this.el;
911
930
  updateBulkBar();
912
- });
913
- // Q64: double-click → pop out the message in a floating overlay so
914
- // the user can read it without losing the selected list context.
915
- row.addEventListener("dblclick", (e) => {
916
- e.preventDefault();
917
- e.stopPropagation();
918
- document.dispatchEvent(new CustomEvent("mailx-popout-message", {
919
- detail: { accountId: msgAccountId, uid: msg.uid, folderId: msg.folderId, subject: msg.subject }
920
- }));
921
- });
922
- row.addEventListener("dragstart", (e) => {
923
- if (!row.classList.contains("selected")) {
931
+ return;
932
+ }
933
+ if (e.shiftKey) {
934
+ const anchor = resolveShiftAnchor();
935
+ if (anchor) {
924
936
  clearSelection();
925
- row.classList.add("selected");
926
- lastClickedRow = row;
937
+ selectRange(anchor, this.el);
938
+ lastClickedRow = this.el;
939
+ this.setUnreadClass(false);
940
+ focusRow(this);
927
941
  }
928
- const selected = getSelectedMessages();
929
- e.dataTransfer.setData("application/x-mailx-messages", JSON.stringify(selected));
930
- e.dataTransfer.setData("application/x-mailx-message", JSON.stringify({
931
- accountId: msgAccountId,
932
- uid: msg.uid,
933
- folderId: msg.folderId,
934
- subject: msg.subject,
935
- }));
936
- e.dataTransfer.effectAllowed = "copyMove";
937
- row.classList.add("dragging");
938
- if (selected.length > 1) {
939
- const badge = document.createElement("div");
940
- badge.textContent = `${selected.length} messages`;
941
- badge.style.cssText = "position:absolute;top:-1000px;background:#333;color:white;padding:4px 8px;border-radius:4px;font-size:12px";
942
- document.body.appendChild(badge);
943
- e.dataTransfer.setDragImage(badge, 0, 0);
944
- setTimeout(() => badge.remove(), 0);
942
+ else {
943
+ clearSelection();
944
+ focusRow(this);
945
+ lastClickedRow = this.el;
946
+ this.setUnreadClass(false);
945
947
  }
946
- });
947
- row.addEventListener("dragend", () => row.classList.remove("dragging"));
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.
948
+ }
949
+ else if (e.ctrlKey || e.metaKey) {
950
+ this.setSelected(!this.isSelected);
951
+ lastClickedRow = this.el;
952
+ }
953
+ else {
954
+ clearSelection();
955
+ focusRow(this);
956
+ lastClickedRow = this.el;
957
+ this.setUnreadClass(false);
958
+ }
959
+ updateBulkBar();
960
+ }
961
+ onRowDoubleClick(e) {
962
+ e.preventDefault();
963
+ e.stopPropagation();
964
+ document.dispatchEvent(new CustomEvent("mailx-popout-message", {
965
+ detail: { accountId: this.accountId, uid: this.msg.uid, folderId: this.msg.folderId, subject: this.msg.subject },
966
+ }));
967
+ }
968
+ onDragStart(e) {
969
+ if (!this.isSelected) {
970
+ clearSelection();
971
+ this.setSelected(true);
972
+ lastClickedRow = this.el;
973
+ }
974
+ const selected = getSelectedMessages();
975
+ e.dataTransfer.setData("application/x-mailx-messages", JSON.stringify(selected));
976
+ e.dataTransfer.setData("application/x-mailx-message", JSON.stringify({
977
+ accountId: this.accountId,
978
+ uid: this.msg.uid,
979
+ folderId: this.msg.folderId,
980
+ subject: this.msg.subject,
981
+ }));
982
+ e.dataTransfer.effectAllowed = "copyMove";
983
+ this.el.classList.add("dragging");
984
+ if (selected.length > 1) {
985
+ const badge = document.createElement("div");
986
+ badge.textContent = `${selected.length} messages`;
987
+ badge.style.cssText = "position:absolute;top:-1000px;background:#333;color:white;padding:4px 8px;border-radius:4px;font-size:12px";
988
+ document.body.appendChild(badge);
989
+ e.dataTransfer.setDragImage(badge, 0, 0);
990
+ setTimeout(() => badge.remove(), 0);
991
+ }
992
+ }
993
+ wireLongPress(row) {
951
994
  let longPressTimer = null;
952
995
  const LONG_PRESS_MS = 550;
953
- row.addEventListener("touchstart", (e) => {
954
- const t = e.touches[0];
955
- if (!t)
956
- return;
957
- const cx = t.clientX, cy = t.clientY;
996
+ row.addEventListener("touchstart", (_e) => {
958
997
  if (longPressTimer)
959
998
  clearTimeout(longPressTimer);
960
999
  longPressTimer = setTimeout(() => {
961
1000
  longPressTimer = null;
962
- // Long-press semantics:
963
- // - If the list is already in multi-select mode, toggle this
964
- // row's selected state (so the user can extend a selection
965
- // without needing a second long-press-and-menu dance).
966
- // - Otherwise enter multi-select mode: mark THIS row selected
967
- // and add a sticky class on the body so future taps toggle
968
- // instead of opening messages. Tap elsewhere or press
969
- // Escape to exit.
970
1001
  const body = row.parentElement;
971
1002
  const alreadyMulti = body?.classList.contains("multi-select-on");
972
1003
  if (alreadyMulti) {
973
- row.classList.toggle("selected");
1004
+ this.setSelected(!this.isSelected);
974
1005
  }
975
1006
  else {
976
1007
  clearSelection();
977
- row.classList.add("selected");
1008
+ this.setSelected(true);
978
1009
  body?.classList.add("multi-select-on");
979
1010
  }
980
- lastClickedRow = row;
1011
+ lastClickedRow = this.el;
981
1012
  updateBulkBar();
982
- // Haptic hint if the platform supports it (Android WebView does).
983
1013
  try {
984
1014
  navigator.vibrate?.(20);
985
1015
  }
@@ -995,130 +1025,137 @@ function appendMessages(body, accountId, items) {
995
1025
  row.addEventListener("touchmove", cancelLongPress, { passive: true });
996
1026
  row.addEventListener("touchend", cancelLongPress, { passive: true });
997
1027
  row.addEventListener("touchcancel", cancelLongPress, { passive: true });
998
- // ── Right-click context menu ──
999
- row.addEventListener("contextmenu", (e) => {
1000
- e.preventDefault();
1001
- // Selection-on-context-click semantics:
1002
- // - If the right-clicked row is already selected → keep the
1003
- // existing selection (single or multi). The menu acts on
1004
- // whatever is selected.
1005
- // - If it's NOT selected and we're in multi-select mode →
1006
- // ADD this row to the selection (don't clear). Long-press
1007
- // to open a context menu used to wipe the multi-select set,
1008
- // which was the user-reported "annoying" behavior.
1009
- // - If it's NOT selected and we're NOT in multi-select →
1010
- // single-select this row (replace prior selection).
1011
- const body = row.parentElement;
1012
- const inMulti = !!body?.classList.contains("multi-select-on");
1013
- if (!row.classList.contains("selected")) {
1014
- if (inMulti) {
1015
- row.classList.add("selected");
1016
- lastClickedRow = row;
1017
- }
1018
- else {
1019
- clearSelection();
1020
- row.classList.add("selected");
1021
- lastClickedRow = row;
1022
- focusMessage(msgAccountId, msg);
1023
- }
1028
+ }
1029
+ onRowContextMenu(e) {
1030
+ e.preventDefault();
1031
+ const body = this.el.parentElement;
1032
+ const inMulti = !!body?.classList.contains("multi-select-on");
1033
+ if (!this.isSelected) {
1034
+ if (inMulti) {
1035
+ this.setSelected(true);
1036
+ lastClickedRow = this.el;
1024
1037
  }
1025
- const isSeen = msg.flags.includes("\\Seen");
1026
- const isFlagged = msg.flags.includes("\\Flagged");
1027
- const items = [
1028
- {
1029
- label: isSeen ? "Mark unread" : "Mark read",
1030
- action: async () => {
1031
- const newFlags = isSeen
1032
- ? msg.flags.filter((f) => f !== "\\Seen")
1033
- : [...msg.flags, "\\Seen"];
1034
- try {
1035
- await updateFlags(msgAccountId, msg.uid, newFlags);
1036
- msg.flags = newFlags;
1037
- state.updateMessageFlags(msgAccountId, msg.uid, newFlags);
1038
- row.classList.toggle("unread", !newFlags.includes("\\Seen"));
1039
- }
1040
- catch { /* ignore */ }
1041
- },
1042
- },
1043
- {
1044
- label: isFlagged ? "Unflag" : "Flag",
1045
- action: async () => {
1046
- const newFlags = isFlagged
1047
- ? msg.flags.filter((f) => f !== "\\Flagged")
1048
- : [...msg.flags, "\\Flagged"];
1049
- try {
1050
- await updateFlags(msgAccountId, msg.uid, newFlags);
1051
- msg.flags = newFlags;
1052
- row.classList.toggle("flagged");
1053
- flag.textContent = row.classList.contains("flagged") ? "\u2605" : "\u2606";
1054
- }
1055
- catch { /* ignore */ }
1056
- },
1057
- },
1058
- { label: "", action: () => { }, separator: true },
1059
- {
1060
- label: "Reply",
1061
- action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "reply" } })),
1062
- },
1063
- {
1064
- label: "Reply All",
1065
- action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "replyAll" } })),
1066
- },
1067
- {
1068
- label: "Forward",
1069
- action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "forward" } })),
1070
- },
1071
- { label: "", action: () => { }, separator: true },
1072
- {
1073
- label: "Move to folder…",
1074
- action: async () => {
1075
- // Move all currently-selected rows (or just this one if it's the only selection)
1076
- const selectedRows = Array.from(document.querySelectorAll(".ml-row.selected"));
1077
- const uids = selectedRows.length > 0
1078
- ? selectedRows.map((r) => Number(r.dataset.uid)).filter(u => !isNaN(u))
1079
- : [msg.uid];
1080
- const pick = await pickFolder(msgAccountId, { excludeFolderIds: [msg.folderId] });
1081
- if (!pick)
1082
- return;
1083
- try {
1084
- await apiMoveMessages(msgAccountId, uids, pick.folderId);
1085
- // Remove from local state — reconciler handles server sync.
1086
- state.removeMessages(uids.map(u => ({ accountId: msgAccountId, uid: u })));
1087
- }
1088
- catch (err) {
1089
- alert(`Move failed: ${err.message}`);
1090
- }
1091
- },
1038
+ else {
1039
+ clearSelection();
1040
+ lastClickedRow = this.el;
1041
+ focusRow(this);
1042
+ }
1043
+ }
1044
+ const isSeen = this.msg.flags.includes("\\Seen");
1045
+ const isFlagged = this.msg.flags.includes("\\Flagged");
1046
+ const accountId = this.accountId;
1047
+ const msg = this.msg;
1048
+ const self = this;
1049
+ const items = [
1050
+ {
1051
+ label: isSeen ? "Mark unread" : "Mark read",
1052
+ action: async () => {
1053
+ const newFlags = isSeen
1054
+ ? msg.flags.filter((f) => f !== "\\Seen")
1055
+ : [...msg.flags, "\\Seen"];
1056
+ try {
1057
+ await updateFlags(accountId, msg.uid, newFlags);
1058
+ msg.flags = newFlags;
1059
+ state.updateMessageFlags(accountId, msg.uid, newFlags);
1060
+ self.setUnreadClass(!newFlags.includes("\\Seen"));
1061
+ }
1062
+ catch { /* ignore */ }
1092
1063
  },
1093
- {
1094
- label: "Delete",
1095
- action: () => document.dispatchEvent(new CustomEvent("mailx-delete")),
1064
+ },
1065
+ {
1066
+ label: isFlagged ? "Unflag" : "Flag",
1067
+ action: async () => {
1068
+ const newFlags = isFlagged
1069
+ ? msg.flags.filter((f) => f !== "\\Flagged")
1070
+ : [...msg.flags, "\\Flagged"];
1071
+ try {
1072
+ await updateFlags(accountId, msg.uid, newFlags);
1073
+ msg.flags = newFlags;
1074
+ self.setFlaggedClass(newFlags.includes("\\Flagged"));
1075
+ }
1076
+ catch { /* ignore */ }
1096
1077
  },
1097
- { label: "", action: () => { }, separator: true },
1098
- {
1099
- label: " Mark as spam",
1100
- action: () => document.getElementById("btn-spam")?.click(),
1078
+ },
1079
+ { label: "", action: () => { }, separator: true },
1080
+ { label: "Reply", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "reply" } })) },
1081
+ { label: "Reply All", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "replyAll" } })) },
1082
+ { label: "Forward", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "forward" } })) },
1083
+ { label: "", action: () => { }, separator: true },
1084
+ {
1085
+ label: "Move to folder…",
1086
+ action: async () => {
1087
+ const selectedRows = Array.from(document.querySelectorAll(".ml-row.selected"));
1088
+ const uids = selectedRows.length > 0
1089
+ ? selectedRows.map((r) => Number(r.dataset.uid)).filter(u => !isNaN(u))
1090
+ : [msg.uid];
1091
+ const pick = await pickFolder(accountId, { excludeFolderIds: [msg.folderId] });
1092
+ if (!pick)
1093
+ return;
1094
+ try {
1095
+ await apiMoveMessages(accountId, uids, pick.folderId);
1096
+ removeMessagesAndReconcile(uids.map(u => ({ accountId, uid: u })));
1097
+ }
1098
+ catch (err) {
1099
+ alert(`Move failed: ${err.message}`);
1100
+ }
1101
1101
  },
1102
- { label: "", action: () => { }, separator: true },
1103
- {
1104
- label: "Copy Message-ID",
1105
- action: async () => {
1106
- // Useful when asking "where did my letter go?" pair the
1107
- // Message-ID with the reconcile-delete log line.
1108
- if (!msg.messageId) {
1109
- alert("No Message-ID on this row.");
1110
- return;
1111
- }
1112
- try {
1113
- await navigator.clipboard.writeText(msg.messageId);
1114
- }
1115
- catch { /* */ }
1116
- },
1102
+ },
1103
+ { label: "Delete", action: () => document.dispatchEvent(new CustomEvent("mailx-delete")) },
1104
+ { label: "", action: () => { }, separator: true },
1105
+ { label: "⚠ Mark as spam", action: () => document.getElementById("btn-spam")?.click() },
1106
+ { label: "", action: () => { }, separator: true },
1107
+ {
1108
+ label: "Copy Message-ID",
1109
+ action: async () => {
1110
+ if (!msg.messageId) {
1111
+ alert("No Message-ID on this row.");
1112
+ return;
1113
+ }
1114
+ try {
1115
+ await navigator.clipboard.writeText(msg.messageId);
1116
+ }
1117
+ catch { /* */ }
1117
1118
  },
1118
- ];
1119
- showContextMenu(e.clientX, e.clientY, items);
1120
- });
1121
- body.appendChild(row);
1119
+ },
1120
+ ];
1121
+ showContextMenu(e.clientX, e.clientY, items);
1122
+ }
1123
+ }
1124
+ function appendMessages(body, accountId, items) {
1125
+ // Thread grouping: when the list has the "threaded" class, collapse
1126
+ // messages sharing the same threadId to a single row showing the most
1127
+ // recent message, with a small pill indicating the thread size.
1128
+ const threaded = body.classList.contains("threaded");
1129
+ let rowsToRender = items;
1130
+ let threadSize = null;
1131
+ if (threaded) {
1132
+ const threadMap = new Map();
1133
+ threadSize = new Map();
1134
+ for (const msg of items) {
1135
+ const key = msg.threadId || `_msg_${msg.accountId || accountId}_${msg.uid}`;
1136
+ const existing = threadMap.get(key);
1137
+ if (!existing || (msg.date || 0) > (existing.date || 0)) {
1138
+ threadMap.set(key, msg);
1139
+ }
1140
+ }
1141
+ for (const msg of items) {
1142
+ const key = msg.threadId || `_msg_${msg.accountId || accountId}_${msg.uid}`;
1143
+ const head = threadMap.get(key);
1144
+ if (head)
1145
+ threadSize.set(head, (threadSize.get(head) || 0) + 1);
1146
+ }
1147
+ rowsToRender = Array.from(threadMap.values()).sort((a, b) => (b.date || 0) - (a.date || 0));
1148
+ }
1149
+ for (const msg of rowsToRender) {
1150
+ const msgAccountId = msg.accountId || accountId;
1151
+ const threadCount = threadSize ? (threadSize.get(msg) || 1) : 1;
1152
+ const isThreadHead = threadCount > 1 && !!msg.threadId;
1153
+ // showAccountTag: true when rendering the unified inbox (no
1154
+ // single accountId for the page; each row carries its own and
1155
+ // gets a one-letter account chip).
1156
+ const showAccountTag = !accountId && !!msgAccountId;
1157
+ const row = new MessageRow(msg, msgAccountId, isThreadHead, threadCount, showAccountTag);
1158
+ row.attach(body);
1122
1159
  }
1123
1160
  }
1124
1161
  function formatDate(epochMs) {