@bobfrankston/mailx 1.0.12 → 1.0.13

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 (237) hide show
  1. package/bin/mailx.js +47 -29
  2. package/client/app.js +93 -13
  3. package/client/components/folder-tree.js +84 -3
  4. package/client/components/message-list.js +134 -8
  5. package/client/components/message-viewer.js +130 -13
  6. package/client/compose/compose.html +4 -4
  7. package/client/compose/compose.js +53 -34
  8. package/client/index.html +33 -9
  9. package/client/lib/api-client.js +102 -30
  10. package/client/lib/mailxapi.js +123 -0
  11. package/client/package.json +1 -1
  12. package/client/styles/components.css +188 -15
  13. package/client/styles/layout.css +2 -1
  14. package/killmail.cmd +6 -0
  15. package/launch.ps1 +47 -5
  16. package/launcher/bin/mailx-app.exe +0 -0
  17. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Breadcrumbs +0 -0
  18. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Crashpad/metadata +0 -0
  19. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Crashpad/settings.dat +0 -0
  20. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Crashpad/throttle_store.dat +1 -0
  21. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/CrashpadMetrics-active.pma +0 -0
  22. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/BrowsingTopicsSiteData +0 -0
  23. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Cache/No_Vary_Search/journal.baj +1 -0
  24. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/DIPS +0 -0
  25. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/DashTrackerDatabase +0 -0
  26. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/EdgeJourneys/EdgeJourneys.db +0 -0
  27. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Extension Rules/LOCK +0 -0
  28. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Extension Rules/LOG +3 -0
  29. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Extension Rules/MANIFEST-000001 +0 -0
  30. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Extension Scripts/LOCK +0 -0
  31. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Extension Scripts/LOG +3 -0
  32. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Extension Scripts/MANIFEST-000001 +0 -0
  33. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Extension State/LOCK +0 -0
  34. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Extension State/LOG +3 -0
  35. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Extension State/MANIFEST-000001 +0 -0
  36. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/ExtensionActivityComp +0 -0
  37. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/ExtensionActivityEdge +0 -0
  38. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Favicons +0 -0
  39. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/History +0 -0
  40. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/History-journal +0 -0
  41. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/IndexedDB/devtools_devtools_0.indexeddb.leveldb/LOCK +0 -0
  42. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/IndexedDB/devtools_devtools_0.indexeddb.leveldb/LOG +3 -0
  43. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/IndexedDB/devtools_devtools_0.indexeddb.leveldb/MANIFEST-000001 +0 -0
  44. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Local Storage/leveldb/LOCK +0 -0
  45. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Local Storage/leveldb/LOG +3 -0
  46. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Local Storage/leveldb/MANIFEST-000001 +0 -0
  47. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Login Data +0 -0
  48. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Login Data For Account +0 -0
  49. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Network/Cookies +0 -0
  50. NEL +0 -0
  51. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Network/Trust Tokens +0 -0
  52. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Network Action Predictor +0 -0
  53. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Preferences +1 -0
  54. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Safe Browsing Network/Safe Browsing Cookies +0 -0
  55. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/ServerCertificate +0 -0
  56. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Session Storage/LOCK +0 -0
  57. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Session Storage/LOG +3 -0
  58. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Session Storage/MANIFEST-000001 +0 -0
  59. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Shared Dictionary/db +0 -0
  60. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/SharedStorage +0 -0
  61. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Shortcuts +0 -0
  62. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Site Characteristics Database/LOCK +0 -0
  63. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Site Characteristics Database/LOG +3 -0
  64. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Site Characteristics Database/MANIFEST-000001 +0 -0
  65. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Sync Data/LevelDB/LOCK +0 -0
  66. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Sync Data/LevelDB/LOG +3 -0
  67. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Sync Data/LevelDB/MANIFEST-000001 +0 -0
  68. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Top Sites +0 -0
  69. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Vpn Tokens +0 -0
  70. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Web Data +0 -0
  71. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/Web Data-journal +0 -0
  72. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/WebStorage/QuotaManager +0 -0
  73. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/WebStorage/QuotaManager-journal +0 -0
  74. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/heavy_ad_intervention_opt_out.db +0 -0
  75. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/shared_proto_db/LOCK +0 -0
  76. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/shared_proto_db/LOG +3 -0
  77. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/shared_proto_db/MANIFEST-000001 +0 -0
  78. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/shared_proto_db/metadata/LOCK +0 -0
  79. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/shared_proto_db/metadata/LOG +3 -0
  80. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Default/shared_proto_db/metadata/MANIFEST-000001 +0 -0
  81. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/DeferredBrowserMetrics/BrowserMetrics-69CAD063-BE24.pma +0 -0
  82. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Local State +1 -0
  83. package/launcher/bin/mailx-app.exe.WebView2/EBWebView/Variations +1 -0
  84. package/launcher/bin/mailx-app.old.exe +0 -0
  85. package/package.json +1 -1
  86. package/packages/mailx-api/index.js +79 -26
  87. package/packages/mailx-core/index.d.ts +129 -0
  88. package/packages/mailx-core/index.js +323 -0
  89. package/packages/mailx-core/ipc.d.ts +13 -0
  90. package/packages/mailx-core/ipc.js +56 -0
  91. package/packages/mailx-core/package.json +18 -0
  92. package/packages/mailx-imap/index.d.ts +5 -1
  93. package/packages/mailx-imap/index.js +76 -14
  94. package/packages/mailx-server/index.js +42 -31
  95. package/packages/mailx-server/package.json +1 -2
  96. package/packages/mailx-settings/index.d.ts +1 -1
  97. package/packages/mailx-settings/index.js +21 -12
  98. package/packages/mailx-store/db.d.ts +5 -1
  99. package/packages/mailx-store/db.js +64 -12
  100. package/packages/mailx-store/file-store.d.ts +2 -8
  101. package/packages/mailx-store/file-store.js +7 -31
  102. package/packages/mailx-types/index.d.ts +3 -1
  103. package/.tswalk.json +0 -7396
  104. package/launcher/release.cmd +0 -4
  105. package/mailx.json +0 -9
  106. package/packages/mailx-api/node_modules/nodemailer/.ncurc.js +0 -9
  107. package/packages/mailx-api/node_modules/nodemailer/.prettierignore +0 -8
  108. package/packages/mailx-api/node_modules/nodemailer/.prettierrc +0 -12
  109. package/packages/mailx-api/node_modules/nodemailer/.prettierrc.js +0 -10
  110. package/packages/mailx-api/node_modules/nodemailer/.release-please-config.json +0 -9
  111. package/packages/mailx-api/node_modules/nodemailer/LICENSE +0 -16
  112. package/packages/mailx-api/node_modules/nodemailer/README.md +0 -86
  113. package/packages/mailx-api/node_modules/nodemailer/SECURITY.txt +0 -22
  114. package/packages/mailx-api/node_modules/nodemailer/eslint.config.js +0 -88
  115. package/packages/mailx-api/node_modules/nodemailer/lib/addressparser/index.js +0 -383
  116. package/packages/mailx-api/node_modules/nodemailer/lib/base64/index.js +0 -139
  117. package/packages/mailx-api/node_modules/nodemailer/lib/dkim/index.js +0 -253
  118. package/packages/mailx-api/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
  119. package/packages/mailx-api/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
  120. package/packages/mailx-api/node_modules/nodemailer/lib/dkim/sign.js +0 -117
  121. package/packages/mailx-api/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
  122. package/packages/mailx-api/node_modules/nodemailer/lib/fetch/index.js +0 -280
  123. package/packages/mailx-api/node_modules/nodemailer/lib/json-transport/index.js +0 -82
  124. package/packages/mailx-api/node_modules/nodemailer/lib/mail-composer/index.js +0 -629
  125. package/packages/mailx-api/node_modules/nodemailer/lib/mailer/index.js +0 -441
  126. package/packages/mailx-api/node_modules/nodemailer/lib/mailer/mail-message.js +0 -316
  127. package/packages/mailx-api/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
  128. package/packages/mailx-api/node_modules/nodemailer/lib/mime-funcs/mime-types.js +0 -2113
  129. package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/index.js +0 -1316
  130. package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/last-newline.js +0 -33
  131. package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/le-unix.js +0 -43
  132. package/packages/mailx-api/node_modules/nodemailer/lib/mime-node/le-windows.js +0 -52
  133. package/packages/mailx-api/node_modules/nodemailer/lib/nodemailer.js +0 -157
  134. package/packages/mailx-api/node_modules/nodemailer/lib/punycode/index.js +0 -460
  135. package/packages/mailx-api/node_modules/nodemailer/lib/qp/index.js +0 -227
  136. package/packages/mailx-api/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
  137. package/packages/mailx-api/node_modules/nodemailer/lib/ses-transport/index.js +0 -234
  138. package/packages/mailx-api/node_modules/nodemailer/lib/shared/index.js +0 -754
  139. package/packages/mailx-api/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
  140. package/packages/mailx-api/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
  141. package/packages/mailx-api/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1870
  142. package/packages/mailx-api/node_modules/nodemailer/lib/smtp-pool/index.js +0 -652
  143. package/packages/mailx-api/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +0 -259
  144. package/packages/mailx-api/node_modules/nodemailer/lib/smtp-transport/index.js +0 -421
  145. package/packages/mailx-api/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
  146. package/packages/mailx-api/node_modules/nodemailer/lib/well-known/index.js +0 -47
  147. package/packages/mailx-api/node_modules/nodemailer/lib/well-known/services.json +0 -611
  148. package/packages/mailx-api/node_modules/nodemailer/lib/xoauth2/index.js +0 -427
  149. package/packages/mailx-api/node_modules/nodemailer/package.json +0 -47
  150. package/packages/mailx-imap/node_modules/nodemailer/.ncurc.js +0 -9
  151. package/packages/mailx-imap/node_modules/nodemailer/.prettierignore +0 -8
  152. package/packages/mailx-imap/node_modules/nodemailer/.prettierrc +0 -12
  153. package/packages/mailx-imap/node_modules/nodemailer/.prettierrc.js +0 -10
  154. package/packages/mailx-imap/node_modules/nodemailer/.release-please-config.json +0 -9
  155. package/packages/mailx-imap/node_modules/nodemailer/LICENSE +0 -16
  156. package/packages/mailx-imap/node_modules/nodemailer/README.md +0 -86
  157. package/packages/mailx-imap/node_modules/nodemailer/SECURITY.txt +0 -22
  158. package/packages/mailx-imap/node_modules/nodemailer/eslint.config.js +0 -88
  159. package/packages/mailx-imap/node_modules/nodemailer/lib/addressparser/index.js +0 -383
  160. package/packages/mailx-imap/node_modules/nodemailer/lib/base64/index.js +0 -139
  161. package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/index.js +0 -253
  162. package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
  163. package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
  164. package/packages/mailx-imap/node_modules/nodemailer/lib/dkim/sign.js +0 -117
  165. package/packages/mailx-imap/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
  166. package/packages/mailx-imap/node_modules/nodemailer/lib/fetch/index.js +0 -280
  167. package/packages/mailx-imap/node_modules/nodemailer/lib/json-transport/index.js +0 -82
  168. package/packages/mailx-imap/node_modules/nodemailer/lib/mail-composer/index.js +0 -629
  169. package/packages/mailx-imap/node_modules/nodemailer/lib/mailer/index.js +0 -441
  170. package/packages/mailx-imap/node_modules/nodemailer/lib/mailer/mail-message.js +0 -316
  171. package/packages/mailx-imap/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
  172. package/packages/mailx-imap/node_modules/nodemailer/lib/mime-funcs/mime-types.js +0 -2113
  173. package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/index.js +0 -1316
  174. package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/last-newline.js +0 -33
  175. package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/le-unix.js +0 -43
  176. package/packages/mailx-imap/node_modules/nodemailer/lib/mime-node/le-windows.js +0 -52
  177. package/packages/mailx-imap/node_modules/nodemailer/lib/nodemailer.js +0 -157
  178. package/packages/mailx-imap/node_modules/nodemailer/lib/punycode/index.js +0 -460
  179. package/packages/mailx-imap/node_modules/nodemailer/lib/qp/index.js +0 -227
  180. package/packages/mailx-imap/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
  181. package/packages/mailx-imap/node_modules/nodemailer/lib/ses-transport/index.js +0 -234
  182. package/packages/mailx-imap/node_modules/nodemailer/lib/shared/index.js +0 -754
  183. package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
  184. package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
  185. package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1870
  186. package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-pool/index.js +0 -652
  187. package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +0 -259
  188. package/packages/mailx-imap/node_modules/nodemailer/lib/smtp-transport/index.js +0 -421
  189. package/packages/mailx-imap/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
  190. package/packages/mailx-imap/node_modules/nodemailer/lib/well-known/index.js +0 -47
  191. package/packages/mailx-imap/node_modules/nodemailer/lib/well-known/services.json +0 -611
  192. package/packages/mailx-imap/node_modules/nodemailer/lib/xoauth2/index.js +0 -427
  193. package/packages/mailx-imap/node_modules/nodemailer/package.json +0 -47
  194. package/packages/mailx-send/node_modules/nodemailer/.ncurc.js +0 -9
  195. package/packages/mailx-send/node_modules/nodemailer/.prettierignore +0 -8
  196. package/packages/mailx-send/node_modules/nodemailer/.prettierrc +0 -12
  197. package/packages/mailx-send/node_modules/nodemailer/.prettierrc.js +0 -10
  198. package/packages/mailx-send/node_modules/nodemailer/.release-please-config.json +0 -9
  199. package/packages/mailx-send/node_modules/nodemailer/LICENSE +0 -16
  200. package/packages/mailx-send/node_modules/nodemailer/README.md +0 -86
  201. package/packages/mailx-send/node_modules/nodemailer/SECURITY.txt +0 -22
  202. package/packages/mailx-send/node_modules/nodemailer/eslint.config.js +0 -88
  203. package/packages/mailx-send/node_modules/nodemailer/lib/addressparser/index.js +0 -383
  204. package/packages/mailx-send/node_modules/nodemailer/lib/base64/index.js +0 -139
  205. package/packages/mailx-send/node_modules/nodemailer/lib/dkim/index.js +0 -253
  206. package/packages/mailx-send/node_modules/nodemailer/lib/dkim/message-parser.js +0 -155
  207. package/packages/mailx-send/node_modules/nodemailer/lib/dkim/relaxed-body.js +0 -154
  208. package/packages/mailx-send/node_modules/nodemailer/lib/dkim/sign.js +0 -117
  209. package/packages/mailx-send/node_modules/nodemailer/lib/fetch/cookies.js +0 -281
  210. package/packages/mailx-send/node_modules/nodemailer/lib/fetch/index.js +0 -280
  211. package/packages/mailx-send/node_modules/nodemailer/lib/json-transport/index.js +0 -82
  212. package/packages/mailx-send/node_modules/nodemailer/lib/mail-composer/index.js +0 -629
  213. package/packages/mailx-send/node_modules/nodemailer/lib/mailer/index.js +0 -441
  214. package/packages/mailx-send/node_modules/nodemailer/lib/mailer/mail-message.js +0 -316
  215. package/packages/mailx-send/node_modules/nodemailer/lib/mime-funcs/index.js +0 -625
  216. package/packages/mailx-send/node_modules/nodemailer/lib/mime-funcs/mime-types.js +0 -2113
  217. package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/index.js +0 -1316
  218. package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/last-newline.js +0 -33
  219. package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/le-unix.js +0 -43
  220. package/packages/mailx-send/node_modules/nodemailer/lib/mime-node/le-windows.js +0 -52
  221. package/packages/mailx-send/node_modules/nodemailer/lib/nodemailer.js +0 -157
  222. package/packages/mailx-send/node_modules/nodemailer/lib/punycode/index.js +0 -460
  223. package/packages/mailx-send/node_modules/nodemailer/lib/qp/index.js +0 -227
  224. package/packages/mailx-send/node_modules/nodemailer/lib/sendmail-transport/index.js +0 -210
  225. package/packages/mailx-send/node_modules/nodemailer/lib/ses-transport/index.js +0 -234
  226. package/packages/mailx-send/node_modules/nodemailer/lib/shared/index.js +0 -754
  227. package/packages/mailx-send/node_modules/nodemailer/lib/smtp-connection/data-stream.js +0 -108
  228. package/packages/mailx-send/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +0 -143
  229. package/packages/mailx-send/node_modules/nodemailer/lib/smtp-connection/index.js +0 -1870
  230. package/packages/mailx-send/node_modules/nodemailer/lib/smtp-pool/index.js +0 -652
  231. package/packages/mailx-send/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +0 -259
  232. package/packages/mailx-send/node_modules/nodemailer/lib/smtp-transport/index.js +0 -421
  233. package/packages/mailx-send/node_modules/nodemailer/lib/stream-transport/index.js +0 -135
  234. package/packages/mailx-send/node_modules/nodemailer/lib/well-known/index.js +0 -47
  235. package/packages/mailx-send/node_modules/nodemailer/lib/well-known/services.json +0 -611
  236. package/packages/mailx-send/node_modules/nodemailer/lib/xoauth2/index.js +0 -427
  237. package/packages/mailx-send/node_modules/nodemailer/package.json +0 -47
package/bin/mailx.js CHANGED
@@ -1,25 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * mailx — email client launcher
3
+ * mailx — email client
4
4
  *
5
5
  * Usage:
6
- * mailx Start unified (in-process server + open browser)
7
- * mailx --server Start as a standalone server only (no browser)
8
- * mailx --no-browser Start server in-process, don't open browser
9
- * mailx --external Bind to all interfaces (default: localhost only)
6
+ * mailx Launch native app (IPC, no server)
7
+ * mailx --server Start Express server + open browser
8
+ * mailx --no-browser Server mode, don't open browser
9
+ * mailx --external Bind to all interfaces (server mode only)
10
10
  */
11
11
 
12
+ import fs from "node:fs";
13
+ import path from "node:path";
12
14
  import net from "node:net";
13
15
 
14
16
  const PORT = 9333;
15
17
  const args = process.argv.slice(2);
18
+ const serverMode = args.includes("--server");
19
+ const noBrowser = args.includes("--no-browser");
16
20
 
17
21
  function isPortInUse(port) {
18
22
  return new Promise((resolve) => {
19
- const server = net.createServer();
20
- server.once("error", () => resolve(true));
21
- server.once("listening", () => { server.close(); resolve(false); });
22
- server.listen(port, "127.0.0.1");
23
+ const socket = net.createConnection({ port, host: "127.0.0.1" });
24
+ socket.once("connect", () => { socket.destroy(); resolve(true); });
25
+ socket.once("error", () => resolve(false));
23
26
  });
24
27
  }
25
28
 
@@ -32,33 +35,48 @@ function openBrowser(url) {
32
35
  }
33
36
 
34
37
  async function main() {
35
- const serverOnly = args.includes("--server");
36
- const noBrowser = args.includes("--no-browser") || serverOnly;
38
+ if (serverMode) {
39
+ // Server mode Express + WebSocket, open browser
40
+ const inUse = await isPortInUse(PORT);
41
+ if (inUse) {
42
+ console.log("mailx server already running");
43
+ if (!noBrowser) openBrowser(`http://localhost:${PORT}`);
44
+ return;
45
+ }
46
+
47
+ console.log("Starting mailx server...");
48
+ if (args.includes("--external")) process.argv.push("--external");
49
+ await import("../packages/mailx-server/index.js");
37
50
 
38
- const inUse = await isPortInUse(PORT);
39
- if (inUse) {
40
- console.log("mailx is already running");
41
51
  if (!noBrowser) {
52
+ for (let i = 0; i < 30; i++) {
53
+ await new Promise(r => setTimeout(r, 200));
54
+ if (await isPortInUse(PORT)) break;
55
+ }
42
56
  openBrowser(`http://localhost:${PORT}`);
43
- console.log("mailx opened");
57
+ console.log("mailx opened (browser)");
44
58
  }
45
- return;
46
- }
47
-
48
- console.log("Starting mailx...");
49
-
50
- if (args.includes("--external")) process.argv.push("--external");
59
+ } else {
60
+ // Default: launch native WebView app with IPC
61
+ const launcherPaths = [
62
+ path.join(import.meta.dirname, "..", "launcher", "bin", "mailx-app.exe"),
63
+ path.join(import.meta.dirname, "..", "launcher", "target", "debug", "mailx-app.exe"),
64
+ ];
51
65
 
52
- // Import server directly relative path, in-process
53
- await import("../packages/mailx-server/index.js");
66
+ let launcherPath = launcherPaths.find(p => fs.existsSync(p));
54
67
 
55
- if (!noBrowser) {
56
- for (let i = 0; i < 30; i++) {
57
- await new Promise(r => setTimeout(r, 200));
58
- if (await isPortInUse(PORT)) break;
68
+ if (launcherPath) {
69
+ console.log("Starting mailx...");
70
+ const { spawn } = await import("node:child_process");
71
+ const child = spawn(launcherPath, args, { detached: true, stdio: "ignore" });
72
+ child.unref();
73
+ console.log("mailx launched");
74
+ } else {
75
+ // No native launcher — fall back to server mode + browser
76
+ console.log("Native launcher not found, falling back to server mode");
77
+ process.argv.push("--server");
78
+ await main(); // recurse with --server
59
79
  }
60
- openBrowser(`http://localhost:${PORT}`);
61
- console.log("mailx opened");
62
80
  }
63
81
  }
64
82
 
package/client/app.js CHANGED
@@ -8,16 +8,19 @@ import { showMessage, getCurrentMessage } from "./components/message-viewer.js";
8
8
  import { connectWebSocket, onWsEvent, triggerSync, getAccounts } from "./lib/api-client.js";
9
9
  // ── Wire up components ──
10
10
  const folderTree = document.getElementById("folder-tree");
11
+ let currentFolderSpecialUse = "";
11
12
  initFolderTree(folderTree, (accountId, folderId, folderName, specialUse) => {
13
+ currentFolderSpecialUse = specialUse;
12
14
  loadMessages(accountId, folderId, 1, specialUse);
13
15
  document.title = `mailx - ${folderName}`;
14
16
  }, () => {
15
17
  // Unified inbox handler
18
+ currentFolderSpecialUse = "inbox";
16
19
  loadUnifiedInbox();
17
20
  document.title = "mailx - All Inboxes";
18
21
  });
19
- initMessageList((accountId, uid) => {
20
- showMessage(accountId, uid);
22
+ initMessageList((accountId, uid, folderId) => {
23
+ showMessage(accountId, uid, folderId, currentFolderSpecialUse);
21
24
  // Enable action buttons when a message is selected
22
25
  for (const id of ["btn-reply", "btn-reply-all", "btn-forward", "btn-delete", "btn-flag"]) {
23
26
  const btn = document.getElementById(id);
@@ -144,14 +147,27 @@ async function deleteCurrentMessage() {
144
147
  if (statusSync?.textContent?.includes("Ctrl+Z"))
145
148
  statusSync.textContent = "";
146
149
  }, 30000);
147
- // Clear the preview pane
148
- const bodyEl = document.getElementById("mv-body");
149
- const headerEl = document.getElementById("mv-header");
150
- if (bodyEl)
151
- bodyEl.innerHTML = `<div class="mv-empty">Select a message to read</div>`;
152
- if (headerEl)
153
- headerEl.hidden = true;
154
- reloadCurrentFolder();
150
+ // Remove the deleted row and select the next message
151
+ const mlBody = document.getElementById("ml-body");
152
+ if (mlBody) {
153
+ const deletedRow = mlBody.querySelector(`.ml-row[data-uid="${message.uid}"]`);
154
+ if (deletedRow) {
155
+ const nextRow = (deletedRow.nextElementSibling || deletedRow.previousElementSibling);
156
+ deletedRow.remove();
157
+ if (nextRow?.classList.contains("ml-row")) {
158
+ nextRow.click();
159
+ }
160
+ else {
161
+ // No more messages — clear preview
162
+ const bodyEl = document.getElementById("mv-body");
163
+ const headerEl = document.getElementById("mv-header");
164
+ if (bodyEl)
165
+ bodyEl.innerHTML = `<div class="mv-empty">Select a message to read</div>`;
166
+ if (headerEl)
167
+ headerEl.hidden = true;
168
+ }
169
+ }
170
+ }
155
171
  }
156
172
  catch (e) {
157
173
  console.error(`Delete failed: ${e.message}`);
@@ -220,6 +236,52 @@ searchInput?.addEventListener("keydown", (e) => {
220
236
  reloadCurrentFolder();
221
237
  }
222
238
  });
239
+ // ── Reload message list after drag-move ──
240
+ document.addEventListener("mailx-message-moved", () => reloadCurrentFolder());
241
+ // ── Folder filter ──
242
+ const ftFilterInput = document.getElementById("ft-filter-input");
243
+ if (ftFilterInput) {
244
+ ftFilterInput.addEventListener("input", () => {
245
+ const query = ftFilterInput.value.toLowerCase();
246
+ const tree = document.getElementById("folder-tree");
247
+ if (!tree)
248
+ return;
249
+ if (!query) {
250
+ // Clear filter — show everything
251
+ tree.querySelectorAll(".ft-filter-hidden").forEach(el => el.classList.remove("ft-filter-hidden"));
252
+ return;
253
+ }
254
+ // Hide all folders first, then show matches + their parent accounts
255
+ const folders = tree.querySelectorAll(".ft-folder");
256
+ const accounts = tree.querySelectorAll(".ft-account");
257
+ for (const acct of accounts)
258
+ acct.classList.add("ft-filter-hidden");
259
+ for (const f of folders)
260
+ f.classList.add("ft-filter-hidden");
261
+ for (const f of folders) {
262
+ const name = f.querySelector(".ft-folder-name")?.textContent?.toLowerCase() || "";
263
+ if (name.includes(query)) {
264
+ f.classList.remove("ft-filter-hidden");
265
+ // Show parent account
266
+ const acct = f.closest(".ft-account");
267
+ if (acct)
268
+ acct.classList.remove("ft-filter-hidden");
269
+ }
270
+ }
271
+ // Also show unified inbox if it matches
272
+ const unified = tree.querySelector(".ft-unified");
273
+ if (unified) {
274
+ const text = unified.textContent?.toLowerCase() || "";
275
+ unified.classList.toggle("ft-filter-hidden", !text.includes(query));
276
+ }
277
+ });
278
+ ftFilterInput.addEventListener("keydown", (e) => {
279
+ if (e.key === "Escape") {
280
+ ftFilterInput.value = "";
281
+ ftFilterInput.dispatchEvent(new Event("input"));
282
+ }
283
+ });
284
+ }
223
285
  // ── Open links from email body in system browser ──
224
286
  window.addEventListener("message", (e) => {
225
287
  if (e.data?.type === "openLink" && e.data.url) {
@@ -260,16 +322,20 @@ if (splitter) {
260
322
  connectWebSocket();
261
323
  onWsEvent((event) => {
262
324
  const statusSync = document.getElementById("status-sync");
325
+ const startupStatus = document.getElementById("startup-status");
263
326
  switch (event.type) {
264
327
  case "connected":
265
328
  if (statusSync)
266
329
  statusSync.textContent = "Connected";
267
- // Refresh folder counts on reconnect (non-disruptive)
268
- refreshFolderTree();
330
+ if (startupStatus)
331
+ startupStatus.textContent = "Loading accounts...";
332
+ // Don't refresh folder tree on connect — it's already loaded by initFolderTree
269
333
  break;
270
334
  case "syncProgress":
271
335
  if (statusSync)
272
336
  statusSync.textContent = `Sync: ${event.phase} ${event.progress}%`;
337
+ if (startupStatus)
338
+ startupStatus.textContent = `Syncing ${event.accountId}: ${event.phase}`;
273
339
  break;
274
340
  case "folderCountsChanged": {
275
341
  refreshFolderTree();
@@ -402,7 +468,10 @@ fetch("/api/version").then(r => r.json()).then(d => {
402
468
  el.textContent = `mailx s${d.server}/c${d.client}${isApp ? "" : " [browser]"}`;
403
469
  }).catch(async () => {
404
470
  // Server not running — try to start it if we're in the app
471
+ const startupStatus = document.getElementById("startup-status");
405
472
  if (isApp) {
473
+ if (startupStatus)
474
+ startupStatus.textContent = "Starting server...";
406
475
  await mailxapi.ensureServer();
407
476
  location.reload();
408
477
  }
@@ -410,6 +479,8 @@ fetch("/api/version").then(r => r.json()).then(d => {
410
479
  const el = document.getElementById("app-version");
411
480
  if (el)
412
481
  el.textContent = "mailx [server offline]";
482
+ if (startupStatus)
483
+ startupStatus.textContent = "Server offline — start with: node packages/mailx-server/index.js";
413
484
  }
414
485
  });
415
486
  // ── Sync pending indicator ──
@@ -427,5 +498,14 @@ setInterval(async () => {
427
498
  }
428
499
  catch { /* offline */ }
429
500
  }, 5000);
430
- console.log("mailx client initialized");
501
+ console.log("mailx client initialized, location:", location.href);
502
+ // Diagnostic: test API connectivity (helps debug WebView2 blank screen)
503
+ fetch("/api/version").then(r => r.json()).then(d => {
504
+ console.log("API reachable:", d);
505
+ }).catch(e => {
506
+ console.error("API unreachable:", e.message);
507
+ const status = document.getElementById("startup-status");
508
+ if (status)
509
+ status.textContent = `Cannot reach server API: ${e.message}`;
510
+ });
431
511
  //# sourceMappingURL=app.js.map
@@ -123,7 +123,16 @@ function renderNode(node, container, depth) {
123
123
  nameSpan.className = "ft-folder-name";
124
124
  nameSpan.textContent = node.name;
125
125
  folderEl.appendChild(nameSpan);
126
- if (node.unreadCount > 0) {
126
+ const isOutbox = node.specialUse === "outbox" || node.path.toLowerCase() === "outbox";
127
+ if (isOutbox && node.totalCount > 0) {
128
+ // Outbox: show total (pending) count with warning style
129
+ const badge = document.createElement("span");
130
+ badge.className = "ft-badge ft-badge-outbox";
131
+ badge.textContent = String(node.totalCount);
132
+ badge.title = `${node.totalCount} pending`;
133
+ folderEl.appendChild(badge);
134
+ }
135
+ else if (node.unreadCount > 0) {
127
136
  const badge = document.createElement("span");
128
137
  badge.className = "ft-badge";
129
138
  badge.textContent = String(node.unreadCount);
@@ -147,6 +156,58 @@ function renderNode(node, container, depth) {
147
156
  selectedFolderId = node.id;
148
157
  onFolderSelect(node.accountId, node.id, node.name, node.specialUse || node.path.toLowerCase());
149
158
  });
159
+ // ── Drop target for message drag-and-drop ──
160
+ if (node.id !== -1) {
161
+ folderEl.addEventListener("dragover", (e) => {
162
+ e.preventDefault();
163
+ e.dataTransfer.dropEffect = e.ctrlKey ? "copy" : "move";
164
+ folderEl.classList.add("drop-target");
165
+ });
166
+ folderEl.addEventListener("dragleave", () => folderEl.classList.remove("drop-target"));
167
+ folderEl.addEventListener("drop", async (e) => {
168
+ e.preventDefault();
169
+ folderEl.classList.remove("drop-target");
170
+ // Multi-message or single-message drop
171
+ const multiData = e.dataTransfer.getData("application/x-mailx-messages");
172
+ const singleData = e.dataTransfer.getData("application/x-mailx-message");
173
+ const messages = multiData ? JSON.parse(multiData) : singleData ? [JSON.parse(singleData)] : [];
174
+ // Filter: not already in target folder
175
+ const toMove = messages.filter(m => m.folderId !== node.id || m.accountId !== node.accountId);
176
+ if (toMove.length === 0)
177
+ return;
178
+ const statusEl = document.getElementById("status-sync");
179
+ const crossAccount = toMove.some(m => m.accountId !== node.accountId);
180
+ try {
181
+ let moved = 0;
182
+ for (const msg of toMove) {
183
+ const body = { targetFolderId: node.id };
184
+ if (msg.accountId !== node.accountId)
185
+ body.targetAccountId = node.accountId;
186
+ const res = await fetch(`/api/message/${msg.accountId}/${msg.uid}/move`, {
187
+ method: "POST",
188
+ headers: { "Content-Type": "application/json" },
189
+ body: JSON.stringify(body),
190
+ });
191
+ if (!res.ok) {
192
+ const err = await res.json().catch(() => ({ error: res.statusText }));
193
+ throw new Error(err.error);
194
+ }
195
+ moved++;
196
+ }
197
+ if (statusEl)
198
+ statusEl.textContent = `Moved ${moved} message${moved > 1 ? "s" : ""} to ${node.name}`;
199
+ const treeContainer = document.getElementById("folder-tree");
200
+ if (treeContainer)
201
+ loadFolderTree(treeContainer);
202
+ document.dispatchEvent(new CustomEvent("mailx-message-moved"));
203
+ }
204
+ catch (err) {
205
+ console.error(`Move failed: ${err.message}`);
206
+ if (statusEl)
207
+ statusEl.textContent = `Move failed: ${err.message}`;
208
+ }
209
+ });
210
+ }
150
211
  container.appendChild(folderEl);
151
212
  // Render children if expanded
152
213
  if (hasChildren && isExpanded) {
@@ -161,13 +222,19 @@ export function initFolderTree(container, handler, unifiedHandler) {
161
222
  loadFolderTree(container);
162
223
  }
163
224
  async function loadFolderTree(container) {
164
- container.innerHTML = "";
225
+ // Show loading state while preserving existing tree (if any) on refresh
226
+ const hadContent = container.children.length > 0 && !container.querySelector(".folder-loading");
227
+ if (!hadContent) {
228
+ container.innerHTML = `<div class="folder-loading">Loading accounts...</div>`;
229
+ }
165
230
  try {
166
231
  const accounts = await getAccounts();
167
232
  if (accounts.length === 0) {
168
233
  container.innerHTML = `<div class="folder-loading">No accounts configured.<br>Edit ~/.mailx/settings.jsonc</div>`;
169
234
  return;
170
235
  }
236
+ // Clear loading state now that we have data
237
+ container.innerHTML = "";
171
238
  // Unified Inbox (if multiple accounts)
172
239
  if (accounts.length > 1) {
173
240
  const unifiedEl = document.createElement("div");
@@ -192,7 +259,7 @@ async function loadFolderTree(container) {
192
259
  const accountExpanded = expandState[accountKey] !== false; // accounts default expanded
193
260
  const header = document.createElement("div");
194
261
  header.className = "ft-account-header";
195
- header.textContent = `${accountExpanded ? "▾" : "▸"} ${account.name}`;
262
+ header.textContent = `${accountExpanded ? "▾" : "▸"} ${account.label || account.name}`;
196
263
  header.addEventListener("click", () => {
197
264
  expandState[accountKey] = !accountExpanded;
198
265
  saveExpandState();
@@ -272,9 +339,23 @@ async function loadFolderTree(container) {
272
339
  if (target)
273
340
  target.click();
274
341
  }
342
+ // Dismiss startup overlay once tree is loaded
343
+ const overlay = document.getElementById("startup-overlay");
344
+ if (overlay)
345
+ overlay.classList.add("hidden");
346
+ // Remove from DOM after transition
347
+ setTimeout(() => overlay?.remove(), 400);
275
348
  }
276
349
  catch (e) {
277
350
  container.innerHTML = `<div class="folder-loading">Error loading folders: ${e.message}</div>`;
351
+ // Dismiss overlay on error too — show the error, not a spinner
352
+ const overlay = document.getElementById("startup-overlay");
353
+ if (overlay) {
354
+ const status = document.getElementById("startup-status");
355
+ if (status)
356
+ status.textContent = `Error: ${e.message}`;
357
+ setTimeout(() => { overlay.classList.add("hidden"); setTimeout(() => overlay.remove(), 400); }, 2000);
358
+ }
278
359
  }
279
360
  }
280
361
  /** Refresh folder tree (e.g., after sync) */
@@ -3,10 +3,24 @@
3
3
  * Loads more messages on scroll.
4
4
  */
5
5
  import { getMessages, getUnifiedInbox, searchMessages } from "../lib/api-client.js";
6
+ function clearFilter() {
7
+ const f = document.getElementById("ml-filter-input");
8
+ if (f)
9
+ f.value = "";
10
+ }
11
+ /** Clear the message viewer when no message is selected */
12
+ function clearViewer() {
13
+ const bodyEl = document.getElementById("mv-body");
14
+ const headerEl = document.getElementById("mv-header");
15
+ if (bodyEl)
16
+ bodyEl.innerHTML = `<div class="mv-empty">Select a message to read</div>`;
17
+ if (headerEl)
18
+ headerEl.hidden = true;
19
+ }
6
20
  let onMessageSelect;
7
21
  let currentAccountId;
8
22
  let currentFolderId;
9
- let selectedRow;
23
+ let lastClickedRow = null;
10
24
  let currentPage;
11
25
  let totalMessages;
12
26
  let loading = false;
@@ -14,11 +28,62 @@ let unifiedMode = false;
14
28
  let searchMode = false;
15
29
  let currentSearchQuery = "";
16
30
  let showToInsteadOfFrom = false;
31
+ /** Get all selected message rows */
32
+ export function getSelectedMessages() {
33
+ const body = document.getElementById("ml-body");
34
+ if (!body)
35
+ return [];
36
+ const rows = body.querySelectorAll(".ml-row.selected");
37
+ return Array.from(rows).map(r => ({
38
+ accountId: r.dataset.accountId || "",
39
+ uid: Number(r.dataset.uid),
40
+ folderId: Number(r.dataset.folderId),
41
+ }));
42
+ }
43
+ function clearSelection() {
44
+ const body = document.getElementById("ml-body");
45
+ if (body)
46
+ body.querySelectorAll(".ml-row.selected").forEach(r => r.classList.remove("selected"));
47
+ }
48
+ function selectRange(from, to) {
49
+ const body = document.getElementById("ml-body");
50
+ if (!body)
51
+ return;
52
+ const rows = Array.from(body.querySelectorAll(".ml-row"));
53
+ const fromIdx = rows.indexOf(from);
54
+ const toIdx = rows.indexOf(to);
55
+ if (fromIdx < 0 || toIdx < 0)
56
+ return;
57
+ const lo = Math.min(fromIdx, toIdx);
58
+ const hi = Math.max(fromIdx, toIdx);
59
+ for (let i = lo; i <= hi; i++)
60
+ rows[i].classList.add("selected");
61
+ }
17
62
  const timeFmt = { hour: "2-digit", minute: "2-digit", hour12: false };
18
63
  const dateFmt = { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false };
19
64
  const dateFmtSameYear = { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false };
20
65
  export function initMessageList(handler) {
21
66
  onMessageSelect = handler;
67
+ // Client-side filter
68
+ const filterInput = document.getElementById("ml-filter-input");
69
+ if (filterInput) {
70
+ filterInput.addEventListener("input", () => {
71
+ const query = filterInput.value.toLowerCase();
72
+ const body = document.getElementById("ml-body");
73
+ if (!body)
74
+ return;
75
+ for (const row of body.querySelectorAll(".ml-row")) {
76
+ const text = row.textContent?.toLowerCase() || "";
77
+ row.classList.toggle("filter-hidden", query.length > 0 && !text.includes(query));
78
+ }
79
+ });
80
+ filterInput.addEventListener("keydown", (e) => {
81
+ if (e.key === "Escape") {
82
+ filterInput.value = "";
83
+ filterInput.dispatchEvent(new Event("input"));
84
+ }
85
+ });
86
+ }
22
87
  // Infinite scroll
23
88
  const body = document.getElementById("ml-body");
24
89
  if (body) {
@@ -46,6 +111,7 @@ export function reloadCurrentFolder() {
46
111
  }
47
112
  /** Load unified inbox (all accounts) */
48
113
  export async function loadUnifiedInbox() {
114
+ clearFilter();
49
115
  unifiedMode = true;
50
116
  currentPage = 1;
51
117
  totalMessages = 0;
@@ -58,17 +124,24 @@ export async function loadUnifiedInbox() {
58
124
  totalMessages = result.total;
59
125
  if (result.items.length === 0) {
60
126
  body.innerHTML = `<div class="ml-empty">No messages</div>`;
127
+ clearViewer();
61
128
  return;
62
129
  }
63
130
  body.innerHTML = "";
64
131
  appendMessages(body, "", result.items);
132
+ const firstRow = body.querySelector(".ml-row");
133
+ if (firstRow)
134
+ firstRow.click();
65
135
  }
66
136
  catch (e) {
137
+ if (e.name === "AbortError")
138
+ return;
67
139
  body.innerHTML = `<div class="ml-empty">Error: ${e.message}</div>`;
68
140
  }
69
141
  }
70
142
  /** Load search results */
71
143
  export async function loadSearchResults(query) {
144
+ clearFilter();
72
145
  searchMode = true;
73
146
  unifiedMode = false;
74
147
  currentSearchQuery = query;
@@ -93,6 +166,7 @@ export async function loadSearchResults(query) {
93
166
  }
94
167
  }
95
168
  export async function loadMessages(accountId, folderId, page = 1, specialUse = "") {
169
+ clearFilter();
96
170
  searchMode = false;
97
171
  unifiedMode = false;
98
172
  showToInsteadOfFrom = ["sent", "drafts", "outbox"].includes(specialUse) ||
@@ -114,12 +188,19 @@ export async function loadMessages(accountId, folderId, page = 1, specialUse = "
114
188
  totalMessages = result.total;
115
189
  if (result.items.length === 0) {
116
190
  body.innerHTML = `<div class="ml-empty">No messages</div>`;
191
+ clearViewer();
117
192
  return;
118
193
  }
119
194
  body.innerHTML = "";
120
195
  appendMessages(body, accountId, result.items);
196
+ // Auto-select first message
197
+ const firstRow = body.querySelector(".ml-row");
198
+ if (firstRow)
199
+ firstRow.click();
121
200
  }
122
201
  catch (e) {
202
+ if (e.name === "AbortError")
203
+ return; // Superseded by newer request
123
204
  body.innerHTML = `<div class="ml-empty">Error: ${e.message}</div>`;
124
205
  }
125
206
  }
@@ -146,13 +227,18 @@ async function loadMoreMessages() {
146
227
  }
147
228
  function appendMessages(body, accountId, items) {
148
229
  for (const msg of items) {
230
+ // Use per-message accountId for unified inbox, fallback to list-level accountId
231
+ const msgAccountId = msg.accountId || accountId;
149
232
  const row = document.createElement("div");
150
233
  row.className = "ml-row";
234
+ row.draggable = true;
151
235
  if (!msg.flags.includes("\\Seen"))
152
236
  row.classList.add("unread");
153
237
  if (msg.flags.includes("\\Flagged"))
154
238
  row.classList.add("flagged");
155
239
  row.dataset.uid = String(msg.uid);
240
+ row.dataset.accountId = msgAccountId;
241
+ row.dataset.folderId = String(msg.folderId);
156
242
  const flag = document.createElement("span");
157
243
  flag.className = "ml-flag";
158
244
  flag.textContent = msg.flags.includes("\\Flagged") ? "\u2605" : "\u2606"; // ★ or ☆
@@ -185,7 +271,7 @@ function appendMessages(body, accountId, items) {
185
271
  ? currentFlags.filter((f) => f !== "\\Flagged")
186
272
  : [...currentFlags, "\\Flagged"];
187
273
  try {
188
- await fetch(`/api/message/${accountId}/${msg.uid}/flags`, {
274
+ await fetch(`/api/message/${msgAccountId}/${msg.uid}/flags`, {
189
275
  method: "PATCH",
190
276
  headers: { "Content-Type": "application/json" },
191
277
  body: JSON.stringify({ flags: newFlags }),
@@ -200,14 +286,54 @@ function appendMessages(body, accountId, items) {
200
286
  row.appendChild(from);
201
287
  row.appendChild(date);
202
288
  row.appendChild(subject);
203
- row.addEventListener("click", () => {
204
- if (selectedRow)
205
- selectedRow.classList.remove("selected");
206
- row.classList.add("selected");
207
- selectedRow = row;
289
+ row.addEventListener("click", (e) => {
290
+ if (e.shiftKey && lastClickedRow) {
291
+ // Shift+click: range select from last clicked to this row
292
+ clearSelection();
293
+ selectRange(lastClickedRow, row);
294
+ }
295
+ else if (e.ctrlKey || e.metaKey) {
296
+ // Ctrl+click: toggle this row
297
+ row.classList.toggle("selected");
298
+ }
299
+ else {
300
+ // Plain click: single select
301
+ clearSelection();
302
+ row.classList.add("selected");
303
+ }
304
+ lastClickedRow = row;
208
305
  row.classList.remove("unread");
209
- onMessageSelect(accountId, msg.uid);
306
+ onMessageSelect(msgAccountId, msg.uid, msg.folderId);
307
+ });
308
+ row.addEventListener("dragstart", (e) => {
309
+ // If dragging a non-selected row, select it first
310
+ if (!row.classList.contains("selected")) {
311
+ clearSelection();
312
+ row.classList.add("selected");
313
+ lastClickedRow = row;
314
+ }
315
+ const selected = getSelectedMessages();
316
+ e.dataTransfer.setData("application/x-mailx-messages", JSON.stringify(selected));
317
+ // Legacy single-message format for backwards compat
318
+ e.dataTransfer.setData("application/x-mailx-message", JSON.stringify({
319
+ accountId: msgAccountId,
320
+ uid: msg.uid,
321
+ folderId: msg.folderId,
322
+ subject: msg.subject,
323
+ }));
324
+ e.dataTransfer.effectAllowed = "copyMove";
325
+ row.classList.add("dragging");
326
+ // Show drag count
327
+ if (selected.length > 1) {
328
+ const badge = document.createElement("div");
329
+ badge.textContent = `${selected.length} messages`;
330
+ badge.style.cssText = "position:absolute;top:-1000px;background:#333;color:white;padding:4px 8px;border-radius:4px;font-size:12px";
331
+ document.body.appendChild(badge);
332
+ e.dataTransfer.setDragImage(badge, 0, 0);
333
+ setTimeout(() => badge.remove(), 0);
334
+ }
210
335
  });
336
+ row.addEventListener("dragend", () => row.classList.remove("dragging"));
211
337
  body.appendChild(row);
212
338
  }
213
339
  }