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