@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,1257 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Message viewer component -- displays full message in sandboxed iframe.
|
|
3
|
-
* Subscribes to message-state: clears when selected becomes null.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { getMessage, updateFlags, allowRemoteContent, flagSenderOrDomain, getAttachment, addContact, listContacts, upsertContact, unsubscribeOneClick, addPreferredContact } from "../lib/api-client.js";
|
|
7
|
-
import { showContextMenu } from "./context-menu.js";
|
|
8
|
-
import type { MenuItem } from "./context-menu.js";
|
|
9
|
-
import * as state from "../lib/message-state.js";
|
|
10
|
-
|
|
11
|
-
/** Currently displayed message (for reply/forward) */
|
|
12
|
-
let currentMessage: any = null;
|
|
13
|
-
let currentAccountId: string = "";
|
|
14
|
-
let showMessageGeneration = 0; // Cancel stale fetches
|
|
15
|
-
let retryCount = 0;
|
|
16
|
-
|
|
17
|
-
export function getCurrentMessage(): { accountId: string; message: any } | null {
|
|
18
|
-
if (!currentMessage) return null;
|
|
19
|
-
return { accountId: currentAccountId, message: currentMessage };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Initialize viewer — subscribe to state changes */
|
|
23
|
-
export function initViewer(): void {
|
|
24
|
-
state.subscribe((change) => {
|
|
25
|
-
if (change === "removed") {
|
|
26
|
-
// Message was deleted/moved — show auto-selected next, or clear
|
|
27
|
-
const sel = state.getSelected();
|
|
28
|
-
if (!sel) {
|
|
29
|
-
clearViewer();
|
|
30
|
-
} else if (sel.uid !== currentMessage?.uid || sel.accountId !== currentAccountId) {
|
|
31
|
-
showMessage(sel.accountId, sel.uid, sel.folderId);
|
|
32
|
-
}
|
|
33
|
-
} else if (change === "selected") {
|
|
34
|
-
// Explicit deselect (folder switch, clearViewer)
|
|
35
|
-
if (!state.getSelected()) {
|
|
36
|
-
clearViewer();
|
|
37
|
-
}
|
|
38
|
-
} else if (change === "messages") {
|
|
39
|
-
// List was replaced (search, folder switch, sync reload). If the
|
|
40
|
-
// currently-displayed message is no longer in the list, clear the
|
|
41
|
-
// viewer — otherwise the user sees a preview that doesn't match
|
|
42
|
-
// any visible row. (The "search-clears-preview" bug class.)
|
|
43
|
-
// setMessages already deselects in this case; we just need to
|
|
44
|
-
// notice and clear here since the viewer ignored "messages" before.
|
|
45
|
-
if (currentMessage && !state.getSelected()) {
|
|
46
|
-
clearViewer();
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Zoom is persisted across messages via localStorage
|
|
53
|
-
const ZOOM_KEY = "mailx-preview-zoom";
|
|
54
|
-
const ZOOM_MIN = 0.5;
|
|
55
|
-
const ZOOM_MAX = 3.0;
|
|
56
|
-
const ZOOM_STEP = 0.1;
|
|
57
|
-
let previewZoom = clampZoom(parseFloat(localStorage.getItem(ZOOM_KEY) || "1"));
|
|
58
|
-
|
|
59
|
-
function clampZoom(z: number): number {
|
|
60
|
-
if (!Number.isFinite(z) || z <= 0) return 1;
|
|
61
|
-
return Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, Math.round(z * 100) / 100));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function applyZoom(doc: Document): void {
|
|
65
|
-
// Zoom lives on <html>, not <body>, because WebView2/Chromium's scroll
|
|
66
|
-
// container is the root element — body.style.zoom leaves the scroll
|
|
67
|
-
// container out of sync with the zoomed content, breaking scrollbar
|
|
68
|
-
// display and wheel scrolling. documentElement.style.zoom keeps them
|
|
69
|
-
// aligned so the iframe scrolls normally at any zoom level.
|
|
70
|
-
if (doc.documentElement) (doc.documentElement.style as any).zoom = String(previewZoom);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function setZoom(z: number, doc: Document): void {
|
|
74
|
-
previewZoom = clampZoom(z);
|
|
75
|
-
localStorage.setItem(ZOOM_KEY, String(previewZoom));
|
|
76
|
-
applyZoom(doc);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Install preview iframe controls: key forwarding to parent, Ctrl+wheel zoom,
|
|
80
|
-
* keyboard zoom shortcuts (Ctrl+= / Ctrl+- / Ctrl+0), and the right-click menu. */
|
|
81
|
-
/** Run AI translate on `text` and show result in a small modal. Disabled
|
|
82
|
-
* by default — user enables via Settings (translateEnabled in
|
|
83
|
-
* AutocompleteSettings). When disabled, the modal explains how to enable. */
|
|
84
|
-
async function translateAndShow(text: string): Promise<void> {
|
|
85
|
-
if (!text.trim()) return;
|
|
86
|
-
const status = document.getElementById("status-sync");
|
|
87
|
-
if (status) status.textContent = "Translating…";
|
|
88
|
-
|
|
89
|
-
const overlay = document.createElement("div");
|
|
90
|
-
overlay.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:10000;display:flex;align-items:center;justify-content:center;";
|
|
91
|
-
const modal = document.createElement("div");
|
|
92
|
-
modal.style.cssText = "background:var(--bg, #fff);color:var(--fg, #000);border:1px solid var(--border, #ccc);border-radius:6px;width:560px;max-width:90vw;max-height:70vh;display:flex;flex-direction:column;box-shadow:0 4px 24px rgba(0,0,0,0.3);";
|
|
93
|
-
const header = document.createElement("div");
|
|
94
|
-
header.style.cssText = "padding:10px 14px;border-bottom:1px solid var(--border, #ddd);font-weight:600;display:flex;justify-content:space-between;align-items:center;";
|
|
95
|
-
header.innerHTML = `<span>Translation</span><button style="cursor:pointer;border:0;background:transparent;font-size:16px;" aria-label="Close">×</button>`;
|
|
96
|
-
const body = document.createElement("div");
|
|
97
|
-
body.style.cssText = "flex:1;overflow:auto;padding:12px 14px;white-space:pre-wrap;font-size:13px;line-height:1.45;";
|
|
98
|
-
body.textContent = "Working…";
|
|
99
|
-
modal.appendChild(header);
|
|
100
|
-
modal.appendChild(body);
|
|
101
|
-
overlay.appendChild(modal);
|
|
102
|
-
document.body.appendChild(overlay);
|
|
103
|
-
const close = () => overlay.remove();
|
|
104
|
-
header.querySelector("button")?.addEventListener("click", close);
|
|
105
|
-
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
|
|
106
|
-
document.addEventListener("keydown", function onKey(e: KeyboardEvent) {
|
|
107
|
-
if (e.key === "Escape") { document.removeEventListener("keydown", onKey); close(); }
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
const { aiTransform } = await import("../lib/api-client.js");
|
|
112
|
-
const r = await aiTransform({ action: "translate", text, targetLang: "en" });
|
|
113
|
-
if (r.text) {
|
|
114
|
-
body.textContent = r.text;
|
|
115
|
-
if (status) status.textContent = "";
|
|
116
|
-
} else {
|
|
117
|
-
body.innerHTML = `<div style="color:var(--muted, #888);">No result.</div>` +
|
|
118
|
-
`<div style="margin-top:8px;font-size:12px;color:var(--muted, #888);">${r.reason || ""}</div>` +
|
|
119
|
-
`<div style="margin-top:14px;font-size:12px;">Enable AI translate in Settings → AI features (off by default).</div>`;
|
|
120
|
-
if (status) status.textContent = `Translate: ${r.reason || "no result"}`;
|
|
121
|
-
}
|
|
122
|
-
} catch (err: any) {
|
|
123
|
-
body.textContent = `Error: ${err?.message || String(err)}`;
|
|
124
|
-
if (status) status.textContent = `Translate error: ${err?.message || ""}`;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function installPreviewControls(iframe: HTMLIFrameElement): void {
|
|
129
|
-
const attach = () => {
|
|
130
|
-
const doc = iframe.contentDocument;
|
|
131
|
-
if (!doc) return;
|
|
132
|
-
|
|
133
|
-
applyZoom(doc);
|
|
134
|
-
|
|
135
|
-
doc.addEventListener("keydown", (e) => {
|
|
136
|
-
const target = e.target as HTMLElement | null;
|
|
137
|
-
if (target && (target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName))) return;
|
|
138
|
-
// Zoom is iframe-local — handle here, don't forward.
|
|
139
|
-
if (e.ctrlKey && (e.key === "=" || e.key === "+")) { e.preventDefault(); setZoom(previewZoom + ZOOM_STEP, doc); return; }
|
|
140
|
-
if (e.ctrlKey && e.key === "-") { e.preventDefault(); setZoom(previewZoom - ZOOM_STEP, doc); return; }
|
|
141
|
-
if (e.ctrlKey && e.key === "0") { e.preventDefault(); setZoom(1, doc); return; }
|
|
142
|
-
// Forward EVERY keydown to the parent — no duplicated hotkey list.
|
|
143
|
-
// If the parent's handler calls preventDefault (because it owns the
|
|
144
|
-
// shortcut), dispatchEvent returns false, and we preventDefault on
|
|
145
|
-
// the iframe side too so the browser doesn't ALSO act on it
|
|
146
|
-
// (Ctrl+N otherwise pops a new browser window in some hosts).
|
|
147
|
-
// Single source of truth = app.ts hotkey handlers. Plain typing in
|
|
148
|
-
// the email body — letters, etc. — propagates with no parent
|
|
149
|
-
// handler matching, so dispatchEvent returns true and the iframe
|
|
150
|
-
// event is left alone.
|
|
151
|
-
const synth = new KeyboardEvent("keydown", {
|
|
152
|
-
key: e.key, code: e.code,
|
|
153
|
-
ctrlKey: e.ctrlKey, shiftKey: e.shiftKey, altKey: e.altKey, metaKey: e.metaKey,
|
|
154
|
-
bubbles: true, cancelable: true,
|
|
155
|
-
});
|
|
156
|
-
const allowDefault = document.dispatchEvent(synth);
|
|
157
|
-
if (!allowDefault) e.preventDefault();
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
doc.addEventListener("wheel", (e) => {
|
|
161
|
-
if (!e.ctrlKey) return;
|
|
162
|
-
e.preventDefault();
|
|
163
|
-
setZoom(previewZoom + (e.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP), doc);
|
|
164
|
-
}, { passive: false });
|
|
165
|
-
|
|
166
|
-
// Link interception lives in the iframe's own inline <script> (see
|
|
167
|
-
// wrapHtmlBody). That script runs under a CSP nonce so email-body
|
|
168
|
-
// scripts stay blocked while ours forwards taps to the parent frame.
|
|
169
|
-
|
|
170
|
-
doc.addEventListener("contextmenu", (e) => {
|
|
171
|
-
e.preventDefault();
|
|
172
|
-
const me = e as MouseEvent;
|
|
173
|
-
const rect = iframe.getBoundingClientRect();
|
|
174
|
-
const x = rect.left + me.clientX;
|
|
175
|
-
const y = rect.top + me.clientY;
|
|
176
|
-
const pct = Math.round(previewZoom * 100);
|
|
177
|
-
const sel = doc.defaultView?.getSelection();
|
|
178
|
-
const selectedText = sel?.toString().trim() || "";
|
|
179
|
-
const runSearch = (query: string): void => {
|
|
180
|
-
const input = document.getElementById("search-input") as HTMLInputElement | null;
|
|
181
|
-
if (!input) return;
|
|
182
|
-
input.value = query;
|
|
183
|
-
// Trigger the existing search path — Enter keydown hits the
|
|
184
|
-
// immediate branch in app.ts's handler.
|
|
185
|
-
input.dispatchEvent(new Event("input"));
|
|
186
|
-
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
|
187
|
-
input.focus();
|
|
188
|
-
};
|
|
189
|
-
const items: MenuItem[] = [
|
|
190
|
-
{ label: "Copy", action: () => doc.execCommand("copy") },
|
|
191
|
-
{ label: "Select all", action: () => {
|
|
192
|
-
const s = doc.defaultView?.getSelection();
|
|
193
|
-
if (!s) return;
|
|
194
|
-
const range = doc.createRange();
|
|
195
|
-
range.selectNodeContents(doc.body);
|
|
196
|
-
s.removeAllRanges();
|
|
197
|
-
s.addRange(range);
|
|
198
|
-
} },
|
|
199
|
-
];
|
|
200
|
-
if (selectedText) {
|
|
201
|
-
items.push(
|
|
202
|
-
{ label: "", action: () => {}, separator: true },
|
|
203
|
-
// Truncate long selections in the label so the menu doesn't
|
|
204
|
-
// blow out; full string is what we search for.
|
|
205
|
-
{ label: `Search messages for "${selectedText.length > 40 ? selectedText.slice(0, 40) + "…" : selectedText}"`,
|
|
206
|
-
action: () => runSearch(selectedText) },
|
|
207
|
-
{
|
|
208
|
-
label: "Copy as quoted (> prefix)",
|
|
209
|
-
action: async () => {
|
|
210
|
-
// Prefix each line with "> " (RFC 3676 reply-quote).
|
|
211
|
-
// Useful when pasting a snippet into a compose window
|
|
212
|
-
// without the usual full-message blockquote wrapping.
|
|
213
|
-
const quoted = selectedText.split(/\r?\n/).map(l => "> " + l).join("\n");
|
|
214
|
-
try { await navigator.clipboard.writeText(quoted); } catch { /* */ }
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
const senderAddr = currentMessage?.from?.address || "";
|
|
220
|
-
if (senderAddr) {
|
|
221
|
-
items.push({
|
|
222
|
-
label: `Search messages from ${senderAddr}`,
|
|
223
|
-
action: () => runSearch(`from:${senderAddr}`),
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
items.push(
|
|
227
|
-
{ label: "", action: () => {}, separator: true },
|
|
228
|
-
{ label: selectedText ? "Translate selection" : "Translate message",
|
|
229
|
-
action: () => translateAndShow(selectedText || (doc.body?.innerText || "")) },
|
|
230
|
-
{ label: "", action: () => {}, separator: true },
|
|
231
|
-
{ label: "Zoom in", action: () => setZoom(previewZoom + ZOOM_STEP, doc) },
|
|
232
|
-
{ label: "Zoom out", action: () => setZoom(previewZoom - ZOOM_STEP, doc) },
|
|
233
|
-
{ label: `Reset zoom (${pct}%)`, action: () => setZoom(1, doc) },
|
|
234
|
-
);
|
|
235
|
-
showContextMenu(x, y, items);
|
|
236
|
-
});
|
|
237
|
-
};
|
|
238
|
-
if (iframe.contentDocument?.readyState === "complete") attach();
|
|
239
|
-
else iframe.addEventListener("load", attach, { once: true });
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function clearViewer(): void {
|
|
243
|
-
currentMessage = null;
|
|
244
|
-
currentAccountId = "";
|
|
245
|
-
showMessageGeneration++;
|
|
246
|
-
const headerEl = document.getElementById("mv-header") as HTMLElement;
|
|
247
|
-
const bodyEl = document.getElementById("mv-body") as HTMLElement;
|
|
248
|
-
const attEl = document.getElementById("mv-attachments") as HTMLElement;
|
|
249
|
-
if (bodyEl) bodyEl.innerHTML = `<div class="mv-empty">Select a message to read</div>`;
|
|
250
|
-
if (headerEl) headerEl.hidden = true;
|
|
251
|
-
if (attEl) attEl.hidden = true;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export async function showMessage(accountId: string, uid: number, folderId?: number, specialUse?: string, isRetry = false): Promise<void> {
|
|
255
|
-
const gen = ++showMessageGeneration;
|
|
256
|
-
if (!isRetry) retryCount = 0;
|
|
257
|
-
const headerEl = document.getElementById("mv-header") as HTMLElement;
|
|
258
|
-
const bodyEl = document.getElementById("mv-body") as HTMLElement;
|
|
259
|
-
const attEl = document.getElementById("mv-attachments") as HTMLElement;
|
|
260
|
-
|
|
261
|
-
// Envelope-first render: the row the user just clicked already has the
|
|
262
|
-
// subject / from / to / cc / date / preview in the message-state. Use
|
|
263
|
-
// that to populate the header + a snippet placeholder IMMEDIATELY so
|
|
264
|
-
// tapping a message never shows just "Fetching message body..." with
|
|
265
|
-
// nothing actionable. The full getMessage() call (which might block on
|
|
266
|
-
// a slow IMAP body fetch) only fills in the body and attachments.
|
|
267
|
-
const cached: any = state.getSelected();
|
|
268
|
-
if (cached && cached.uid === uid && (cached.accountId || accountId) === accountId) {
|
|
269
|
-
try { renderHeaderFromEnvelope(headerEl, cached); } catch { /* */ }
|
|
270
|
-
bodyEl.innerHTML = `<div class="mv-empty">Fetching message body…<br><br><span style="color:var(--color-text-muted);font-size:0.9em">${escapeHtml(cached.preview || "")}</span></div>`;
|
|
271
|
-
} else {
|
|
272
|
-
bodyEl.innerHTML = `<div class="mv-empty">Fetching message body...</div>`;
|
|
273
|
-
headerEl.hidden = true;
|
|
274
|
-
}
|
|
275
|
-
attEl.hidden = true;
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
const msg = await getMessage(accountId, uid, false, folderId);
|
|
279
|
-
// Stale response — a newer showMessage was called while we were fetching
|
|
280
|
-
if (gen !== showMessageGeneration) return;
|
|
281
|
-
currentMessage = msg;
|
|
282
|
-
currentAccountId = accountId;
|
|
283
|
-
|
|
284
|
-
// Mark as read — gated by user prefs:
|
|
285
|
-
// - mailx-automark-read (default "true"): if "false", never auto-mark
|
|
286
|
-
// - mailx-automark-delay (default "2"): seconds to wait before
|
|
287
|
-
// marking. Lets the user click through messages quickly without
|
|
288
|
-
// marking ones they didn't actually read. The timer is tied to
|
|
289
|
-
// showMessageGeneration; navigating to another message advances
|
|
290
|
-
// the generation and cancels the pending mark.
|
|
291
|
-
if (!msg.flags.includes("\\Seen")) {
|
|
292
|
-
let enabled = true;
|
|
293
|
-
let delaySec = 2;
|
|
294
|
-
try {
|
|
295
|
-
enabled = localStorage.getItem("mailx-automark-read") !== "false";
|
|
296
|
-
const d = parseFloat(localStorage.getItem("mailx-automark-delay") || "2");
|
|
297
|
-
if (Number.isFinite(d) && d >= 0) delaySec = d;
|
|
298
|
-
} catch { /* private mode — defaults */ }
|
|
299
|
-
if (enabled) {
|
|
300
|
-
const captureGen = gen;
|
|
301
|
-
const newFlags = [...msg.flags, "\\Seen"];
|
|
302
|
-
if (delaySec === 0) {
|
|
303
|
-
updateFlags(accountId, uid, newFlags);
|
|
304
|
-
} else {
|
|
305
|
-
setTimeout(() => {
|
|
306
|
-
// Stale: user moved on before the timer fired.
|
|
307
|
-
if (captureGen !== showMessageGeneration) return;
|
|
308
|
-
updateFlags(accountId, uid, newFlags);
|
|
309
|
-
// Reflect locally so the list row stops looking unread.
|
|
310
|
-
msg.flags = newFlags;
|
|
311
|
-
try { state.updateMessageFlags(accountId, uid, newFlags); } catch { /* */ }
|
|
312
|
-
}, delaySec * 1000);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Header
|
|
318
|
-
headerEl.hidden = false;
|
|
319
|
-
const fromEl = headerEl.querySelector(".mv-from")!;
|
|
320
|
-
const toEl = headerEl.querySelector(".mv-to")!;
|
|
321
|
-
fromEl.textContent = formatAddr(msg.from);
|
|
322
|
-
let toLine = `To: ${msg.to.map(formatAddr).join(", ")}`;
|
|
323
|
-
if (msg.cc?.length) toLine += ` Cc: ${msg.cc.map(formatAddr).join(", ")}`;
|
|
324
|
-
// Always-visible Delivered-To line — shown when present and not already
|
|
325
|
-
// covered by the To/Cc list. Critical for accounts with multiple aliases
|
|
326
|
-
// where you need to see which one received the message at a glance.
|
|
327
|
-
const toAddrs = (msg.to || []).map((a: { address: string }) => a.address.toLowerCase());
|
|
328
|
-
const ccAddrs = (msg.cc || []).map((a: { address: string }) => a.address.toLowerCase());
|
|
329
|
-
const dt = (msg.deliveredTo || "").toLowerCase();
|
|
330
|
-
if (msg.deliveredTo && !toAddrs.includes(dt) && !ccAddrs.includes(dt)) {
|
|
331
|
-
toLine += ` Delivered-To: ${msg.deliveredTo}`;
|
|
332
|
-
}
|
|
333
|
-
toEl.textContent = toLine;
|
|
334
|
-
headerEl.querySelector(".mv-subject")!.textContent = msg.subject;
|
|
335
|
-
document.dispatchEvent(new CustomEvent("mailx-message-shown", { detail: { accountId } }));
|
|
336
|
-
|
|
337
|
-
// Right-click on email addresses in header: copy name, copy address,
|
|
338
|
-
// copy both, add to contacts, plus reply actions for the whole message.
|
|
339
|
-
for (const el of [fromEl, toEl]) {
|
|
340
|
-
el.addEventListener("contextmenu", (e: Event) => {
|
|
341
|
-
e.preventDefault();
|
|
342
|
-
const me = e as MouseEvent;
|
|
343
|
-
const items: MenuItem[] = [];
|
|
344
|
-
const addrs = el === fromEl ? [msg.from] : [...(msg.to || []), ...(msg.cc || [])];
|
|
345
|
-
for (const addr of addrs) {
|
|
346
|
-
if (!addr?.address) continue;
|
|
347
|
-
const name = addr.name || "";
|
|
348
|
-
const both = name ? `${name} <${addr.address}>` : addr.address;
|
|
349
|
-
if (name) {
|
|
350
|
-
items.push({ label: `Copy name: ${name}`, action: () => navigator.clipboard.writeText(name) });
|
|
351
|
-
}
|
|
352
|
-
items.push({ label: `Copy address: ${addr.address}`, action: () => navigator.clipboard.writeText(addr.address) });
|
|
353
|
-
if (name) {
|
|
354
|
-
items.push({ label: `Copy both: ${both}`, action: () => navigator.clipboard.writeText(both) });
|
|
355
|
-
}
|
|
356
|
-
items.push({
|
|
357
|
-
label: `Add to contacts: ${addr.address}`,
|
|
358
|
-
action: async () => {
|
|
359
|
-
await showAddContactDialog(name, addr.address);
|
|
360
|
-
},
|
|
361
|
-
});
|
|
362
|
-
// "Add to preferred" — separate path: writes to
|
|
363
|
-
// contacts.jsonc#preferred[] with an optional source tag.
|
|
364
|
-
// Distinct from "Add to contacts" which goes into the DB +
|
|
365
|
-
// pushes to Google. Preferred entries rank higher in
|
|
366
|
-
// autocomplete and survive Google sync's churn.
|
|
367
|
-
items.push({
|
|
368
|
-
label: `Add to preferred: ${addr.address}`,
|
|
369
|
-
action: async () => {
|
|
370
|
-
const tag = prompt("Tag (e.g. work, family, vendor) — leave blank for default:", "");
|
|
371
|
-
if (tag === null) return; // user cancelled
|
|
372
|
-
try {
|
|
373
|
-
await addPreferredContact({ name, email: addr.address, source: tag.trim() || undefined });
|
|
374
|
-
const status = document.getElementById("status-sync");
|
|
375
|
-
if (status) status.textContent = `Added to preferred: ${addr.address}${tag ? ` [${tag}]` : ""}`;
|
|
376
|
-
} catch (e: any) {
|
|
377
|
-
alert(`Couldn't add to preferred: ${e?.message || e}`);
|
|
378
|
-
}
|
|
379
|
-
},
|
|
380
|
-
});
|
|
381
|
-
items.push({ label: "", action: () => {}, separator: true });
|
|
382
|
-
}
|
|
383
|
-
items.push({ label: "Reply", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "reply" } })) });
|
|
384
|
-
items.push({ label: "Reply All", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "replyAll" } })) });
|
|
385
|
-
items.push({ label: "Forward", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "forward" } })) });
|
|
386
|
-
showContextMenu(me.clientX, me.clientY, items);
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
headerEl.querySelector(".mv-date")!.textContent = new Date(msg.date).toLocaleString(undefined, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false });
|
|
390
|
-
|
|
391
|
-
// Unsubscribe button (upper right of header).
|
|
392
|
-
// - One-Click (RFC 8058): POST via service; show result in status bar.
|
|
393
|
-
// - Plain HTTPS URL: open externally for user confirmation.
|
|
394
|
-
// - mailto: open a pre-filled compose so the reply uses the right account.
|
|
395
|
-
const unsubBtn = document.getElementById("mv-unsubscribe") as HTMLAnchorElement;
|
|
396
|
-
const httpUrl = (msg as any).listUnsubscribeHttp || "";
|
|
397
|
-
const mailUrl = (msg as any).listUnsubscribeMail || "";
|
|
398
|
-
const oneClick = !!(msg as any).listUnsubscribeOneClick;
|
|
399
|
-
const anyUrl = httpUrl || mailUrl || msg.listUnsubscribe || "";
|
|
400
|
-
if (unsubBtn) {
|
|
401
|
-
if (anyUrl) {
|
|
402
|
-
unsubBtn.hidden = false;
|
|
403
|
-
unsubBtn.textContent = "Unsubscribe";
|
|
404
|
-
unsubBtn.removeAttribute("title");
|
|
405
|
-
unsubBtn.href = httpUrl || mailUrl || "#";
|
|
406
|
-
unsubBtn.onclick = async (e) => {
|
|
407
|
-
e.preventDefault();
|
|
408
|
-
const status = document.getElementById("status-sync");
|
|
409
|
-
if (httpUrl && oneClick) {
|
|
410
|
-
if (status) status.textContent = "Unsubscribing…";
|
|
411
|
-
try {
|
|
412
|
-
const result = await unsubscribeOneClick(httpUrl);
|
|
413
|
-
if (status) {
|
|
414
|
-
status.textContent = result.ok
|
|
415
|
-
? `Unsubscribed (${result.status} ${result.statusText})`
|
|
416
|
-
: `Unsubscribe failed: ${result.status} ${result.statusText}`;
|
|
417
|
-
}
|
|
418
|
-
} catch (err: any) {
|
|
419
|
-
if (status) status.textContent = `Unsubscribe error: ${err?.message || err}`;
|
|
420
|
-
}
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
if (httpUrl) {
|
|
424
|
-
const api = (window as any).mailxapi;
|
|
425
|
-
if (api?.openExternal) api.openExternal(httpUrl);
|
|
426
|
-
else window.open(httpUrl, "_blank", "noopener,noreferrer");
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
if (mailUrl) {
|
|
430
|
-
const m = mailUrl.match(/^mailto:([^?]*)(?:\?(.*))?$/i);
|
|
431
|
-
const to = m?.[1] ? decodeURIComponent(m[1]) : "";
|
|
432
|
-
const qs = new URLSearchParams(m?.[2] || "");
|
|
433
|
-
const subject = qs.get("subject") || "Unsubscribe";
|
|
434
|
-
const body = qs.get("body") || "";
|
|
435
|
-
const init = {
|
|
436
|
-
mode: "new",
|
|
437
|
-
accountId: currentAccountId,
|
|
438
|
-
to: to ? [{ name: "", address: to }] : [] as { name: string; address: string }[],
|
|
439
|
-
cc: [] as { name: string; address: string }[],
|
|
440
|
-
subject,
|
|
441
|
-
bodyHtml: body ? `<p>${body}</p>` : "",
|
|
442
|
-
inReplyTo: "",
|
|
443
|
-
references: [] as string[],
|
|
444
|
-
accounts: [] as { id: string; name: string; email: string }[],
|
|
445
|
-
};
|
|
446
|
-
sessionStorage.setItem("composeInit", JSON.stringify(init));
|
|
447
|
-
document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "new" } }));
|
|
448
|
-
}
|
|
449
|
-
};
|
|
450
|
-
} else {
|
|
451
|
-
unsubBtn.hidden = true;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// View Thread button — opens the thread popup from the message list
|
|
456
|
-
// so the user can see all messages in the conversation. Works from
|
|
457
|
-
// the viewer even when thread-grouping is off.
|
|
458
|
-
const threadBtn = document.getElementById("mv-view-thread") as HTMLButtonElement;
|
|
459
|
-
if (threadBtn) {
|
|
460
|
-
const tid = (msg as any).threadId || "";
|
|
461
|
-
if (tid) {
|
|
462
|
-
threadBtn.hidden = false;
|
|
463
|
-
threadBtn.onclick = async () => {
|
|
464
|
-
const { showThreadPopup } = await import("./message-list.js");
|
|
465
|
-
await showThreadPopup(threadBtn, { accountId, threadId: tid });
|
|
466
|
-
};
|
|
467
|
-
} else {
|
|
468
|
-
threadBtn.hidden = true;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// View Source button — shows .eml file path
|
|
473
|
-
const srcBtn = document.getElementById("mv-view-source") as HTMLButtonElement;
|
|
474
|
-
if (srcBtn) {
|
|
475
|
-
if (msg.emlPath) {
|
|
476
|
-
srcBtn.hidden = false;
|
|
477
|
-
srcBtn.title = msg.emlPath;
|
|
478
|
-
srcBtn.onclick = () => {
|
|
479
|
-
// Copy path to clipboard and show in status bar
|
|
480
|
-
navigator.clipboard.writeText(msg.emlPath).then(() => {
|
|
481
|
-
const status = document.getElementById("status-sync");
|
|
482
|
-
if (status) status.textContent = `Path copied: ${msg.emlPath}`;
|
|
483
|
-
}).catch(() => {
|
|
484
|
-
prompt("EML file path:", msg.emlPath);
|
|
485
|
-
});
|
|
486
|
-
};
|
|
487
|
-
} else {
|
|
488
|
-
srcBtn.hidden = true;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Edit Draft / Send from Outbox button
|
|
493
|
-
const editBtn = document.getElementById("mv-edit-draft") as HTMLButtonElement;
|
|
494
|
-
if (editBtn) {
|
|
495
|
-
const isDraft = specialUse === "drafts" || specialUse === "outbox";
|
|
496
|
-
if (isDraft) {
|
|
497
|
-
editBtn.hidden = false;
|
|
498
|
-
editBtn.textContent = specialUse === "outbox" ? "Edit & Send" : "Edit Draft";
|
|
499
|
-
editBtn.onclick = () => {
|
|
500
|
-
// Open compose window pre-filled with this draft
|
|
501
|
-
const init = {
|
|
502
|
-
mode: "draft",
|
|
503
|
-
accountId,
|
|
504
|
-
to: msg.to || [],
|
|
505
|
-
cc: msg.cc || [],
|
|
506
|
-
subject: msg.subject || "",
|
|
507
|
-
bodyHtml: msg.bodyHtml || "",
|
|
508
|
-
inReplyTo: msg.inReplyTo || "",
|
|
509
|
-
references: msg.references || [],
|
|
510
|
-
accounts: [] as { id: string; name: string; email: string }[],
|
|
511
|
-
draftUid: msg.uid,
|
|
512
|
-
draftFolderId: msg.folderId,
|
|
513
|
-
};
|
|
514
|
-
sessionStorage.setItem("composeInit", JSON.stringify(init));
|
|
515
|
-
// Trigger compose overlay via custom event (app.ts handles it)
|
|
516
|
-
document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "draft" } }));
|
|
517
|
-
};
|
|
518
|
-
} else {
|
|
519
|
-
editBtn.hidden = true;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// Details toggle — show extra headers (Delivered-To, Return-Path, Message-ID, etc.)
|
|
524
|
-
const detailsEl = document.getElementById("mv-details") as HTMLElement;
|
|
525
|
-
const detailsBtn = document.getElementById("mv-toggle-details") as HTMLButtonElement;
|
|
526
|
-
if (detailsEl && detailsBtn) {
|
|
527
|
-
// Q56: every row gets a Copy button so paths / IDs can be pasted
|
|
528
|
-
// into accounts.jsonc hints or bug reports.
|
|
529
|
-
const row = (label: string, value: string) =>
|
|
530
|
-
`<div class="mv-details-row"><span class="mv-details-label">${label}:</span> <span class="mv-details-value">${escapeText(value)}</span> <button type="button" class="mv-details-copy" data-copy="${escapeText(value).replace(/"/g, """)}" title="Copy">⧉</button></div>`;
|
|
531
|
-
const lines: string[] = [];
|
|
532
|
-
if (msg.deliveredTo) lines.push(row("Delivered-To", msg.deliveredTo));
|
|
533
|
-
if (msg.returnPath) lines.push(row("Return-Path", msg.returnPath));
|
|
534
|
-
if (msg.messageId) lines.push(row("Message-ID", msg.messageId));
|
|
535
|
-
if (msg.listUnsubscribe) lines.push(row("Unsubscribe", msg.listUnsubscribe));
|
|
536
|
-
if (msg.emlPath) lines.push(row("EML file", msg.emlPath));
|
|
537
|
-
lines.push(row("Account", accountId));
|
|
538
|
-
lines.push(row("UID", `${msg.uid} (folder ${msg.folderId})`));
|
|
539
|
-
detailsEl.innerHTML = lines.join("");
|
|
540
|
-
detailsEl.hidden = true;
|
|
541
|
-
detailsBtn.textContent = "Details";
|
|
542
|
-
detailsBtn.onclick = () => {
|
|
543
|
-
detailsEl.hidden = !detailsEl.hidden;
|
|
544
|
-
detailsBtn.textContent = detailsEl.hidden ? "Details" : "\u2713 Details";
|
|
545
|
-
};
|
|
546
|
-
// Wire copy buttons.
|
|
547
|
-
detailsEl.querySelectorAll<HTMLButtonElement>(".mv-details-copy").forEach(btn => {
|
|
548
|
-
btn.addEventListener("click", async (e) => {
|
|
549
|
-
e.stopPropagation();
|
|
550
|
-
const val = btn.dataset.copy || "";
|
|
551
|
-
try {
|
|
552
|
-
await navigator.clipboard.writeText(val);
|
|
553
|
-
btn.textContent = "✓";
|
|
554
|
-
setTimeout(() => { btn.textContent = "⧉"; }, 1500);
|
|
555
|
-
} catch {
|
|
556
|
-
prompt("Copy:", val);
|
|
557
|
-
}
|
|
558
|
-
});
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Remote content banner (collapsible dropdown with sender/recipient details)
|
|
563
|
-
bodyEl.innerHTML = "";
|
|
564
|
-
if (msg.hasRemoteContent) {
|
|
565
|
-
const senderAddr = msg.from?.address || "";
|
|
566
|
-
const senderName = msg.from?.name || "";
|
|
567
|
-
const senderDomain = senderAddr.split("@")[1] || "";
|
|
568
|
-
const deliveredTo = msg.deliveredTo || "";
|
|
569
|
-
const toAddr = msg.to?.[0]?.address || "";
|
|
570
|
-
const returnPath = msg.returnPath || "";
|
|
571
|
-
const isFlagged = !!(msg as any).isFlagged;
|
|
572
|
-
const reputation = (msg as any).reputation as {
|
|
573
|
-
flagged: boolean; listedCount: number; checkedCount: number;
|
|
574
|
-
sources: Array<{ service: string; verdict: string }>;
|
|
575
|
-
verdict: string; service: string;
|
|
576
|
-
} | null;
|
|
577
|
-
const reputationFlagged = !!(reputation && reputation.flagged);
|
|
578
|
-
const reputationText = reputationFlagged
|
|
579
|
-
? `⚠ ${reputation!.listedCount} of ${reputation!.checkedCount} reputation services flag <strong>${escapeText(senderDomain)}</strong> as <strong>${escapeText(reputation!.verdict)}</strong> (${escapeText(reputation!.sources.map(s => s.service).join(", "))})`
|
|
580
|
-
: "";
|
|
581
|
-
|
|
582
|
-
const banner = document.createElement("div");
|
|
583
|
-
banner.className = "mv-remote-banner"
|
|
584
|
-
+ (isFlagged ? " mv-remote-banner-flagged" : "")
|
|
585
|
-
+ (reputationFlagged ? " mv-remote-banner-reputation" : "");
|
|
586
|
-
banner.innerHTML =
|
|
587
|
-
(isFlagged
|
|
588
|
-
? `<div class="mv-rb-flagged">⚠ FLAGGED: this sender or domain is on your flagged list</div>`
|
|
589
|
-
: "") +
|
|
590
|
-
(reputationFlagged
|
|
591
|
-
? `<div class="mv-rb-reputation">${reputationText}</div>`
|
|
592
|
-
: "") +
|
|
593
|
-
`<div class="mv-rb-summary">` +
|
|
594
|
-
`<span class="mv-rb-toggle">▸</span>` +
|
|
595
|
-
`<span>Remote content blocked</span>` +
|
|
596
|
-
`<span class="mv-rb-buttons">` +
|
|
597
|
-
`<button id="btn-load-remote">Load once</button>` +
|
|
598
|
-
`<button id="btn-allow-sender" title="${escapeText(senderAddr)}">Always: ${escapeText(senderAddr)}</button>` +
|
|
599
|
-
(senderDomain ? `<button id="btn-allow-domain" title="*@${escapeText(senderDomain)}">Always: *@${escapeText(senderDomain)}</button>` : "") +
|
|
600
|
-
`</span>` +
|
|
601
|
-
`</div>` +
|
|
602
|
-
`<div class="mv-rb-details" hidden>` +
|
|
603
|
-
`<div class="mv-rb-info">` +
|
|
604
|
-
`<div><span class="mv-rb-label">From:</span> ${escapeText(senderName ? `${senderName} <${senderAddr}>` : senderAddr)}</div>` +
|
|
605
|
-
(deliveredTo ? `<div><span class="mv-rb-label">Delivered-To:</span> ${escapeText(deliveredTo)}</div>` : "") +
|
|
606
|
-
(toAddr && toAddr !== deliveredTo ? `<div><span class="mv-rb-label">To:</span> ${escapeText(toAddr)}</div>` : "") +
|
|
607
|
-
(returnPath && returnPath !== senderAddr ? `<div><span class="mv-rb-label">Return-Path:</span> ${escapeText(returnPath)}</div>` : "") +
|
|
608
|
-
`</div>` +
|
|
609
|
-
(deliveredTo || toAddr ? `<div class="mv-rb-actions"><button id="btn-allow-to">Always allow to: ${escapeText(deliveredTo || toAddr)}</button></div>` : "") +
|
|
610
|
-
`<div class="mv-rb-actions">` +
|
|
611
|
-
`<button id="btn-flag-sender" class="mv-rb-flag-btn" title="${escapeText(senderAddr)}">${isFlagged ? "Unflag" : "Flag"} sender</button>` +
|
|
612
|
-
(senderDomain ? `<button id="btn-flag-domain" class="mv-rb-flag-btn" title="*@${escapeText(senderDomain)}">Flag domain *@${escapeText(senderDomain)}</button>` : "") +
|
|
613
|
-
`</div>` +
|
|
614
|
-
`<div class="mv-rb-actions"><button id="btn-edit-allowlist" title="View / edit the full allowlist">Edit allowlist…</button></div>` +
|
|
615
|
-
`</div>`;
|
|
616
|
-
bodyEl.appendChild(banner);
|
|
617
|
-
|
|
618
|
-
// Toggle dropdown — click arrow or text to expand details
|
|
619
|
-
const summary = banner.querySelector(".mv-rb-summary")!;
|
|
620
|
-
const details = banner.querySelector(".mv-rb-details") as HTMLElement;
|
|
621
|
-
const toggle = banner.querySelector(".mv-rb-toggle")!;
|
|
622
|
-
summary.addEventListener("click", (e) => {
|
|
623
|
-
if ((e.target as HTMLElement).tagName === "BUTTON") return;
|
|
624
|
-
details.hidden = !details.hidden;
|
|
625
|
-
toggle.textContent = details.hidden ? "\u25B8" : "\u25BE";
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
const loadRemote = async () => {
|
|
629
|
-
banner.remove();
|
|
630
|
-
const full = await getMessage(accountId, uid, true);
|
|
631
|
-
if (full.bodyHtml) {
|
|
632
|
-
const oldIframe = bodyEl.querySelector("iframe");
|
|
633
|
-
if (oldIframe) oldIframe.remove();
|
|
634
|
-
const iframe = document.createElement("iframe");
|
|
635
|
-
iframe.srcdoc = wrapHtmlBody(full.bodyHtml, true);
|
|
636
|
-
bodyEl.appendChild(iframe);
|
|
637
|
-
installPreviewControls(iframe);
|
|
638
|
-
}
|
|
639
|
-
};
|
|
640
|
-
|
|
641
|
-
banner.querySelector("#btn-load-remote")!.addEventListener("click", loadRemote);
|
|
642
|
-
|
|
643
|
-
banner.querySelector("#btn-allow-sender")?.addEventListener("click", async () => {
|
|
644
|
-
await allowRemoteContent("sender", senderAddr);
|
|
645
|
-
loadRemote();
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
banner.querySelector("#btn-allow-domain")?.addEventListener("click", async () => {
|
|
649
|
-
await allowRemoteContent("domain", senderDomain);
|
|
650
|
-
loadRemote();
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
banner.querySelector("#btn-allow-to")?.addEventListener("click", async () => {
|
|
654
|
-
const addr = deliveredTo || toAddr;
|
|
655
|
-
if (!addr) return;
|
|
656
|
-
await allowRemoteContent("recipient", addr);
|
|
657
|
-
loadRemote();
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
// Flag (or unflag) sender / domain — toggles the allowlist's
|
|
661
|
-
// flaggedSenders / flaggedDomains lists. Subsequent messages
|
|
662
|
-
// from this sender or domain show a red FLAGGED warning at the
|
|
663
|
-
// top of this banner. Doesn't load remote content; this is a
|
|
664
|
-
// signal-to-the-user feature, orthogonal to allow/block.
|
|
665
|
-
const onFlagToggle = async (type: "sender" | "domain", value: string) => {
|
|
666
|
-
if (!value) return;
|
|
667
|
-
try {
|
|
668
|
-
const result = await flagSenderOrDomain(type, value);
|
|
669
|
-
const status = document.getElementById("status-sync");
|
|
670
|
-
if (status) status.textContent = result.flagged
|
|
671
|
-
? `Flagged ${type}: ${value}`
|
|
672
|
-
: `Unflagged ${type}: ${value}`;
|
|
673
|
-
// Re-render this message so the banner picks up the new
|
|
674
|
-
// flagged state without the user having to reselect.
|
|
675
|
-
if (currentMessage) {
|
|
676
|
-
showMessage(currentAccountId, currentMessage.uid, currentMessage.folderId, specialUse).catch(() => { /* */ });
|
|
677
|
-
}
|
|
678
|
-
} catch (e: any) {
|
|
679
|
-
const status = document.getElementById("status-sync");
|
|
680
|
-
if (status) status.textContent = `Flag failed: ${e?.message || e}`;
|
|
681
|
-
}
|
|
682
|
-
};
|
|
683
|
-
banner.querySelector("#btn-flag-sender")?.addEventListener("click", () => onFlagToggle("sender", senderAddr));
|
|
684
|
-
banner.querySelector("#btn-flag-domain")?.addEventListener("click", () => onFlagToggle("domain", senderDomain));
|
|
685
|
-
|
|
686
|
-
// "Edit allowlist…" — fires a document-level event that app.ts
|
|
687
|
-
// listens for and opens the JSONC editor pre-selected to
|
|
688
|
-
// allowlist.jsonc. Keeps message-viewer free of the editor import.
|
|
689
|
-
banner.querySelector("#btn-edit-allowlist")?.addEventListener("click", () => {
|
|
690
|
-
document.dispatchEvent(new CustomEvent("mailx-open-jsonc-editor", { detail: { file: "allowlist.jsonc" } }));
|
|
691
|
-
});
|
|
692
|
-
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
// Body fetch error — show banner above (empty) body instead of polluting
|
|
696
|
-
// the main content area with the error text. Transient errors get a retry.
|
|
697
|
-
if ((msg as any).bodyError) {
|
|
698
|
-
const err = String((msg as any).bodyError);
|
|
699
|
-
const isTransient = !!(msg as any).bodyErrorTransient;
|
|
700
|
-
const errBanner = document.createElement("div");
|
|
701
|
-
errBanner.className = "mv-system-message mv-system-error";
|
|
702
|
-
errBanner.innerHTML = `
|
|
703
|
-
<div class="mv-system-tag">mailx</div>
|
|
704
|
-
<div class="mv-system-title">Body unavailable</div>
|
|
705
|
-
<div class="mv-system-body">${err.replace(/[&<>"]/g, c => ({"&":"&","<":"<",">":">",'"':"""}[c] || c))}</div>
|
|
706
|
-
${isTransient ? `<div class="mv-system-actions"><button id="btn-retry-body" class="mv-system-btn">Retry</button></div>` : ""}
|
|
707
|
-
`;
|
|
708
|
-
bodyEl.appendChild(errBanner);
|
|
709
|
-
if (isTransient) {
|
|
710
|
-
errBanner.querySelector("#btn-retry-body")?.addEventListener("click", async () => {
|
|
711
|
-
errBanner.remove();
|
|
712
|
-
bodyEl.innerHTML = `<div class="mv-empty">Fetching message body...</div>`;
|
|
713
|
-
try {
|
|
714
|
-
const retry: any = await getMessage(accountId, uid, false);
|
|
715
|
-
if (retry.bodyError) {
|
|
716
|
-
// Still failing — rebuild the error banner via recursive render.
|
|
717
|
-
showMessage(accountId, uid, folderId, specialUse).catch(() => { });
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
720
|
-
bodyEl.innerHTML = "";
|
|
721
|
-
if (retry.bodyHtml) {
|
|
722
|
-
const iframe = document.createElement("iframe");
|
|
723
|
-
iframe.sandbox.add("allow-same-origin");
|
|
724
|
-
iframe.sandbox.add("allow-popups");
|
|
725
|
-
iframe.sandbox.add("allow-popups-to-escape-sandbox");
|
|
726
|
-
iframe.sandbox.add("allow-top-navigation-by-user-activation");
|
|
727
|
-
iframe.sandbox.add("allow-scripts");
|
|
728
|
-
iframe.srcdoc = wrapHtmlBody(retry.bodyHtml, retry.remoteAllowed);
|
|
729
|
-
bodyEl.appendChild(iframe);
|
|
730
|
-
installPreviewControls(iframe);
|
|
731
|
-
} else if (retry.bodyText) {
|
|
732
|
-
const pre = document.createElement("pre");
|
|
733
|
-
pre.style.cssText = "padding: 1rem; white-space: pre-wrap; word-break: break-word;";
|
|
734
|
-
pre.innerHTML = linkifyText(retry.bodyText);
|
|
735
|
-
bodyEl.appendChild(pre);
|
|
736
|
-
} else {
|
|
737
|
-
bodyEl.innerHTML = `<div class="mv-empty">No content</div>`;
|
|
738
|
-
}
|
|
739
|
-
} catch (e: any) {
|
|
740
|
-
bodyEl.innerHTML = `<div class="mv-empty">Retry failed: ${e.message}</div>`;
|
|
741
|
-
}
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// Body in sandboxed iframe
|
|
748
|
-
if (msg.bodyHtml) {
|
|
749
|
-
const iframe = document.createElement("iframe");
|
|
750
|
-
iframe.sandbox.add("allow-same-origin");
|
|
751
|
-
iframe.sandbox.add("allow-popups");
|
|
752
|
-
iframe.sandbox.add("allow-popups-to-escape-sandbox");
|
|
753
|
-
iframe.sandbox.add("allow-top-navigation-by-user-activation");
|
|
754
|
-
// allow-scripts lets OUR injected <script> run (for Android link
|
|
755
|
-
// interception — parent-side contentDocument listeners don't fire
|
|
756
|
-
// reliably on Android WebView). CSP with a nonce restricts script
|
|
757
|
-
// execution to our tag only; inline scripts in the email body are
|
|
758
|
-
// still blocked.
|
|
759
|
-
iframe.sandbox.add("allow-scripts");
|
|
760
|
-
iframe.srcdoc = wrapHtmlBody(msg.bodyHtml, msg.remoteAllowed);
|
|
761
|
-
bodyEl.appendChild(iframe);
|
|
762
|
-
installPreviewControls(iframe);
|
|
763
|
-
} else if (msg.bodyText) {
|
|
764
|
-
const pre = document.createElement("pre");
|
|
765
|
-
pre.style.cssText = "padding: 1rem; white-space: pre-wrap; word-break: break-word;";
|
|
766
|
-
// Auto-linkify URLs in plain text
|
|
767
|
-
pre.innerHTML = linkifyText(msg.bodyText);
|
|
768
|
-
bodyEl.appendChild(pre);
|
|
769
|
-
} else {
|
|
770
|
-
bodyEl.innerHTML = `<div class="mv-empty">No content</div>`;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// Attachments — always clear first to avoid stale chips from previous message
|
|
774
|
-
attEl.innerHTML = "";
|
|
775
|
-
attEl.hidden = true;
|
|
776
|
-
if (msg.attachments?.length) {
|
|
777
|
-
attEl.hidden = false;
|
|
778
|
-
for (let i = 0; i < msg.attachments.length; i++) {
|
|
779
|
-
const att = msg.attachments[i];
|
|
780
|
-
const chip = document.createElement("a");
|
|
781
|
-
chip.className = "mv-att-chip";
|
|
782
|
-
chip.textContent = `\uD83D\uDCCE ${att.filename} (${formatSize(att.size)})`;
|
|
783
|
-
chip.href = "#";
|
|
784
|
-
chip.title = `${att.filename} (${att.mimeType})`;
|
|
785
|
-
chip.addEventListener("click", async (e) => {
|
|
786
|
-
e.preventDefault();
|
|
787
|
-
try {
|
|
788
|
-
const data = await getAttachment(accountId, uid, i, msg.folderId);
|
|
789
|
-
const bridge = (window as any)._nativeBridge;
|
|
790
|
-
if (bridge?.openAttachment) {
|
|
791
|
-
// Android: blob URLs don't work in WebView. Pass base64
|
|
792
|
-
// to native bridge which saves to Downloads and opens
|
|
793
|
-
// with the system viewer.
|
|
794
|
-
await bridge.openAttachment(att.filename, data.contentType, data.content);
|
|
795
|
-
} else {
|
|
796
|
-
const bytes = Uint8Array.from(atob(data.content), c => c.charCodeAt(0));
|
|
797
|
-
const blob = new Blob([bytes], { type: data.contentType });
|
|
798
|
-
const url = URL.createObjectURL(blob);
|
|
799
|
-
window.open(url, "_blank");
|
|
800
|
-
}
|
|
801
|
-
} catch (err: any) {
|
|
802
|
-
console.error(`Attachment download failed: ${err.message}`);
|
|
803
|
-
}
|
|
804
|
-
});
|
|
805
|
-
// Drag the chip to an external target (Explorer / Finder / Files app)
|
|
806
|
-
// to drop the file there. Uses the Chromium `DownloadURL` dataTransfer
|
|
807
|
-
// format: "mime:filename:blob-url". We fetch the attachment first so
|
|
808
|
-
// the blob URL is valid by the time the drop lands.
|
|
809
|
-
chip.draggable = true;
|
|
810
|
-
chip.addEventListener("dragstart", async (e) => {
|
|
811
|
-
if (!e.dataTransfer) return;
|
|
812
|
-
try {
|
|
813
|
-
const data = await getAttachment(accountId, uid, i, msg.folderId);
|
|
814
|
-
const bytes = Uint8Array.from(atob(data.content), c => c.charCodeAt(0));
|
|
815
|
-
const blob = new Blob([bytes], { type: data.contentType || "application/octet-stream" });
|
|
816
|
-
const url = URL.createObjectURL(blob);
|
|
817
|
-
// Sanitize filename: no path separators, no newlines.
|
|
818
|
-
const safeName = (att.filename || "attachment").replace(/[\r\n"\/\\]/g, "_");
|
|
819
|
-
const downloadUrl = `${data.contentType || "application/octet-stream"}:${safeName}:${url}`;
|
|
820
|
-
e.dataTransfer.setData("DownloadURL", downloadUrl);
|
|
821
|
-
e.dataTransfer.effectAllowed = "copy";
|
|
822
|
-
} catch (err: any) {
|
|
823
|
-
console.error(`Attachment drag-out failed: ${err.message || err}`);
|
|
824
|
-
}
|
|
825
|
-
});
|
|
826
|
-
attEl.appendChild(chip);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
} catch (e: any) {
|
|
830
|
-
const err = e.message || "Unknown error";
|
|
831
|
-
console.error("showMessage error:", e);
|
|
832
|
-
// "Message was deleted from the server" — the service already dropped
|
|
833
|
-
// the local row. Remove it from the list so the UI advances to the next
|
|
834
|
-
// message instead of sitting on a stale error banner.
|
|
835
|
-
const isNotFound = /deleted from the server|isNotFound|not found|Not Found|404/.test(err);
|
|
836
|
-
if (isNotFound) {
|
|
837
|
-
// Drop the stale row so the list auto-advances to the next message
|
|
838
|
-
// (or clears the viewer). Leaves the user a way back on mobile where
|
|
839
|
-
// the viewer takes the whole screen.
|
|
840
|
-
state.removeMessages([{ accountId, uid }]);
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
if (retryCount < 3) {
|
|
844
|
-
retryCount++;
|
|
845
|
-
bodyEl.innerHTML = `<div class="mv-empty">Loading failed: ${err} — retrying (${retryCount}/3)...</div>`;
|
|
846
|
-
setTimeout(() => { if (gen === showMessageGeneration) showMessage(accountId, uid, folderId, specialUse, true); }, 3000);
|
|
847
|
-
} else {
|
|
848
|
-
bodyEl.innerHTML = `<div class="mv-empty">Failed to load: ${err}</div>`;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
function formatAddr(addr: { name: string; address: string }): string {
|
|
854
|
-
if (addr.name) return `${addr.name} <${addr.address}>`;
|
|
855
|
-
return addr.address;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
/** Render the viewer header from a list-row envelope (instant — no body
|
|
859
|
-
* fetch awaited). Used to populate the header pane the moment a message
|
|
860
|
-
* is clicked so the user always sees something actionable; getMessage()
|
|
861
|
-
* later overwrites the same fields with the authoritative values from the
|
|
862
|
-
* body parse (which can add Cc, Delivered-To, etc. that the list row
|
|
863
|
-
* doesn't track). */
|
|
864
|
-
function renderHeaderFromEnvelope(headerEl: HTMLElement, env: any): void {
|
|
865
|
-
headerEl.hidden = false;
|
|
866
|
-
const fromEl = headerEl.querySelector(".mv-from");
|
|
867
|
-
const toEl = headerEl.querySelector(".mv-to");
|
|
868
|
-
const subjEl = headerEl.querySelector(".mv-subject");
|
|
869
|
-
const dateEl = headerEl.querySelector(".mv-date");
|
|
870
|
-
if (fromEl) fromEl.textContent = formatAddr(env.from);
|
|
871
|
-
if (toEl) {
|
|
872
|
-
let toLine = `To: ${(env.to || []).map(formatAddr).join(", ")}`;
|
|
873
|
-
if (env.cc?.length) toLine += ` Cc: ${env.cc.map(formatAddr).join(", ")}`;
|
|
874
|
-
toEl.textContent = toLine;
|
|
875
|
-
}
|
|
876
|
-
if (subjEl) subjEl.textContent = env.subject || "";
|
|
877
|
-
if (dateEl) {
|
|
878
|
-
try { dateEl.textContent = new Date(env.date).toLocaleString(); }
|
|
879
|
-
catch { dateEl.textContent = ""; }
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
function escapeHtml(s: string): string {
|
|
884
|
-
return (s || "").replace(/[&<>"']/g, c =>
|
|
885
|
-
({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }[c]!));
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
/** Convert plain text URLs into clickable links, escaping HTML */
|
|
889
|
-
function linkifyText(text: string): string {
|
|
890
|
-
// Escape HTML first
|
|
891
|
-
const escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
892
|
-
// Then linkify URLs
|
|
893
|
-
return escaped.replace(
|
|
894
|
-
/(https?:\/\/[^\s<>"')\]]+)/g,
|
|
895
|
-
'<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>'
|
|
896
|
-
);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
function escapeText(s: string): string {
|
|
900
|
-
const div = document.createElement("div");
|
|
901
|
-
div.textContent = s;
|
|
902
|
-
return div.innerHTML;
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
/** Minimal add-contact modal: name + email + organization with a duplicate
|
|
906
|
-
* check (checks the contacts DB for an existing row with the same email
|
|
907
|
-
* and surfaces it so the user can update instead of creating a second
|
|
908
|
-
* row with a different name). Future: AI-extracted fields from the letter
|
|
909
|
-
* body populate the form before it opens. */
|
|
910
|
-
async function showAddContactDialog(nameIn: string, emailIn: string): Promise<void> {
|
|
911
|
-
let dup: { name: string; email: string; source: string } | null = null;
|
|
912
|
-
try {
|
|
913
|
-
const existing = await listContacts(emailIn, 1, 10);
|
|
914
|
-
const match = (existing?.items || []).find((c: any) => (c.email || "").toLowerCase() === emailIn.toLowerCase());
|
|
915
|
-
if (match) dup = match;
|
|
916
|
-
} catch { /* non-fatal — dialog still works without dup info */ }
|
|
917
|
-
|
|
918
|
-
const backdrop = document.createElement("div");
|
|
919
|
-
backdrop.className = "mailx-modal-backdrop";
|
|
920
|
-
const panel = document.createElement("div");
|
|
921
|
-
panel.className = "mailx-modal";
|
|
922
|
-
panel.innerHTML = `
|
|
923
|
-
<div class="mailx-modal-title">
|
|
924
|
-
<span class="mailx-modal-title-text">${dup ? "Update contact" : "Add contact"}</span>
|
|
925
|
-
<button type="button" class="mailx-modal-close" id="ac-close" aria-label="Close">×</button>
|
|
926
|
-
</div>
|
|
927
|
-
${dup ? `<div class="mailx-modal-info">Already in address book as <strong>${escapeText(dup.name || "(no name)")}</strong> (${escapeText(dup.source)}). Saving will update the name.</div>` : ""}
|
|
928
|
-
<label class="mailx-modal-label">Name
|
|
929
|
-
<input class="mailx-modal-input" id="ac-name" type="text" value="${escapeText(dup?.name || nameIn || "")}" autofocus>
|
|
930
|
-
</label>
|
|
931
|
-
<label class="mailx-modal-label">Email
|
|
932
|
-
<input class="mailx-modal-input" id="ac-email" type="email" value="${escapeText(emailIn)}" readonly>
|
|
933
|
-
</label>
|
|
934
|
-
<label class="mailx-modal-label">Organization <span style="color:var(--color-text-muted);font-size:0.85em">(optional)</span>
|
|
935
|
-
<input class="mailx-modal-input" id="ac-org" type="text" placeholder="">
|
|
936
|
-
</label>
|
|
937
|
-
<div class="mailx-modal-buttons">
|
|
938
|
-
<span class="mailx-modal-spacer"></span>
|
|
939
|
-
<button type="button" class="mailx-modal-btn" data-action="cancel">Cancel</button>
|
|
940
|
-
<button type="button" class="mailx-modal-btn mailx-modal-btn-primary" data-action="save">${dup ? "Update" : "Save"}</button>
|
|
941
|
-
</div>`;
|
|
942
|
-
backdrop.appendChild(panel);
|
|
943
|
-
document.body.appendChild(backdrop);
|
|
944
|
-
|
|
945
|
-
const close = () => backdrop.remove();
|
|
946
|
-
panel.querySelector<HTMLButtonElement>("#ac-close")!.addEventListener("click", close);
|
|
947
|
-
panel.querySelectorAll<HTMLButtonElement>(".mailx-modal-btn").forEach(btn => {
|
|
948
|
-
btn.addEventListener("click", async () => {
|
|
949
|
-
if (btn.dataset.action === "cancel") { close(); return; }
|
|
950
|
-
const nameEl = panel.querySelector<HTMLInputElement>("#ac-name")!;
|
|
951
|
-
const emailEl = panel.querySelector<HTMLInputElement>("#ac-email")!;
|
|
952
|
-
btn.disabled = true;
|
|
953
|
-
btn.textContent = "Saving…";
|
|
954
|
-
try {
|
|
955
|
-
// upsertContact is the two-way cache path (enqueues a Google
|
|
956
|
-
// People push); for pure local-first addContact would also
|
|
957
|
-
// work but skips the Google sync. Use upsertContact so the
|
|
958
|
-
// row propagates to Google Contacts next drain tick.
|
|
959
|
-
await upsertContact(nameEl.value.trim(), emailEl.value.trim());
|
|
960
|
-
close();
|
|
961
|
-
} catch (e: any) {
|
|
962
|
-
btn.disabled = false;
|
|
963
|
-
btn.textContent = dup ? "Update" : "Save";
|
|
964
|
-
alert(`Couldn't save: ${e?.message || e}`);
|
|
965
|
-
}
|
|
966
|
-
});
|
|
967
|
-
});
|
|
968
|
-
const onKey = (e: KeyboardEvent) => {
|
|
969
|
-
if (e.key === "Escape") { close(); document.removeEventListener("keydown", onKey, true); }
|
|
970
|
-
};
|
|
971
|
-
document.addEventListener("keydown", onKey, true);
|
|
972
|
-
// addContact is kept as a legacy silent path (no-form) for any caller
|
|
973
|
-
// that still invokes it — currently none after this refactor.
|
|
974
|
-
void addContact;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
function formatSize(bytes: number): string {
|
|
978
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
979
|
-
if (bytes < 1048576) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
980
|
-
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
function wrapHtmlBody(html: string, allowRemote = false): string {
|
|
984
|
-
// CSP blocks remote resources (tracking pixels, external CSS). Inline
|
|
985
|
-
// scripts are allowed via 'unsafe-inline' so our injected link-tap handler
|
|
986
|
-
// runs; email-body <script> tags and on* handlers are stripped server-side
|
|
987
|
-
// by sanitizeHtml() in mailx-core, so this doesn't actually widen the
|
|
988
|
-
// attack surface. (A per-render nonce would be tidier, but meta-CSP with
|
|
989
|
-
// nonces isn't reliably honored across older WebViews — and when a nonce
|
|
990
|
-
// is present, 'unsafe-inline' is ignored, so our script fell back to
|
|
991
|
-
// blocked on those WebViews.)
|
|
992
|
-
const csp = allowRemote
|
|
993
|
-
? ""
|
|
994
|
-
: `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: cid:; form-action 'none';">`;
|
|
995
|
-
return `<!DOCTYPE html>
|
|
996
|
-
<html><head>
|
|
997
|
-
<meta charset="UTF-8">
|
|
998
|
-
${csp}
|
|
999
|
-
<style>
|
|
1000
|
-
html, body { touch-action: pan-y pinch-zoom; }
|
|
1001
|
-
html { height: 100%; overflow-y: auto; overflow-x: hidden; }
|
|
1002
|
-
body {
|
|
1003
|
-
font-family: system-ui, sans-serif;
|
|
1004
|
-
font-size: 17.5px;
|
|
1005
|
-
line-height: 1.5;
|
|
1006
|
-
color: #1a1a2e;
|
|
1007
|
-
background: #fff;
|
|
1008
|
-
padding: 1rem;
|
|
1009
|
-
margin: 0;
|
|
1010
|
-
min-height: 100%;
|
|
1011
|
-
word-break: break-word;
|
|
1012
|
-
color-scheme: dark light;
|
|
1013
|
-
}
|
|
1014
|
-
img { max-width: 100%; height: auto; }
|
|
1015
|
-
a { color: #1a6dd4; }
|
|
1016
|
-
pre, code { white-space: pre-wrap; }
|
|
1017
|
-
blockquote { border-left: 3px solid #ccc; padding-left: 1rem; margin-left: 0; color: #666; }
|
|
1018
|
-
@media (prefers-color-scheme: dark) {
|
|
1019
|
-
body { color: #cdd6f4; background: #282840; }
|
|
1020
|
-
a { color: #89b4fa; }
|
|
1021
|
-
blockquote { border-color: #45475a; color: #6c7086; }
|
|
1022
|
-
}
|
|
1023
|
-
</style>
|
|
1024
|
-
<base target="_blank">
|
|
1025
|
-
<script>
|
|
1026
|
-
// Link interception — Android WebView doesn't fire the default <a target="_blank">
|
|
1027
|
-
// new-window handler, so we postMessage to the parent which routes through the
|
|
1028
|
-
// native bridge (mailxapi://openurl) to Launcher.OpenAsync.
|
|
1029
|
-
(function () {
|
|
1030
|
-
function handleLinkTap(e) {
|
|
1031
|
-
var a = e.target && e.target.closest ? e.target.closest("a[href]") : null;
|
|
1032
|
-
if (!a) return;
|
|
1033
|
-
var url = a.href;
|
|
1034
|
-
if (!url || url.indexOf("javascript:") === 0 || url.charAt(0) === "#") return;
|
|
1035
|
-
e.preventDefault();
|
|
1036
|
-
e.stopPropagation();
|
|
1037
|
-
window.parent.postMessage({ type: "linkClick", url: url }, "*");
|
|
1038
|
-
}
|
|
1039
|
-
document.addEventListener("click", handleLinkTap, true);
|
|
1040
|
-
// Android WebView fallback: some builds drop the synthetic click after
|
|
1041
|
-
// touchend, so treat a stationary touchstart→touchend on the same link
|
|
1042
|
-
// as a tap. Anything that moves more than TAP_SLOP pixels is a scroll
|
|
1043
|
-
// and must NOT activate the link.
|
|
1044
|
-
var TAP_SLOP = 10;
|
|
1045
|
-
var lastTouchTarget = null;
|
|
1046
|
-
var lastTouchX = 0;
|
|
1047
|
-
var lastTouchY = 0;
|
|
1048
|
-
var touchMoved = false;
|
|
1049
|
-
// All touch listeners are passive so Android WebView can compositor-scroll
|
|
1050
|
-
// the iframe without waiting on our JS. handleLinkTap's preventDefault only
|
|
1051
|
-
// matters for the "click" path (which is non-passive by default).
|
|
1052
|
-
document.addEventListener("touchstart", function (e) {
|
|
1053
|
-
var t0 = e.touches && e.touches[0];
|
|
1054
|
-
lastTouchX = t0 ? t0.clientX : 0;
|
|
1055
|
-
lastTouchY = t0 ? t0.clientY : 0;
|
|
1056
|
-
touchMoved = false;
|
|
1057
|
-
lastTouchTarget = (e.target && e.target.closest) ? e.target.closest("a[href]") || e.target : e.target;
|
|
1058
|
-
}, { passive: true, capture: true });
|
|
1059
|
-
document.addEventListener("touchmove", function (e) {
|
|
1060
|
-
if (touchMoved) return;
|
|
1061
|
-
var t = e.touches && e.touches[0];
|
|
1062
|
-
if (!t) return;
|
|
1063
|
-
if (Math.abs(t.clientX - lastTouchX) > TAP_SLOP || Math.abs(t.clientY - lastTouchY) > TAP_SLOP) {
|
|
1064
|
-
touchMoved = true;
|
|
1065
|
-
lastTouchTarget = null;
|
|
1066
|
-
}
|
|
1067
|
-
}, { passive: true, capture: true });
|
|
1068
|
-
document.addEventListener("touchend", function (e) {
|
|
1069
|
-
if (touchMoved) { lastTouchTarget = null; touchMoved = false; return; }
|
|
1070
|
-
var t = (e.target && e.target.closest) ? e.target.closest("a[href]") || e.target : e.target;
|
|
1071
|
-
if (lastTouchTarget && lastTouchTarget === t) handleLinkTap(e);
|
|
1072
|
-
lastTouchTarget = null;
|
|
1073
|
-
}, { passive: true, capture: true });
|
|
1074
|
-
document.addEventListener("touchcancel", function () {
|
|
1075
|
-
lastTouchTarget = null; touchMoved = false;
|
|
1076
|
-
}, { passive: true, capture: true });
|
|
1077
|
-
// Link hover popover removed 2026-04-24 — user feedback: it persisted
|
|
1078
|
-
// over the message body when the dismissers (mousedown/scroll/blur)
|
|
1079
|
-
// didn't fire from inside the iframe, leaving a multi-line URL hanging
|
|
1080
|
-
// in the middle of the reading pane. Right-click (desktop) and long-
|
|
1081
|
-
// press (touch) on a link still open the existing C29 menu with Open /
|
|
1082
|
-
// Save-as / Copy URL / Copy link-text.
|
|
1083
|
-
// C29: right-click on a link → ask parent for the Open/Save/Copy menu.
|
|
1084
|
-
document.addEventListener("contextmenu", function (e) {
|
|
1085
|
-
var a = e.target && e.target.closest ? e.target.closest("a[href]") : null;
|
|
1086
|
-
if (!a) return; // let parent's body-context handler take over
|
|
1087
|
-
e.preventDefault();
|
|
1088
|
-
e.stopPropagation();
|
|
1089
|
-
var rect = (e.target.getBoundingClientRect && e.target.getBoundingClientRect()) || { left: 0, top: 0 };
|
|
1090
|
-
window.parent.postMessage({
|
|
1091
|
-
type: "linkContextMenu",
|
|
1092
|
-
url: a.href,
|
|
1093
|
-
text: (a.textContent || "").slice(0, 100),
|
|
1094
|
-
x: e.clientX, y: e.clientY,
|
|
1095
|
-
iframeLeft: rect.left, iframeTop: rect.top
|
|
1096
|
-
}, "*");
|
|
1097
|
-
});
|
|
1098
|
-
// Key forwarding — Delete, Ctrl+D, arrow keys, etc. need to reach app.ts
|
|
1099
|
-
// even when focus is inside the sandboxed iframe. Parent-side
|
|
1100
|
-
// contentDocument listeners (see installPreviewControls) work on
|
|
1101
|
-
// desktop WebView2 but not Android WebView, so we post every keydown
|
|
1102
|
-
// that isn't plain typing.
|
|
1103
|
-
document.addEventListener("keydown", function (e) {
|
|
1104
|
-
var t = e.target;
|
|
1105
|
-
if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return;
|
|
1106
|
-
// Zoom keys handled by parent-side installPreviewControls; don't double-send.
|
|
1107
|
-
if (e.ctrlKey && (e.key === "=" || e.key === "+" || e.key === "-" || e.key === "0")) return;
|
|
1108
|
-
// Preventing the iframe's default for keys we forward to the parent
|
|
1109
|
-
// is essential — the parent's preventDefault on the synthetic
|
|
1110
|
-
// keydown can't suppress the browser's reaction to the ORIGINAL
|
|
1111
|
-
// event (Ctrl+R reload, Ctrl+F find, etc.). Suppress here so the
|
|
1112
|
-
// browser doesn't act before the parent processes the action.
|
|
1113
|
-
var k = (e.key || "").toLowerCase();
|
|
1114
|
-
var isShortcut = e.ctrlKey && !e.altKey && !e.metaKey && (
|
|
1115
|
-
k === "r" || k === "f" || k === "n" || k === "a" || k === "d" ||
|
|
1116
|
-
k === "z" || k === "y" || k === "k"
|
|
1117
|
-
);
|
|
1118
|
-
if (isShortcut) e.preventDefault();
|
|
1119
|
-
window.parent.postMessage({
|
|
1120
|
-
type: "previewKey",
|
|
1121
|
-
key: e.key, code: e.code,
|
|
1122
|
-
ctrlKey: e.ctrlKey, shiftKey: e.shiftKey, altKey: e.altKey, metaKey: e.metaKey,
|
|
1123
|
-
}, "*");
|
|
1124
|
-
});
|
|
1125
|
-
})();
|
|
1126
|
-
</script>
|
|
1127
|
-
</head><body>${html}</body></html>`;
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
/** Open the current message in a separate view: floating draggable overlay
|
|
1131
|
-
* on desktop (multiple at once, like compose), full-screen mode on mobile.
|
|
1132
|
-
* Threshold matches the layout.css responsive breakpoint so the experience
|
|
1133
|
-
* is consistent with other narrow-mode behavior. Snapshot in time — the
|
|
1134
|
-
* pop-out doesn't auto-update if the user clicks another message. */
|
|
1135
|
-
export function popOutCurrentMessage(): void {
|
|
1136
|
-
if (!currentMessage) return;
|
|
1137
|
-
const isNarrow = window.innerWidth <= 768;
|
|
1138
|
-
if (isNarrow) {
|
|
1139
|
-
document.body.classList.toggle("viewer-fullscreen");
|
|
1140
|
-
return;
|
|
1141
|
-
}
|
|
1142
|
-
spawnDesktopPopout(currentMessage, currentAccountId);
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
/** Build a floating overlay carrying a snapshot of the message: header
|
|
1146
|
-
* (subject, from, to, date) + sandboxed body iframe + attachment chips.
|
|
1147
|
-
* Reuses the compose-overlay drag/resize/close pattern. Independent of the
|
|
1148
|
-
* main viewer — opening pop-out for message A then switching the main pane
|
|
1149
|
-
* to message B leaves A visible in its overlay. */
|
|
1150
|
-
function spawnDesktopPopout(msg: any, accountId: string): void {
|
|
1151
|
-
const wrapper = document.createElement("div");
|
|
1152
|
-
wrapper.className = "compose-overlay viewer-popout";
|
|
1153
|
-
wrapper.style.cssText = "position:fixed;top:60px;right:20px;width:min(720px,55vw);height:min(800px,80vh);z-index:1000;border:1px solid var(--color-border, #ccc);border-radius:8px;box-shadow:0 8px 32px rgba(0,0,0,0.3);display:flex;flex-direction:column;background:var(--color-bg, #fff);resize:both;overflow:hidden;";
|
|
1154
|
-
|
|
1155
|
-
const titleBar = document.createElement("div");
|
|
1156
|
-
titleBar.style.cssText = "display:flex;align-items:center;justify-content:space-between;padding:6px 10px;background:var(--color-bg-alt, #e8ecf0);color:var(--color-text, #000);border-radius:8px 8px 0 0;cursor:move;user-select:none;flex-shrink:0;font-size:13px;";
|
|
1157
|
-
const titleText = document.createElement("span");
|
|
1158
|
-
titleText.style.cssText = "overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;font-weight:600;";
|
|
1159
|
-
titleText.textContent = msg.subject || "(no subject)";
|
|
1160
|
-
titleBar.appendChild(titleText);
|
|
1161
|
-
|
|
1162
|
-
const closeBtn = document.createElement("button");
|
|
1163
|
-
closeBtn.textContent = "✕";
|
|
1164
|
-
closeBtn.title = "Close pop-out";
|
|
1165
|
-
closeBtn.style.cssText = "background:none;border:none;font-size:16px;cursor:pointer;color:#666;padding:2px 8px;border-radius:4px;flex-shrink:0;";
|
|
1166
|
-
closeBtn.addEventListener("mouseenter", () => closeBtn.style.color = "#c00");
|
|
1167
|
-
closeBtn.addEventListener("mouseleave", () => closeBtn.style.color = "#666");
|
|
1168
|
-
closeBtn.addEventListener("click", () => wrapper.remove());
|
|
1169
|
-
titleBar.appendChild(closeBtn);
|
|
1170
|
-
|
|
1171
|
-
const headerInfo = document.createElement("div");
|
|
1172
|
-
headerInfo.style.cssText = "padding:8px 12px;border-bottom:1px solid var(--color-border, #ddd);font-size:13px;line-height:1.4;flex-shrink:0;";
|
|
1173
|
-
const formatAddrLocal = (a: { name?: string; address: string }) =>
|
|
1174
|
-
a.name ? `${a.name} <${a.address}>` : a.address;
|
|
1175
|
-
const fromStr = formatAddrLocal(msg.from || { address: "" });
|
|
1176
|
-
const toStr = (msg.to || []).map(formatAddrLocal).join(", ");
|
|
1177
|
-
const ccStr = msg.cc?.length ? ` Cc: ${msg.cc.map(formatAddrLocal).join(", ")}` : "";
|
|
1178
|
-
const dateStr = msg.date ? new Date(msg.date).toLocaleString() : "";
|
|
1179
|
-
headerInfo.innerHTML =
|
|
1180
|
-
`<div><strong>${escapeHtmlLocal(fromStr)}</strong></div>` +
|
|
1181
|
-
`<div style="color:var(--color-text-muted, #666)">To: ${escapeHtmlLocal(toStr)}${escapeHtmlLocal(ccStr)}</div>` +
|
|
1182
|
-
`<div style="color:var(--color-text-muted, #666);font-size:12px">${escapeHtmlLocal(dateStr)}</div>`;
|
|
1183
|
-
|
|
1184
|
-
const bodyContainer = document.createElement("div");
|
|
1185
|
-
bodyContainer.style.cssText = "flex:1;overflow:hidden;display:flex;";
|
|
1186
|
-
if (msg.bodyHtml) {
|
|
1187
|
-
const iframe = document.createElement("iframe");
|
|
1188
|
-
iframe.sandbox.add("allow-same-origin");
|
|
1189
|
-
iframe.sandbox.add("allow-popups");
|
|
1190
|
-
iframe.sandbox.add("allow-popups-to-escape-sandbox");
|
|
1191
|
-
iframe.sandbox.add("allow-top-navigation-by-user-activation");
|
|
1192
|
-
iframe.sandbox.add("allow-scripts");
|
|
1193
|
-
iframe.srcdoc = wrapHtmlBody(msg.bodyHtml, msg.remoteAllowed);
|
|
1194
|
-
iframe.style.cssText = "flex:1;border:none;width:100%;background:#fff;";
|
|
1195
|
-
bodyContainer.appendChild(iframe);
|
|
1196
|
-
} else {
|
|
1197
|
-
const pre = document.createElement("pre");
|
|
1198
|
-
pre.style.cssText = "padding:12px;white-space:pre-wrap;word-break:break-word;margin:0;flex:1;overflow:auto;";
|
|
1199
|
-
pre.textContent = msg.bodyText || "(no content)";
|
|
1200
|
-
bodyContainer.appendChild(pre);
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
// Drag — same pattern as compose-overlay: pointer-events:none on the
|
|
1204
|
-
// iframe so cursor crossing into it doesn't lose drag events.
|
|
1205
|
-
let dragX = 0, dragY = 0;
|
|
1206
|
-
titleBar.addEventListener("mousedown", (e: MouseEvent) => {
|
|
1207
|
-
if (e.target === closeBtn) return;
|
|
1208
|
-
e.preventDefault();
|
|
1209
|
-
const rect = wrapper.getBoundingClientRect();
|
|
1210
|
-
dragX = e.clientX - rect.left;
|
|
1211
|
-
dragY = e.clientY - rect.top;
|
|
1212
|
-
const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v));
|
|
1213
|
-
bodyContainer.style.pointerEvents = "none";
|
|
1214
|
-
document.body.style.userSelect = "none";
|
|
1215
|
-
const onMove = (ev: MouseEvent) => {
|
|
1216
|
-
ev.preventDefault();
|
|
1217
|
-
const left = clamp(ev.clientX - dragX, 0, window.innerWidth - 40);
|
|
1218
|
-
const top = clamp(ev.clientY - dragY, 0, window.innerHeight - 40);
|
|
1219
|
-
wrapper.style.left = `${left}px`;
|
|
1220
|
-
wrapper.style.top = `${top}px`;
|
|
1221
|
-
wrapper.style.right = "auto";
|
|
1222
|
-
wrapper.style.bottom = "auto";
|
|
1223
|
-
};
|
|
1224
|
-
const onUp = () => {
|
|
1225
|
-
bodyContainer.style.pointerEvents = "";
|
|
1226
|
-
document.body.style.userSelect = "";
|
|
1227
|
-
document.removeEventListener("mousemove", onMove);
|
|
1228
|
-
document.removeEventListener("mouseup", onUp);
|
|
1229
|
-
};
|
|
1230
|
-
document.addEventListener("mousemove", onMove);
|
|
1231
|
-
document.addEventListener("mouseup", onUp);
|
|
1232
|
-
});
|
|
1233
|
-
|
|
1234
|
-
// Bring to front on click — shared with compose-overlay so they all
|
|
1235
|
-
// restack uniformly.
|
|
1236
|
-
wrapper.addEventListener("mousedown", () => {
|
|
1237
|
-
document.querySelectorAll(".compose-overlay").forEach(el => (el as HTMLElement).style.zIndex = "1000");
|
|
1238
|
-
wrapper.style.zIndex = "1001";
|
|
1239
|
-
});
|
|
1240
|
-
|
|
1241
|
-
// Cascade pop-outs so they don't all stack at the same coords.
|
|
1242
|
-
const existing = document.querySelectorAll(".viewer-popout").length;
|
|
1243
|
-
if (existing > 0) {
|
|
1244
|
-
wrapper.style.top = `${60 + existing * 28}px`;
|
|
1245
|
-
wrapper.style.right = `${20 + existing * 28}px`;
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
void accountId; // accountId reserved for future per-account actions on the popout
|
|
1249
|
-
wrapper.appendChild(titleBar);
|
|
1250
|
-
wrapper.appendChild(headerInfo);
|
|
1251
|
-
wrapper.appendChild(bodyContainer);
|
|
1252
|
-
document.body.appendChild(wrapper);
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
function escapeHtmlLocal(s: string): string {
|
|
1256
|
-
return (s || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1257
|
-
}
|