emailengine-app 1.14.7 → 2.60.1
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/.env.development +49 -0
- package/.env.example +82 -0
- package/.env.production +87 -0
- package/.eslintignore +1 -0
- package/.github/workflows/deploy.yml +104 -0
- package/.github/workflows/release.yaml +107 -0
- package/.github/workflows/test.yml +82 -0
- package/.ncurc.js +19 -5
- package/.prettierignore +44 -0
- package/CHANGELOG.md +1110 -0
- package/DOCKER_DEPLOYMENT.md +495 -0
- package/Dockerfile +53 -6
- package/Dockerfile-legacy +18 -0
- package/Fluid-Attacks-Results.csv +1 -0
- package/Gruntfile.js +46 -5
- package/LICENSE_EMAILENGINE.txt +110 -0
- package/README.md +73 -339
- package/app.json +40 -0
- package/bin/emailengine.js +283 -38
- package/config/default.toml +9 -11
- package/config/test.toml +45 -0
- package/copy-static-files.sh +34 -0
- package/data/google-crawlers.json +797 -0
- package/docker-compose.yml +103 -31
- package/encrypt.js +85 -10
- package/eslint.config.js +110 -0
- package/examples/auth-server.js +121 -69
- package/examples/grafana-dashboard.json +2375 -0
- package/help.txt +84 -0
- package/install.sh +426 -0
- package/lib/account.js +2348 -124
- package/lib/add-trackers.js +119 -0
- package/lib/api-routes/bull-board-routes.js +60 -0
- package/lib/api-routes/chat-routes.js +519 -0
- package/lib/api-routes/template-routes.js +490 -0
- package/lib/append-list.js +9 -2
- package/lib/arf-detect.js +200 -0
- package/lib/autodetect-imap-settings.js +781 -0
- package/lib/bounce-detect.js +280 -37
- package/lib/capa.js +97 -0
- package/lib/consts.js +210 -1
- package/lib/db.js +227 -8
- package/lib/document-store.js +54 -0
- package/lib/email-client/base-client.js +3677 -0
- package/lib/email-client/gmail-client.js +2796 -0
- package/lib/email-client/imap/mailbox.js +3721 -0
- package/lib/email-client/imap/subconnection.js +269 -0
- package/lib/email-client/imap-client.js +2628 -0
- package/lib/email-client/outlook-client.js +3805 -0
- package/lib/encrypt.js +85 -14
- package/lib/es.js +784 -0
- package/lib/feature-flags.js +42 -0
- package/lib/gateway.js +271 -0
- package/lib/generate-text-preview.js +56 -0
- package/lib/get-raw-email.js +302 -42
- package/lib/get-secret.js +23 -67
- package/lib/headers-rewriter.js +33 -0
- package/lib/imapproxy/imap-core/index.js +4 -0
- package/lib/imapproxy/imap-core/lib/commands/append.js +187 -0
- package/lib/imapproxy/imap-core/lib/commands/authenticate-plain.js +145 -0
- package/lib/imapproxy/imap-core/lib/commands/capability.js +13 -0
- package/lib/imapproxy/imap-core/lib/commands/check.js +10 -0
- package/lib/imapproxy/imap-core/lib/commands/close.js +44 -0
- package/lib/imapproxy/imap-core/lib/commands/compress.js +102 -0
- package/lib/imapproxy/imap-core/lib/commands/copy.js +109 -0
- package/lib/imapproxy/imap-core/lib/commands/create.js +93 -0
- package/lib/imapproxy/imap-core/lib/commands/delete.js +84 -0
- package/lib/imapproxy/imap-core/lib/commands/enable.js +36 -0
- package/lib/imapproxy/imap-core/lib/commands/expunge.js +68 -0
- package/lib/imapproxy/imap-core/lib/commands/fetch.js +385 -0
- package/lib/imapproxy/imap-core/lib/commands/getquota.js +85 -0
- package/lib/imapproxy/imap-core/lib/commands/getquotaroot.js +111 -0
- package/lib/imapproxy/imap-core/lib/commands/id.js +111 -0
- package/lib/imapproxy/imap-core/lib/commands/idle.js +45 -0
- package/lib/imapproxy/imap-core/lib/commands/list.js +218 -0
- package/lib/imapproxy/imap-core/lib/commands/login.js +135 -0
- package/lib/imapproxy/imap-core/lib/commands/logout.js +26 -0
- package/lib/imapproxy/imap-core/lib/commands/lsub.js +102 -0
- package/lib/imapproxy/imap-core/lib/commands/move.js +106 -0
- package/lib/imapproxy/imap-core/lib/commands/namespace.js +14 -0
- package/lib/imapproxy/imap-core/lib/commands/noop.js +10 -0
- package/lib/imapproxy/imap-core/lib/commands/rename.js +102 -0
- package/lib/imapproxy/imap-core/lib/commands/search.js +306 -0
- package/lib/imapproxy/imap-core/lib/commands/select.js +248 -0
- package/lib/imapproxy/imap-core/lib/commands/setquota.js +24 -0
- package/lib/imapproxy/imap-core/lib/commands/starttls.js +100 -0
- package/lib/imapproxy/imap-core/lib/commands/status.js +149 -0
- package/lib/imapproxy/imap-core/lib/commands/store.js +208 -0
- package/lib/imapproxy/imap-core/lib/commands/subscribe.js +69 -0
- package/lib/imapproxy/imap-core/lib/commands/uid-expunge.js +71 -0
- package/lib/imapproxy/imap-core/lib/commands/uid-store.js +170 -0
- package/lib/imapproxy/imap-core/lib/commands/unselect.js +14 -0
- package/lib/imapproxy/imap-core/lib/commands/unsubscribe.js +69 -0
- package/lib/imapproxy/imap-core/lib/handler/README.md +146 -0
- package/lib/imapproxy/imap-core/lib/handler/imap-compile-stream.js +252 -0
- package/lib/imapproxy/imap-core/lib/handler/imap-compiler.js +134 -0
- package/lib/imapproxy/imap-core/lib/handler/imap-formal-syntax.js +147 -0
- package/lib/imapproxy/imap-core/lib/handler/imap-handler.js +11 -0
- package/lib/imapproxy/imap-core/lib/handler/imap-parser.js +678 -0
- package/lib/imapproxy/imap-core/lib/imap-command.js +381 -0
- package/lib/imapproxy/imap-core/lib/imap-composer.js +71 -0
- package/lib/imapproxy/imap-core/lib/imap-connection.js +929 -0
- package/lib/imapproxy/imap-core/lib/imap-server.js +426 -0
- package/lib/imapproxy/imap-core/lib/imap-stream.js +172 -0
- package/lib/imapproxy/imap-core/lib/imap-tools.js +789 -0
- package/lib/imapproxy/imap-core/lib/indexer/body-structure.js +295 -0
- package/lib/imapproxy/imap-core/lib/indexer/create-envelope.js +103 -0
- package/lib/imapproxy/imap-core/lib/indexer/indexer.js +904 -0
- package/lib/imapproxy/imap-core/lib/indexer/parse-mime-tree.js +340 -0
- package/lib/imapproxy/imap-core/lib/length-limiter.js +76 -0
- package/lib/imapproxy/imap-core/lib/parse-date.js +225 -0
- package/lib/imapproxy/imap-core/lib/search.js +330 -0
- package/lib/imapproxy/imap-core/lib/tls-options.js +69 -0
- package/lib/imapproxy/imap-core/memory-notifier.js +129 -0
- package/lib/imapproxy/imap-core/test/client.js +46 -0
- package/lib/imapproxy/imap-core/test/fixtures/append.eml +1196 -0
- package/lib/imapproxy/imap-core/test/fixtures/chunks.js +44 -0
- package/lib/imapproxy/imap-core/test/fixtures/fix1.eml +6 -0
- package/lib/imapproxy/imap-core/test/fixtures/fix2.eml +599 -0
- package/lib/imapproxy/imap-core/test/fixtures/fix3.eml +32 -0
- package/lib/imapproxy/imap-core/test/fixtures/fix4.eml +6 -0
- package/lib/imapproxy/imap-core/test/fixtures/mimetorture.eml +599 -0
- package/lib/imapproxy/imap-core/test/fixtures/mimetorture.js +2740 -0
- package/lib/imapproxy/imap-core/test/fixtures/mimetorture.json +1411 -0
- package/lib/imapproxy/imap-core/test/fixtures/mimetree.js +85 -0
- package/lib/imapproxy/imap-core/test/fixtures/nodemailer.eml +582 -0
- package/lib/imapproxy/imap-core/test/fixtures/ryan_finnie_mime_torture.eml +599 -0
- package/lib/imapproxy/imap-core/test/fixtures/simple.eml +42 -0
- package/lib/imapproxy/imap-core/test/fixtures/simple.json +164 -0
- package/lib/imapproxy/imap-core/test/imap-compile-stream-test.js +671 -0
- package/lib/imapproxy/imap-core/test/imap-compiler-test.js +272 -0
- package/lib/imapproxy/imap-core/test/imap-indexer-test.js +236 -0
- package/lib/imapproxy/imap-core/test/imap-parser-test.js +922 -0
- package/lib/imapproxy/imap-core/test/memory-notifier.js +129 -0
- package/lib/imapproxy/imap-core/test/prepare.sh +74 -0
- package/lib/imapproxy/imap-core/test/protocol-test.js +1756 -0
- package/lib/imapproxy/imap-core/test/search-test.js +1356 -0
- package/lib/imapproxy/imap-core/test/test-client.js +152 -0
- package/lib/imapproxy/imap-core/test/test-server.js +623 -0
- package/lib/imapproxy/imap-core/test/tools-test.js +22 -0
- package/lib/imapproxy/imap-server.js +577 -0
- package/lib/lists.js +92 -0
- package/lib/llm-pre-process.js +141 -0
- package/lib/logger.js +43 -4
- package/lib/lua/ee-get-idempotency.lua +74 -0
- package/lib/lua/ee-list-add.lua +34 -0
- package/lib/lua/ee-list-remove.lua +37 -0
- package/lib/lua/h-incrby-exists.lua +28 -0
- package/lib/lua/h-push.lua +32 -0
- package/lib/lua/h-set-bigger.lua +40 -0
- package/lib/lua/h-set-exists.lua +29 -0
- package/lib/lua/h-set-new.lua +29 -0
- package/lib/lua/h-update-bigger.lua +45 -0
- package/lib/lua/s-list-accounts.lua +64 -14
- package/lib/lua/z-expunge.lua +86 -10
- package/lib/lua/z-get-by-uid.lua +28 -5
- package/lib/lua/z-get-mailbox-id.lua +24 -2
- package/lib/lua/z-get-mailbox-path.lua +16 -0
- package/lib/lua/z-get.lua +27 -4
- package/lib/lua/z-set.lua +24 -2
- package/lib/metrics-collector.js +209 -0
- package/lib/oauth/gmail.js +663 -0
- package/lib/oauth/mail-ru.js +310 -0
- package/lib/oauth/outlook.js +541 -0
- package/lib/oauth/pubsub/google.js +247 -0
- package/lib/oauth2-apps.js +1420 -0
- package/lib/outbox.js +140 -0
- package/lib/payload-examples-documents.json +404 -0
- package/lib/payload-examples-webhooks.json +266 -0
- package/lib/pre-process.js +193 -0
- package/lib/rate-limit.js +32 -0
- package/lib/reconnection-manager.js +106 -0
- package/lib/redis-scan-delete.js +82 -0
- package/lib/redis-url.js +78 -0
- package/lib/rewrite-text-nodes.js +267 -0
- package/lib/routes-ui.js +10247 -0
- package/lib/schemas.js +1577 -187
- package/lib/settings.js +263 -12
- package/lib/sub-script.js +109 -0
- package/lib/templates.js +240 -0
- package/lib/threads.js +155 -0
- package/lib/tokens.js +353 -0
- package/lib/tools.js +1773 -41
- package/lib/translations.js +33 -0
- package/lib/webhooks.js +605 -0
- package/list-generate.js +96 -0
- package/package.json +130 -54
- package/render.yaml +44 -0
- package/sbom.json +1 -0
- package/scan.js +14 -2
- package/scripts/README.md +50 -0
- package/scripts/refresh-test-tokens.js +180 -0
- package/server.js +2902 -376
- package/setup-production.sh +201 -0
- package/static/{bootstrap-4.6.0-dist → bootstrap-4.6.2-dist}/css/bootstrap-grid.css +3 -3
- package/static/bootstrap-4.6.2-dist/css/bootstrap-grid.css.map +1 -0
- package/static/{bootstrap-4.6.0-dist → bootstrap-4.6.2-dist}/css/bootstrap-grid.min.css +3 -3
- package/static/{bootstrap-4.6.0-dist → bootstrap-4.6.2-dist}/css/bootstrap-grid.min.css.map +1 -1
- package/static/{bootstrap-4.6.0-dist → bootstrap-4.6.2-dist}/css/bootstrap-reboot.css +3 -3
- package/static/bootstrap-4.6.2-dist/css/bootstrap-reboot.css.map +1 -0
- package/static/{bootstrap-4.6.0-dist → bootstrap-4.6.2-dist}/css/bootstrap-reboot.min.css +3 -3
- package/static/bootstrap-4.6.2-dist/css/bootstrap-reboot.min.css.map +1 -0
- package/static/{bootstrap-4.6.0-dist → bootstrap-4.6.2-dist}/css/bootstrap.css +60 -26
- package/static/bootstrap-4.6.2-dist/css/bootstrap.css.map +1 -0
- package/static/bootstrap-4.6.2-dist/css/bootstrap.min.css +7 -0
- package/static/bootstrap-4.6.2-dist/css/bootstrap.min.css.map +1 -0
- package/static/bootstrap-4.6.2-dist/js/bootstrap.bundle.js +7155 -0
- package/static/bootstrap-4.6.2-dist/js/bootstrap.bundle.js.map +784 -0
- package/static/bootstrap-4.6.2-dist/js/bootstrap.bundle.min.js +7 -0
- package/static/bootstrap-4.6.2-dist/js/bootstrap.bundle.min.js.map +959 -0
- package/static/{bootstrap-4.6.0-dist → bootstrap-4.6.2-dist}/js/bootstrap.js +792 -868
- package/static/bootstrap-4.6.2-dist/js/bootstrap.js.map +1 -0
- package/static/bootstrap-4.6.2-dist/js/bootstrap.min.js +7 -0
- package/static/bootstrap-4.6.2-dist/js/bootstrap.min.js.map +1 -0
- package/static/css/app.css +146 -0
- package/static/css/arena.css +777 -0
- package/static/css/default.min.css +9 -0
- package/static/css/highlight.min.css +9 -0
- package/static/css/sb-admin-2.min.css +10 -0
- package/static/emailengine.ico +0 -0
- package/static/favicon/android-chrome-192x192.png +0 -0
- package/static/favicon/android-chrome-512x512.png +0 -0
- package/static/favicon/apple-touch-icon.png +0 -0
- package/static/favicon/favicon-16x16.png +0 -0
- package/static/favicon/favicon-32x32.png +0 -0
- package/static/favicon/favicon.ico +0 -0
- package/static/favicon.ico +0 -0
- package/static/fonts/nunito/OFL.txt +93 -0
- package/static/fonts/nunito/XRXV3I6Li01BKofINeaBTMnFcQ.woff2 +0 -0
- package/static/fonts/nunito-font.css +66 -0
- package/static/front/EmailEngine_logo_horiz.png +0 -0
- package/static/front/EmailEngine_logo_vert.png +0 -0
- package/static/front/index.html +57 -0
- package/static/front/logo.png +0 -0
- package/static/imap-capabilities-1.csv +71 -0
- package/static/index.html +30 -713
- package/static/js/ace/README.txt +1 -0
- package/static/js/ace/ace.js +23 -0
- package/static/js/ace/ext-language_tools.js +8 -0
- package/static/js/ace/ext-searchbox.js +8 -0
- package/static/js/ace/mode-handlebars.js +8 -0
- package/static/js/ace/mode-html.js +8 -0
- package/static/js/ace/mode-javascript.js +8 -0
- package/static/js/ace/mode-json.js +8 -0
- package/static/js/ace/mode-markdown.js +8 -0
- package/static/js/ace/snippets/javascript.js +8 -0
- package/static/js/ace/snippets/markdown.js +8 -0
- package/static/js/ace/theme-kuroir.js +8 -0
- package/static/js/ace/theme-xcode.js +8 -0
- package/static/js/ace/worker-html.js +1 -0
- package/static/js/ace/worker-javascript.js +1 -0
- package/static/js/ace/worker-json.js +1 -0
- package/static/js/app.js +526 -0
- package/static/js/bootstrap-autocomplete.min.js +1 -0
- package/static/js/clipboard.min.js +517 -0
- package/static/js/ee-client.js +1977 -0
- package/static/js/evaluation-worker.js +47 -0
- package/static/js/highlight.min.js +1173 -0
- package/static/js/jquery-3.6.0.min.js +2 -0
- package/static/js/sb-admin-2.min.js +7 -0
- package/static/licenses.html +6606 -50
- package/static/logo/EmailEngine_logo_horiz.png +0 -0
- package/static/logo/EmailEngine_logo_vert.png +0 -0
- package/static/logo.png +0 -0
- package/static/logo_transparent.png +0 -0
- package/static/logo_transparent_small.png +0 -0
- package/static/logo_wide.png +0 -0
- package/static/preview/header-template.png +0 -0
- package/static/preview/render.png +0 -0
- package/static/preview/translation.png +0 -0
- package/static/providers/google_dark.png +0 -0
- package/static/providers/google_dark_edited.png +0 -0
- package/static/providers/google_light.png +0 -0
- package/static/providers/ms_dark.svg +1 -0
- package/static/providers/ms_light.svg +1 -0
- package/static/robots.txt +4 -0
- package/static/undraw_profile.svg +38 -0
- package/static/vendor/fontawesome-free/LICENSE.txt +34 -0
- package/static/vendor/fontawesome-free/attribution.js +3 -0
- package/static/vendor/fontawesome-free/css/all.css +4619 -0
- package/static/vendor/fontawesome-free/css/all.min.css +5 -0
- package/static/vendor/fontawesome-free/css/brands.css +15 -0
- package/static/vendor/fontawesome-free/css/brands.min.css +5 -0
- package/static/vendor/fontawesome-free/css/fontawesome.css +4585 -0
- package/static/vendor/fontawesome-free/css/fontawesome.min.css +5 -0
- package/static/vendor/fontawesome-free/css/regular.css +15 -0
- package/static/vendor/fontawesome-free/css/regular.min.css +5 -0
- package/static/vendor/fontawesome-free/css/solid.css +16 -0
- package/static/vendor/fontawesome-free/css/solid.min.css +5 -0
- package/static/vendor/fontawesome-free/css/svg-with-js.css +371 -0
- package/static/vendor/fontawesome-free/css/svg-with-js.min.css +5 -0
- package/static/vendor/fontawesome-free/css/v4-shims.css +2172 -0
- package/static/vendor/fontawesome-free/css/v4-shims.min.css +5 -0
- package/static/vendor/fontawesome-free/js/all.js +4467 -0
- package/static/vendor/fontawesome-free/js/all.min.js +5 -0
- package/static/vendor/fontawesome-free/js/brands.js +586 -0
- package/static/vendor/fontawesome-free/js/brands.min.js +5 -0
- package/static/vendor/fontawesome-free/js/conflict-detection.js +998 -0
- package/static/vendor/fontawesome-free/js/conflict-detection.min.js +5 -0
- package/static/vendor/fontawesome-free/js/fontawesome.js +2483 -0
- package/static/vendor/fontawesome-free/js/fontawesome.min.js +5 -0
- package/static/vendor/fontawesome-free/js/regular.js +280 -0
- package/static/vendor/fontawesome-free/js/regular.min.js +5 -0
- package/static/vendor/fontawesome-free/js/solid.js +1130 -0
- package/static/vendor/fontawesome-free/js/solid.min.js +5 -0
- package/static/vendor/fontawesome-free/js/v4-shims.js +68 -0
- package/static/vendor/fontawesome-free/js/v4-shims.min.js +5 -0
- package/static/vendor/fontawesome-free/less/_animated.less +19 -0
- package/static/vendor/fontawesome-free/less/_bordered-pulled.less +16 -0
- package/static/vendor/fontawesome-free/less/_core.less +12 -0
- package/static/vendor/fontawesome-free/less/_fixed-width.less +6 -0
- package/static/vendor/fontawesome-free/less/_icons.less +1462 -0
- package/static/vendor/fontawesome-free/less/_larger.less +27 -0
- package/static/vendor/fontawesome-free/less/_list.less +18 -0
- package/static/vendor/fontawesome-free/less/_mixins.less +56 -0
- package/static/vendor/fontawesome-free/less/_rotated-flipped.less +24 -0
- package/static/vendor/fontawesome-free/less/_screen-reader.less +5 -0
- package/static/vendor/fontawesome-free/less/_shims.less +2066 -0
- package/static/vendor/fontawesome-free/less/_stacked.less +22 -0
- package/static/vendor/fontawesome-free/less/_variables.less +1474 -0
- package/static/vendor/fontawesome-free/less/brands.less +23 -0
- package/static/vendor/fontawesome-free/less/fontawesome.less +16 -0
- package/static/vendor/fontawesome-free/less/regular.less +23 -0
- package/static/vendor/fontawesome-free/less/solid.less +24 -0
- package/static/vendor/fontawesome-free/less/v4-shims.less +6 -0
- package/static/vendor/fontawesome-free/metadata/categories.yml +2572 -0
- package/static/vendor/fontawesome-free/metadata/icons.yml +21783 -0
- package/static/vendor/fontawesome-free/metadata/shims.yml +298 -0
- package/static/vendor/fontawesome-free/metadata/sponsors.yml +744 -0
- package/static/vendor/fontawesome-free/package.json +58 -0
- package/static/vendor/fontawesome-free/scss/_animated.scss +20 -0
- package/static/vendor/fontawesome-free/scss/_bordered-pulled.scss +20 -0
- package/static/vendor/fontawesome-free/scss/_core.scss +21 -0
- package/static/vendor/fontawesome-free/scss/_fixed-width.scss +6 -0
- package/static/vendor/fontawesome-free/scss/_icons.scss +1462 -0
- package/static/vendor/fontawesome-free/scss/_larger.scss +23 -0
- package/static/vendor/fontawesome-free/scss/_list.scss +18 -0
- package/static/vendor/fontawesome-free/scss/_mixins.scss +56 -0
- package/static/vendor/fontawesome-free/scss/_rotated-flipped.scss +24 -0
- package/static/vendor/fontawesome-free/scss/_screen-reader.scss +5 -0
- package/static/vendor/fontawesome-free/scss/_shims.scss +2066 -0
- package/static/vendor/fontawesome-free/scss/_stacked.scss +31 -0
- package/static/vendor/fontawesome-free/scss/_variables.scss +1479 -0
- package/static/vendor/fontawesome-free/scss/brands.scss +23 -0
- package/static/vendor/fontawesome-free/scss/fontawesome.scss +16 -0
- package/static/vendor/fontawesome-free/scss/regular.scss +23 -0
- package/static/vendor/fontawesome-free/scss/solid.scss +24 -0
- package/static/vendor/fontawesome-free/scss/v4-shims.scss +6 -0
- package/static/vendor/fontawesome-free/sprites/brands.svg +1381 -0
- package/static/vendor/fontawesome-free/sprites/regular.svg +463 -0
- package/static/vendor/fontawesome-free/sprites/solid.svg +3013 -0
- package/static/vendor/fontawesome-free/svgs/brands/500px.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/accessible-icon.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/accusoft.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/acquisitions-incorporated.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/adn.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/adversal.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/affiliatetheme.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/airbnb.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/algolia.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/alipay.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/amazon-pay.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/amazon.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/amilia.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/android.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/angellist.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/angrycreative.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/angular.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/app-store-ios.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/app-store.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/apper.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/apple-pay.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/apple.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/artstation.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/asymmetrik.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/atlassian.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/audible.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/autoprefixer.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/avianex.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/aviato.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/aws.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/bandcamp.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/battle-net.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/behance-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/behance.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/bimobject.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/bitbucket.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/bitcoin.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/bity.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/black-tie.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/blackberry.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/blogger-b.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/blogger.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/bluetooth-b.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/bluetooth.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/bootstrap.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/btc.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/buffer.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/buromobelexperte.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/buy-n-large.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/buysellads.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/canadian-maple-leaf.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/cc-amazon-pay.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/cc-amex.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/cc-apple-pay.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/cc-diners-club.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/cc-discover.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/cc-jcb.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/cc-mastercard.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/cc-paypal.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/cc-stripe.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/cc-visa.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/centercode.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/centos.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/chrome.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/chromecast.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/cloudflare.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/cloudscale.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/cloudsmith.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/cloudversify.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/codepen.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/codiepie.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/confluence.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/connectdevelop.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/contao.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/cotton-bureau.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/cpanel.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/creative-commons-by.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/creative-commons-nc-eu.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/creative-commons-nc-jp.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/creative-commons-nc.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/creative-commons-nd.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/creative-commons-pd-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/creative-commons-pd.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/creative-commons-remix.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/creative-commons-sa.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/creative-commons-sampling-plus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/creative-commons-sampling.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/creative-commons-share.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/creative-commons-zero.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/creative-commons.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/critical-role.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/css3-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/css3.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/cuttlefish.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/d-and-d-beyond.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/d-and-d.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/dailymotion.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/dashcube.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/deezer.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/delicious.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/deploydog.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/deskpro.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/dev.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/deviantart.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/dhl.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/diaspora.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/digg.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/digital-ocean.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/discord.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/discourse.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/dochub.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/docker.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/draft2digital.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/dribbble-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/dribbble.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/dropbox.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/drupal.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/dyalog.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/earlybirds.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/ebay.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/edge-legacy.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/edge.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/elementor.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/ello.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/ember.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/empire.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/envira.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/erlang.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/ethereum.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/etsy.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/evernote.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/expeditedssl.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/facebook-f.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/facebook-messenger.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/facebook-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/facebook.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/fantasy-flight-games.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/fedex.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/fedora.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/figma.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/firefox-browser.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/firefox.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/first-order-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/first-order.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/firstdraft.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/flickr.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/flipboard.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/fly.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/font-awesome-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/font-awesome-flag.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/font-awesome-logo-full.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/font-awesome.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/fonticons-fi.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/fonticons.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/fort-awesome-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/fort-awesome.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/forumbee.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/foursquare.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/free-code-camp.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/freebsd.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/fulcrum.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/galactic-republic.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/galactic-senate.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/get-pocket.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/gg-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/gg.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/git-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/git-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/git.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/github-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/github-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/github.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/gitkraken.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/gitlab.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/gitter.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/glide-g.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/glide.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/gofore.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/goodreads-g.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/goodreads.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/google-drive.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/google-pay.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/google-play.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/google-plus-g.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/google-plus-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/google-plus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/google-wallet.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/google.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/gratipay.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/grav.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/gripfire.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/grunt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/guilded.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/gulp.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/hacker-news-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/hacker-news.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/hackerrank.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/hips.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/hire-a-helper.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/hive.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/hooli.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/hornbill.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/hotjar.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/houzz.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/html5.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/hubspot.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/ideal.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/imdb.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/innosoft.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/instagram-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/instagram.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/instalod.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/intercom.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/internet-explorer.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/invision.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/ioxhost.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/itch-io.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/itunes-note.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/itunes.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/java.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/jedi-order.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/jenkins.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/jira.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/joget.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/joomla.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/js-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/js.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/jsfiddle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/kaggle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/keybase.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/keycdn.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/kickstarter-k.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/kickstarter.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/korvue.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/laravel.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/lastfm-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/lastfm.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/leanpub.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/less.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/line.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/linkedin-in.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/linkedin.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/linode.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/linux.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/lyft.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/magento.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/mailchimp.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/mandalorian.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/markdown.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/mastodon.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/maxcdn.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/mdb.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/medapps.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/medium-m.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/medium.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/medrt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/meetup.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/megaport.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/mendeley.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/microblog.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/microsoft.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/mix.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/mixcloud.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/mixer.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/mizuni.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/modx.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/monero.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/napster.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/neos.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/nimblr.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/node-js.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/node.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/npm.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/ns8.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/nutritionix.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/octopus-deploy.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/odnoklassniki-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/odnoklassniki.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/old-republic.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/opencart.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/openid.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/opera.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/optin-monster.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/orcid.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/osi.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/page4.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/pagelines.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/palfed.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/patreon.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/paypal.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/penny-arcade.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/perbyte.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/periscope.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/phabricator.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/phoenix-framework.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/phoenix-squadron.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/php.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/pied-piper-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/pied-piper-hat.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/pied-piper-pp.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/pied-piper-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/pied-piper.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/pinterest-p.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/pinterest-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/pinterest.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/playstation.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/product-hunt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/pushed.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/python.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/qq.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/quinscape.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/quora.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/r-project.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/raspberry-pi.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/ravelry.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/react.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/reacteurope.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/readme.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/rebel.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/red-river.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/reddit-alien.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/reddit-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/reddit.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/redhat.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/renren.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/replyd.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/researchgate.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/resolving.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/rev.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/rocketchat.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/rockrms.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/rust.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/safari.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/salesforce.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/sass.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/schlix.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/scribd.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/searchengin.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/sellcast.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/sellsy.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/servicestack.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/shirtsinbulk.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/shopify.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/shopware.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/simplybuilt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/sistrix.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/sith.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/sketch.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/skyatlas.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/skype.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/slack-hash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/slack.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/slideshare.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/snapchat-ghost.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/snapchat-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/snapchat.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/soundcloud.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/sourcetree.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/speakap.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/speaker-deck.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/spotify.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/squarespace.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/stack-exchange.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/stack-overflow.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/stackpath.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/staylinked.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/steam-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/steam-symbol.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/steam.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/sticker-mule.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/strava.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/stripe-s.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/stripe.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/studiovinari.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/stumbleupon-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/stumbleupon.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/superpowers.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/supple.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/suse.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/swift.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/symfony.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/teamspeak.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/telegram-plane.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/telegram.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/tencent-weibo.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/the-red-yeti.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/themeco.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/themeisle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/think-peaks.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/tiktok.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/trade-federation.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/trello.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/tripadvisor.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/tumblr-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/tumblr.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/twitch.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/twitter-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/twitter.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/typo3.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/uber.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/ubuntu.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/uikit.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/umbraco.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/uncharted.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/uniregistry.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/unity.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/unsplash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/untappd.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/ups.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/usb.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/usps.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/ussunnah.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/vaadin.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/viacoin.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/viadeo-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/viadeo.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/viber.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/vimeo-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/vimeo-v.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/vimeo.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/vine.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/vk.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/vnv.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/vuejs.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/watchman-monitoring.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/waze.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/weebly.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/weibo.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/weixin.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/whatsapp-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/whatsapp.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/whmcs.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/wikipedia-w.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/windows.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/wix.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/wizards-of-the-coast.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/wodu.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/wolf-pack-battalion.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/wordpress-simple.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/wordpress.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/wpbeginner.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/wpexplorer.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/wpforms.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/wpressr.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/xbox.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/xing-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/xing.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/y-combinator.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/yahoo.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/yammer.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/yandex-international.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/yandex.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/yarn.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/yelp.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/yoast.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/youtube-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/youtube.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/brands/zhihu.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/address-book.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/address-card.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/angry.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/arrow-alt-circle-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/arrow-alt-circle-left.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/arrow-alt-circle-right.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/arrow-alt-circle-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/bell-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/bell.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/bookmark.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/building.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/calendar-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/calendar-check.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/calendar-minus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/calendar-plus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/calendar-times.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/calendar.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/caret-square-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/caret-square-left.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/caret-square-right.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/caret-square-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/chart-bar.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/check-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/check-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/clipboard.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/clock.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/clone.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/closed-captioning.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/comment-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/comment-dots.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/comment.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/comments.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/compass.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/copy.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/copyright.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/credit-card.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/dizzy.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/dot-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/edit.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/envelope-open.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/envelope.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/eye-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/eye.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/file-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/file-archive.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/file-audio.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/file-code.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/file-excel.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/file-image.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/file-pdf.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/file-powerpoint.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/file-video.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/file-word.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/file.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/flag.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/flushed.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/folder-open.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/folder.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/font-awesome-logo-full.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/frown-open.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/frown.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/futbol.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/gem.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/grimace.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/grin-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/grin-beam-sweat.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/grin-beam.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/grin-hearts.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/grin-squint-tears.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/grin-squint.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/grin-stars.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/grin-tears.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/grin-tongue-squint.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/grin-tongue-wink.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/grin-tongue.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/grin-wink.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/grin.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/hand-lizard.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/hand-paper.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/hand-peace.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/hand-point-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/hand-point-left.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/hand-point-right.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/hand-point-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/hand-pointer.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/hand-rock.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/hand-scissors.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/hand-spock.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/handshake.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/hdd.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/heart.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/hospital.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/hourglass.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/id-badge.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/id-card.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/image.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/images.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/keyboard.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/kiss-beam.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/kiss-wink-heart.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/kiss.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/laugh-beam.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/laugh-squint.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/laugh-wink.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/laugh.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/lemon.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/life-ring.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/lightbulb.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/list-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/map.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/meh-blank.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/meh-rolling-eyes.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/meh.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/minus-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/money-bill-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/moon.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/newspaper.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/object-group.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/object-ungroup.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/paper-plane.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/pause-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/play-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/plus-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/question-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/registered.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/sad-cry.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/sad-tear.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/save.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/share-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/smile-beam.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/smile-wink.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/smile.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/snowflake.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/star-half.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/star.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/sticky-note.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/stop-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/sun.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/surprise.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/thumbs-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/thumbs-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/times-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/tired.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/trash-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/user-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/user.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/window-close.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/window-maximize.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/window-minimize.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/regular/window-restore.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/ad.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/address-book.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/address-card.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/adjust.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/air-freshener.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/align-center.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/align-justify.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/align-left.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/align-right.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/allergies.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/ambulance.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/american-sign-language-interpreting.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/anchor.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/angle-double-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/angle-double-left.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/angle-double-right.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/angle-double-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/angle-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/angle-left.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/angle-right.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/angle-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/angry.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/ankh.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/apple-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/archive.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/archway.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/arrow-alt-circle-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/arrow-alt-circle-left.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/arrow-alt-circle-right.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/arrow-alt-circle-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/arrow-circle-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/arrow-circle-left.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/arrow-circle-right.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/arrow-circle-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/arrow-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/arrow-left.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/arrow-right.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/arrow-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/arrows-alt-h.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/arrows-alt-v.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/arrows-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/assistive-listening-systems.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/asterisk.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/at.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/atlas.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/atom.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/audio-description.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/award.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/baby-carriage.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/baby.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/backspace.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/backward.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bacon.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bacteria.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bacterium.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bahai.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/balance-scale-left.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/balance-scale-right.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/balance-scale.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/ban.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/band-aid.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/barcode.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bars.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/baseball-ball.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/basketball-ball.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bath.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/battery-empty.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/battery-full.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/battery-half.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/battery-quarter.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/battery-three-quarters.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bed.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/beer.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bell-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bell.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bezier-curve.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bible.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bicycle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/biking.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/binoculars.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/biohazard.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/birthday-cake.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/blender-phone.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/blender.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/blind.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/blog.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bold.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bolt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bomb.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bone.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bong.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/book-dead.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/book-medical.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/book-open.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/book-reader.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/book.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bookmark.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/border-all.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/border-none.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/border-style.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bowling-ball.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/box-open.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/box-tissue.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/box.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/boxes.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/braille.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/brain.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bread-slice.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/briefcase-medical.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/briefcase.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/broadcast-tower.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/broom.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/brush.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bug.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/building.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bullhorn.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bullseye.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/burn.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bus-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/bus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/business-time.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/calculator.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/calendar-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/calendar-check.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/calendar-day.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/calendar-minus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/calendar-plus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/calendar-times.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/calendar-week.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/calendar.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/camera-retro.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/camera.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/campground.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/candy-cane.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cannabis.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/capsules.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/car-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/car-battery.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/car-crash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/car-side.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/car.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/caravan.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/caret-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/caret-left.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/caret-right.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/caret-square-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/caret-square-left.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/caret-square-right.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/caret-square-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/caret-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/carrot.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cart-arrow-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cart-plus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cash-register.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cat.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/certificate.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chair.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chalkboard-teacher.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chalkboard.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/charging-station.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chart-area.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chart-bar.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chart-line.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chart-pie.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/check-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/check-double.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/check-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/check.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cheese.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chess-bishop.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chess-board.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chess-king.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chess-knight.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chess-pawn.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chess-queen.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chess-rook.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chess.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chevron-circle-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chevron-circle-left.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chevron-circle-right.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chevron-circle-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chevron-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chevron-left.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chevron-right.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/chevron-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/child.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/church.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/circle-notch.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/city.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/clinic-medical.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/clipboard-check.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/clipboard-list.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/clipboard.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/clock.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/clone.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/closed-captioning.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cloud-download-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cloud-meatball.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cloud-moon-rain.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cloud-moon.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cloud-rain.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cloud-showers-heavy.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cloud-sun-rain.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cloud-sun.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cloud-upload-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cloud.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cocktail.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/code-branch.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/code.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/coffee.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cog.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cogs.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/coins.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/columns.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/comment-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/comment-dollar.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/comment-dots.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/comment-medical.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/comment-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/comment.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/comments-dollar.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/comments.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/compact-disc.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/compass.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/compress-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/compress-arrows-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/compress.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/concierge-bell.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cookie-bite.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cookie.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/copy.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/copyright.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/couch.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/credit-card.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/crop-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/crop.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cross.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/crosshairs.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/crow.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/crown.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/crutch.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cube.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cubes.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/cut.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/database.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/deaf.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/democrat.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/desktop.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dharmachakra.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/diagnoses.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dice-d20.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dice-d6.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dice-five.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dice-four.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dice-one.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dice-six.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dice-three.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dice-two.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dice.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/digital-tachograph.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/directions.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/disease.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/divide.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dizzy.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dna.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dog.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dollar-sign.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dolly-flatbed.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dolly.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/donate.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/door-closed.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/door-open.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dot-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dove.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/download.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/drafting-compass.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dragon.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/draw-polygon.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/drum-steelpan.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/drum.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/drumstick-bite.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dumbbell.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dumpster-fire.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dumpster.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/dungeon.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/edit.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/egg.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/eject.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/ellipsis-h.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/ellipsis-v.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/envelope-open-text.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/envelope-open.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/envelope-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/envelope.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/equals.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/eraser.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/ethernet.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/euro-sign.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/exchange-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/exclamation-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/exclamation-triangle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/exclamation.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/expand-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/expand-arrows-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/expand.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/external-link-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/external-link-square-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/eye-dropper.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/eye-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/eye.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/fan.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/fast-backward.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/fast-forward.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/faucet.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/fax.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/feather-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/feather.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/female.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/fighter-jet.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-archive.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-audio.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-code.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-contract.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-csv.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-download.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-excel.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-export.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-image.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-import.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-invoice-dollar.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-invoice.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-medical-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-medical.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-pdf.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-powerpoint.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-prescription.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-signature.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-upload.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-video.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file-word.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/file.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/fill-drip.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/fill.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/film.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/filter.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/fingerprint.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/fire-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/fire-extinguisher.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/fire.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/first-aid.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/fish.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/fist-raised.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/flag-checkered.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/flag-usa.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/flag.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/flask.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/flushed.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/folder-minus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/folder-open.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/folder-plus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/folder.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/font-awesome-logo-full.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/font.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/football-ball.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/forward.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/frog.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/frown-open.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/frown.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/funnel-dollar.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/futbol.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/gamepad.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/gas-pump.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/gavel.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/gem.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/genderless.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/ghost.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/gift.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/gifts.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/glass-cheers.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/glass-martini-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/glass-martini.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/glass-whiskey.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/glasses.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/globe-africa.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/globe-americas.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/globe-asia.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/globe-europe.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/globe.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/golf-ball.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/gopuram.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/graduation-cap.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/greater-than-equal.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/greater-than.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/grimace.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/grin-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/grin-beam-sweat.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/grin-beam.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/grin-hearts.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/grin-squint-tears.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/grin-squint.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/grin-stars.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/grin-tears.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/grin-tongue-squint.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/grin-tongue-wink.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/grin-tongue.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/grin-wink.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/grin.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/grip-horizontal.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/grip-lines-vertical.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/grip-lines.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/grip-vertical.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/guitar.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/h-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hamburger.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hammer.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hamsa.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hand-holding-heart.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hand-holding-medical.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hand-holding-usd.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hand-holding-water.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hand-holding.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hand-lizard.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hand-middle-finger.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hand-paper.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hand-peace.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hand-point-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hand-point-left.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hand-point-right.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hand-point-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hand-pointer.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hand-rock.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hand-scissors.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hand-sparkles.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hand-spock.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hands-helping.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hands-wash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hands.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/handshake-alt-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/handshake-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/handshake.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hanukiah.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hard-hat.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hashtag.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hat-cowboy-side.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hat-cowboy.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hat-wizard.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hdd.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/head-side-cough-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/head-side-cough.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/head-side-mask.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/head-side-virus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/heading.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/headphones-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/headphones.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/headset.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/heart-broken.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/heart.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/heartbeat.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/helicopter.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/highlighter.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hiking.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hippo.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/history.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hockey-puck.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/holly-berry.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/home.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/horse-head.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/horse.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hospital-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hospital-symbol.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hospital-user.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hospital.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hot-tub.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hotdog.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hotel.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hourglass-end.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hourglass-half.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hourglass-start.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hourglass.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/house-damage.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/house-user.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/hryvnia.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/i-cursor.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/ice-cream.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/icicles.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/icons.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/id-badge.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/id-card-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/id-card.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/igloo.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/image.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/images.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/inbox.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/indent.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/industry.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/infinity.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/info-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/info.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/italic.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/jedi.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/joint.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/journal-whills.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/kaaba.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/key.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/keyboard.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/khanda.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/kiss-beam.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/kiss-wink-heart.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/kiss.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/kiwi-bird.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/landmark.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/language.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/laptop-code.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/laptop-house.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/laptop-medical.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/laptop.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/laugh-beam.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/laugh-squint.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/laugh-wink.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/laugh.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/layer-group.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/leaf.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/lemon.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/less-than-equal.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/less-than.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/level-down-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/level-up-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/life-ring.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/lightbulb.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/link.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/lira-sign.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/list-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/list-ol.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/list-ul.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/list.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/location-arrow.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/lock-open.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/lock.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/long-arrow-alt-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/long-arrow-alt-left.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/long-arrow-alt-right.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/long-arrow-alt-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/low-vision.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/luggage-cart.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/lungs-virus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/lungs.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/magic.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/magnet.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/mail-bulk.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/male.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/map-marked-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/map-marked.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/map-marker-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/map-marker.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/map-pin.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/map-signs.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/map.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/marker.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/mars-double.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/mars-stroke-h.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/mars-stroke-v.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/mars-stroke.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/mars.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/mask.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/medal.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/medkit.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/meh-blank.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/meh-rolling-eyes.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/meh.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/memory.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/menorah.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/mercury.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/meteor.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/microchip.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/microphone-alt-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/microphone-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/microphone-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/microphone.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/microscope.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/minus-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/minus-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/minus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/mitten.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/mobile-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/mobile.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/money-bill-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/money-bill-wave-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/money-bill-wave.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/money-bill.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/money-check-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/money-check.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/monument.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/moon.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/mortar-pestle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/mosque.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/motorcycle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/mountain.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/mouse-pointer.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/mouse.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/mug-hot.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/music.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/network-wired.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/neuter.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/newspaper.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/not-equal.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/notes-medical.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/object-group.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/object-ungroup.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/oil-can.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/om.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/otter.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/outdent.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/pager.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/paint-brush.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/paint-roller.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/palette.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/pallet.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/paper-plane.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/paperclip.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/parachute-box.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/paragraph.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/parking.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/passport.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/pastafarianism.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/paste.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/pause-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/pause.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/paw.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/peace.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/pen-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/pen-fancy.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/pen-nib.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/pen-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/pen.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/pencil-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/pencil-ruler.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/people-arrows.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/people-carry.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/pepper-hot.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/percent.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/percentage.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/person-booth.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/phone-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/phone-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/phone-square-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/phone-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/phone-volume.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/phone.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/photo-video.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/piggy-bank.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/pills.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/pizza-slice.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/place-of-worship.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/plane-arrival.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/plane-departure.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/plane-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/plane.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/play-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/play.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/plug.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/plus-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/plus-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/plus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/podcast.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/poll-h.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/poll.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/poo-storm.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/poo.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/poop.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/portrait.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/pound-sign.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/power-off.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/pray.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/praying-hands.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/prescription-bottle-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/prescription-bottle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/prescription.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/print.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/procedures.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/project-diagram.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/pump-medical.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/pump-soap.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/puzzle-piece.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/qrcode.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/question-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/question.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/quidditch.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/quote-left.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/quote-right.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/quran.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/radiation-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/radiation.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/rainbow.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/random.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/receipt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/record-vinyl.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/recycle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/redo-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/redo.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/registered.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/remove-format.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/reply-all.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/reply.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/republican.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/restroom.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/retweet.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/ribbon.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/ring.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/road.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/robot.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/rocket.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/route.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/rss-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/rss.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/ruble-sign.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/ruler-combined.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/ruler-horizontal.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/ruler-vertical.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/ruler.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/running.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/rupee-sign.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sad-cry.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sad-tear.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/satellite-dish.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/satellite.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/save.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/school.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/screwdriver.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/scroll.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sd-card.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/search-dollar.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/search-location.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/search-minus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/search-plus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/search.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/seedling.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/server.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/shapes.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/share-alt-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/share-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/share-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/share.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/shekel-sign.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/shield-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/shield-virus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/ship.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/shipping-fast.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/shoe-prints.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/shopping-bag.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/shopping-basket.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/shopping-cart.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/shower.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/shuttle-van.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sign-in-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sign-language.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sign-out-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sign.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/signal.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/signature.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sim-card.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sink.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sitemap.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/skating.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/skiing-nordic.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/skiing.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/skull-crossbones.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/skull.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sleigh.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sliders-h.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/smile-beam.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/smile-wink.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/smile.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/smog.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/smoking-ban.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/smoking.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sms.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/snowboarding.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/snowflake.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/snowman.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/snowplow.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/soap.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/socks.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/solar-panel.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sort-alpha-down-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sort-alpha-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sort-alpha-up-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sort-alpha-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sort-amount-down-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sort-amount-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sort-amount-up-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sort-amount-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sort-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sort-numeric-down-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sort-numeric-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sort-numeric-up-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sort-numeric-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sort-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sort.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/spa.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/space-shuttle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/spell-check.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/spider.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/spinner.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/splotch.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/spray-can.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/square-full.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/square-root-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/stamp.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/star-and-crescent.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/star-half-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/star-half.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/star-of-david.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/star-of-life.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/star.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/step-backward.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/step-forward.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/stethoscope.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sticky-note.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/stop-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/stop.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/stopwatch-20.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/stopwatch.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/store-alt-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/store-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/store-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/store.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/stream.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/street-view.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/strikethrough.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/stroopwafel.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/subscript.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/subway.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/suitcase-rolling.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/suitcase.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sun.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/superscript.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/surprise.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/swatchbook.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/swimmer.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/swimming-pool.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/synagogue.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sync-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/sync.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/syringe.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/table-tennis.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/table.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tablet-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tablet.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tablets.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tachometer-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tag.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tags.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tape.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tasks.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/taxi.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/teeth-open.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/teeth.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/temperature-high.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/temperature-low.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tenge.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/terminal.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/text-height.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/text-width.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/th-large.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/th-list.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/th.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/theater-masks.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/thermometer-empty.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/thermometer-full.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/thermometer-half.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/thermometer-quarter.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/thermometer-three-quarters.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/thermometer.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/thumbs-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/thumbs-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/thumbtack.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/ticket-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/times-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/times.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tint-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tint.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tired.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/toggle-off.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/toggle-on.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/toilet-paper-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/toilet-paper.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/toilet.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/toolbox.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tools.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tooth.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/torah.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/torii-gate.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tractor.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/trademark.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/traffic-light.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/trailer.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/train.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tram.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/transgender-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/transgender.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/trash-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/trash-restore-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/trash-restore.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/trash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tree.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/trophy.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/truck-loading.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/truck-monster.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/truck-moving.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/truck-pickup.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/truck.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tshirt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tty.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/tv.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/umbrella-beach.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/umbrella.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/underline.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/undo-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/undo.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/universal-access.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/university.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/unlink.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/unlock-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/unlock.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/upload.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-alt-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-astronaut.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-check.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-circle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-clock.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-cog.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-edit.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-friends.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-graduate.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-injured.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-lock.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-md.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-minus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-ninja.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-nurse.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-plus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-secret.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-shield.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-tag.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-tie.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user-times.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/user.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/users-cog.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/users-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/users.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/utensil-spoon.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/utensils.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/vector-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/venus-double.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/venus-mars.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/venus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/vest-patches.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/vest.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/vial.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/vials.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/video-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/video.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/vihara.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/virus-slash.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/virus.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/viruses.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/voicemail.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/volleyball-ball.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/volume-down.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/volume-mute.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/volume-off.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/volume-up.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/vote-yea.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/vr-cardboard.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/walking.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/wallet.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/warehouse.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/water.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/wave-square.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/weight-hanging.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/weight.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/wheelchair.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/wifi.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/wind.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/window-close.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/window-maximize.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/window-minimize.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/window-restore.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/wine-bottle.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/wine-glass-alt.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/wine-glass.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/won-sign.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/wrench.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/x-ray.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/yen-sign.svg +1 -0
- package/static/vendor/fontawesome-free/svgs/solid/yin-yang.svg +1 -0
- package/static/vendor/fontawesome-free/webfonts/fa-brands-400.eot +0 -0
- package/static/vendor/fontawesome-free/webfonts/fa-brands-400.svg +3717 -0
- package/static/vendor/fontawesome-free/webfonts/fa-brands-400.ttf +0 -0
- package/static/vendor/fontawesome-free/webfonts/fa-brands-400.woff +0 -0
- package/static/vendor/fontawesome-free/webfonts/fa-brands-400.woff2 +0 -0
- package/static/vendor/fontawesome-free/webfonts/fa-regular-400.eot +0 -0
- package/static/vendor/fontawesome-free/webfonts/fa-regular-400.svg +801 -0
- package/static/vendor/fontawesome-free/webfonts/fa-regular-400.ttf +0 -0
- package/static/vendor/fontawesome-free/webfonts/fa-regular-400.woff +0 -0
- package/static/vendor/fontawesome-free/webfonts/fa-regular-400.woff2 +0 -0
- package/static/vendor/fontawesome-free/webfonts/fa-solid-900.eot +0 -0
- package/static/vendor/fontawesome-free/webfonts/fa-solid-900.svg +5034 -0
- package/static/vendor/fontawesome-free/webfonts/fa-solid-900.ttf +0 -0
- package/static/vendor/fontawesome-free/webfonts/fa-solid-900.woff +0 -0
- package/static/vendor/fontawesome-free/webfonts/fa-solid-900.woff2 +0 -0
- package/static/vendor/handlebars/handlebars.min-v4.7.7.js +29 -0
- package/static/vendor/jquery/jquery.js +10881 -0
- package/static/vendor/jquery/jquery.min.js +2 -0
- package/static/vendor/jquery/jquery.min.map +1 -0
- package/static/vendor/jquery/jquery.slim.js +8782 -0
- package/static/vendor/jquery/jquery.slim.min.js +2 -0
- package/static/vendor/jquery/jquery.slim.min.map +1 -0
- package/static/vendor/jquery-easing/jquery.easing.min.js +1 -0
- package/systemd/emailengine.service +11 -3
- package/systemd/nginx-proxy.conf +1 -1
- package/test/api-test.js +899 -0
- package/test/bounce-test.js +151 -0
- package/test/fixtures/bounces/163.eml +2521 -0
- package/test/fixtures/bounces/fastmail.eml +242 -0
- package/test/fixtures/bounces/gmail.eml +252 -0
- package/test/fixtures/bounces/hotmail.eml +655 -0
- package/test/fixtures/bounces/mailru.eml +121 -0
- package/test/fixtures/bounces/outlook.eml +1107 -0
- package/test/fixtures/bounces/postfix.eml +101 -0
- package/test/fixtures/bounces/rambler.eml +116 -0
- package/test/fixtures/bounces/workmail.eml +142 -0
- package/test/fixtures/bounces/yahoo.eml +139 -0
- package/test/fixtures/bounces/zoho.eml +83 -0
- package/test/fixtures/bounces/zonemta.eml +100 -0
- package/test/oauth2-apps-test.js +301 -0
- package/test/sendonly-test.js +160 -0
- package/test/test-config.js +34 -0
- package/test/webhooks-server.js +39 -0
- package/translations/README.md +16 -0
- package/translations/de.mo +0 -0
- package/translations/de.po +335 -0
- package/translations/en.mo +0 -0
- package/translations/en.po +310 -0
- package/translations/et.mo +0 -0
- package/translations/et.po +331 -0
- package/translations/fr.mo +0 -0
- package/translations/fr.po +333 -0
- package/translations/ja.mo +0 -0
- package/translations/ja.po +322 -0
- package/translations/locales.json +43 -0
- package/translations/messages.pot +323 -0
- package/translations/nl.mo +0 -0
- package/translations/nl.po +325 -0
- package/translations/pl.mo +0 -0
- package/translations/pl.po +328 -0
- package/update-info.sh +10 -0
- package/views/account/login.hbs +54 -0
- package/views/account/password.hbs +88 -0
- package/views/account/security.hbs +269 -0
- package/views/account/totp.hbs +30 -0
- package/views/accounts/account.hbs +1254 -0
- package/views/accounts/browse.hbs +102 -0
- package/views/accounts/edit.hbs +332 -0
- package/views/accounts/index.hbs +143 -0
- package/views/accounts/register/imap-server.hbs +507 -0
- package/views/accounts/register/imap.hbs +56 -0
- package/views/accounts/register/index.hbs +52 -0
- package/views/arena/index.hbs +4 -0
- package/views/config/ai.hbs +820 -0
- package/views/config/document-store/chat.hbs +362 -0
- package/views/config/document-store/index.hbs +231 -0
- package/views/config/document-store/mappings/index.hbs +116 -0
- package/views/config/document-store/mappings/new.hbs +95 -0
- package/views/config/document-store/pre-processing/index.hbs +459 -0
- package/views/config/imap-proxy.hbs +479 -0
- package/views/config/license.hbs +256 -0
- package/views/config/logging.hbs +61 -0
- package/views/config/network.hbs +334 -0
- package/views/config/oauth/app.hbs +309 -0
- package/views/config/oauth/edit.hbs +92 -0
- package/views/config/oauth/index.hbs +150 -0
- package/views/config/oauth/new.hbs +90 -0
- package/views/config/oauth.hbs +354 -0
- package/views/config/service-preview.hbs +14 -0
- package/views/config/service.hbs +718 -0
- package/views/config/smtp.hbs +525 -0
- package/views/config/webhooks.hbs +404 -0
- package/views/dashboard.hbs +315 -0
- package/views/error.hbs +6 -1
- package/views/gateways/edit.hbs +52 -0
- package/views/gateways/gateway.hbs +120 -0
- package/views/gateways/index.hbs +152 -0
- package/views/gateways/new.hbs +61 -0
- package/views/index.hbs +21 -0
- package/views/internals/index.hbs +170 -0
- package/views/internals/thread.hbs +143 -0
- package/views/layout/app.hbs +516 -0
- package/views/layout/login.hbs +78 -0
- package/views/layout/main.hbs +67 -0
- package/views/layout/public.hbs +90 -0
- package/views/legal.hbs +83 -0
- package/views/license.hbs +5 -0
- package/views/partials/accounts_header.hbs +6 -0
- package/views/partials/add_account_modal.hbs +60 -0
- package/views/partials/address_list.hbs +37 -0
- package/views/partials/alerts.hbs +33 -0
- package/views/partials/document_store_header.hbs +52 -0
- package/views/partials/editor_scope_info.hbs +10 -0
- package/views/partials/gateway_form.hbs +65 -0
- package/views/partials/gateway_js.hbs +90 -0
- package/views/partials/gateways_header.hbs +6 -0
- package/views/partials/oauth_config_header.hbs +10 -0
- package/views/partials/oauth_form.hbs +1204 -0
- package/views/partials/scope_info.hbs +134 -0
- package/views/partials/security_header.hbs +11 -0
- package/views/partials/side_menu.hbs +114 -0
- package/views/partials/template_form.hbs +121 -0
- package/views/partials/templates_header.hbs +6 -0
- package/views/partials/test_send.hbs +327 -0
- package/views/partials/tokens_header.hbs +6 -0
- package/views/partials/webhook_form.hbs +151 -0
- package/views/partials/webhooks_editor_functions.hbs +372 -0
- package/views/partials/webhooks_header.hbs +6 -0
- package/views/redirect.hbs +1 -0
- package/views/swagger/index.hbs +76 -0
- package/views/templates/edit.hbs +87 -0
- package/views/templates/index.hbs +208 -0
- package/views/templates/new.hbs +85 -0
- package/views/templates/template.hbs +423 -0
- package/views/tokens/index.hbs +207 -0
- package/views/tokens/new.hbs +230 -0
- package/views/unsubscribe.hbs +93 -0
- package/views/upgrade.hbs +56 -0
- package/views/webhooks/edit.hbs +31 -0
- package/views/webhooks/index.hbs +144 -0
- package/views/webhooks/new.hbs +27 -0
- package/views/webhooks/webhook.hbs +265 -0
- package/winconf.js +93 -0
- package/workers/api.js +8246 -1256
- package/workers/documents.js +1120 -0
- package/workers/imap-proxy.js +91 -0
- package/workers/imap.js +552 -161
- package/workers/smtp.js +355 -82
- package/workers/submit.js +319 -54
- package/workers/webhooks.js +542 -80
- package/.eslintrc +0 -14
- package/.github/FUNDING.yml +0 -4
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/LICENSE.txt +0 -661
- package/examples/api.md +0 -137
- package/lib/connection.js +0 -1768
- package/lib/lua/z-push.lua +0 -14
- package/lib/mailbox.js +0 -1545
- package/license-report-config.json +0 -3
- package/licenses.txt +0 -37
- package/static/bootstrap-4.6.0-dist/css/bootstrap-grid.css.map +0 -1
- package/static/bootstrap-4.6.0-dist/css/bootstrap-reboot.css.map +0 -1
- package/static/bootstrap-4.6.0-dist/css/bootstrap-reboot.min.css.map +0 -1
- package/static/bootstrap-4.6.0-dist/css/bootstrap.css.map +0 -1
- package/static/bootstrap-4.6.0-dist/css/bootstrap.min.css +0 -7
- package/static/bootstrap-4.6.0-dist/css/bootstrap.min.css.map +0 -1
- package/static/bootstrap-4.6.0-dist/js/bootstrap.bundle.js +0 -7045
- package/static/bootstrap-4.6.0-dist/js/bootstrap.bundle.js.map +0 -1
- package/static/bootstrap-4.6.0-dist/js/bootstrap.bundle.min.js +0 -7
- package/static/bootstrap-4.6.0-dist/js/bootstrap.bundle.min.js.map +0 -1
- package/static/bootstrap-4.6.0-dist/js/bootstrap.js.map +0 -1
- package/static/bootstrap-4.6.0-dist/js/bootstrap.min.js +0 -7
- package/static/bootstrap-4.6.0-dist/js/bootstrap.min.js.map +0 -1
- package/static/js/emailengine.js +0 -581
- package/workers/arena.js +0 -89
|
@@ -0,0 +1,3721 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const {
|
|
5
|
+
serialize,
|
|
6
|
+
unserialize,
|
|
7
|
+
compareExisting,
|
|
8
|
+
normalizePath,
|
|
9
|
+
download,
|
|
10
|
+
filterEmptyObjectValues,
|
|
11
|
+
validUidValidity,
|
|
12
|
+
calculateFetchBackoff,
|
|
13
|
+
readEnvValue
|
|
14
|
+
} = require('../../tools');
|
|
15
|
+
const msgpack = require('msgpack5')();
|
|
16
|
+
const he = require('he');
|
|
17
|
+
const libmime = require('libmime');
|
|
18
|
+
const settings = require('../../settings');
|
|
19
|
+
const config = require('@zone-eu/wild-config');
|
|
20
|
+
const { bounceDetect } = require('../../bounce-detect');
|
|
21
|
+
const { arfDetect } = require('../../arf-detect');
|
|
22
|
+
const appendList = require('../../append-list');
|
|
23
|
+
const { mimeHtml } = require('@postalsys/email-text-tools');
|
|
24
|
+
const simpleParser = require('mailparser').simpleParser;
|
|
25
|
+
const ical = require('ical.js');
|
|
26
|
+
const addressparser = require('nodemailer/lib/addressparser');
|
|
27
|
+
const { llmPreProcess } = require('../../llm-pre-process');
|
|
28
|
+
|
|
29
|
+
const { getESClient } = require('../../document-store');
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
MESSAGE_NEW_NOTIFY,
|
|
33
|
+
MAILBOX_DELETED_NOTIFY,
|
|
34
|
+
MESSAGE_DELETED_NOTIFY,
|
|
35
|
+
MESSAGE_UPDATED_NOTIFY,
|
|
36
|
+
MESSAGE_MISSING_NOTIFY,
|
|
37
|
+
MAILBOX_RESET_NOTIFY,
|
|
38
|
+
MAILBOX_NEW_NOTIFY,
|
|
39
|
+
EMAIL_BOUNCE_NOTIFY,
|
|
40
|
+
EMAIL_COMPLAINT_NOTIFY,
|
|
41
|
+
REDIS_PREFIX,
|
|
42
|
+
MAX_INLINE_ATTACHMENT_SIZE,
|
|
43
|
+
MAX_ALLOWED_DOWNLOAD_SIZE,
|
|
44
|
+
DEFAULT_FETCH_BATCH_SIZE,
|
|
45
|
+
MAILBOX_HASH
|
|
46
|
+
} = require('../../consts');
|
|
47
|
+
|
|
48
|
+
// Configurable batch size for fetching messages (default: 250)
|
|
49
|
+
const FETCH_BATCH_SIZE = Number(readEnvValue('EENGINE_FETCH_BATCH_SIZE') || config.service.fetchBatchSize) || DEFAULT_FETCH_BATCH_SIZE;
|
|
50
|
+
|
|
51
|
+
// Do not check for flag updates using full sync more often than this value (30 minutes)
|
|
52
|
+
const FULL_SYNC_DELAY = 30 * 60 * 1000;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Calculates the next range of sequence numbers to fetch based on the last fetched range
|
|
56
|
+
* @param {Number} totalMessages - Total number of messages in the mailbox
|
|
57
|
+
* @param {String} lastRange - Last fetched range in format "start:end" or "start:*"
|
|
58
|
+
* @returns {String|false} Next range to fetch or false if no more messages
|
|
59
|
+
*/
|
|
60
|
+
function getFetchRange(totalMessages, lastRange) {
|
|
61
|
+
let lastEndMarker = lastRange ? lastRange.split(':').pop() : false;
|
|
62
|
+
if (lastEndMarker === '*') {
|
|
63
|
+
// Already fetched to the end
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
let lastSeq = lastRange ? Number(lastEndMarker) : 0;
|
|
67
|
+
let startSeq = lastSeq + 1;
|
|
68
|
+
if (startSeq > totalMessages) {
|
|
69
|
+
// No more messages to fetch
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
let endMarker = startSeq + FETCH_BATCH_SIZE - 1;
|
|
73
|
+
if (endMarker >= totalMessages) {
|
|
74
|
+
// Use * to fetch to the end
|
|
75
|
+
endMarker = '*';
|
|
76
|
+
}
|
|
77
|
+
return `${startSeq}:${endMarker}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Represents a single IMAP mailbox/folder and handles all operations on it
|
|
82
|
+
*/
|
|
83
|
+
class Mailbox {
|
|
84
|
+
constructor(connection, entry) {
|
|
85
|
+
this.status = false;
|
|
86
|
+
this.connection = connection; // Parent connection object
|
|
87
|
+
this.path = entry.path; // Mailbox path (e.g., "INBOX", "Sent Mail")
|
|
88
|
+
this.listingEntry = entry; // Mailbox metadata from LIST command
|
|
89
|
+
this.syncDisabled = entry.syncDisabled; // Whether syncing is disabled for this mailbox
|
|
90
|
+
|
|
91
|
+
// Child logger with mailbox context
|
|
92
|
+
this.logger = this.connection.mainLogger.child({
|
|
93
|
+
sub: 'mailbox',
|
|
94
|
+
path: this.path
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Indexing strategy: 'full' maintains complete message list, 'fast' only tracks new messages
|
|
98
|
+
this.imapIndexer = connection.imapIndexer;
|
|
99
|
+
|
|
100
|
+
// Gmail-specific flags
|
|
101
|
+
this.isGmail = connection.isGmail;
|
|
102
|
+
this.isAllMail = this.isGmail && this.listingEntry.specialUse === '\\All';
|
|
103
|
+
|
|
104
|
+
// LarkSuite mail has unreliable ENVELOPE responses, needs special handling
|
|
105
|
+
this.isLarkSuite = connection.isLarkSuite;
|
|
106
|
+
|
|
107
|
+
this.selected = false; // Whether this mailbox is currently selected
|
|
108
|
+
// Does the mailbox open happen before or after initial syncing
|
|
109
|
+
this.previouslyConnected = false;
|
|
110
|
+
|
|
111
|
+
// Generate unique Redis key for this mailbox based on path hash
|
|
112
|
+
this.redisKey = BigInt('0x' + crypto.createHash(MAILBOX_HASH).update(normalizePath(this.path)).digest('hex')).toString(36);
|
|
113
|
+
|
|
114
|
+
this.runPartialSyncTimer = false; // Timer for delayed partial sync after EXISTS
|
|
115
|
+
|
|
116
|
+
this.synced = false; // Whether initial sync is complete
|
|
117
|
+
this.syncing = false; // Whether currently syncing
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Gets current mailbox status from IMAP connection
|
|
122
|
+
* @param {Object} connectionClient - IMAP client to use (defaults to main connection)
|
|
123
|
+
* @returns {Object} Status object with path, highestModseq, uidValidity, uidNext, messages
|
|
124
|
+
*/
|
|
125
|
+
getMailboxStatus(connectionClient) {
|
|
126
|
+
connectionClient = connectionClient || this.connection.imapClient;
|
|
127
|
+
if (!connectionClient) {
|
|
128
|
+
throw new Error('IMAP connection not available');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let mailboxInfo = connectionClient.mailbox;
|
|
132
|
+
|
|
133
|
+
let status = {
|
|
134
|
+
path: this.path
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// MODSEQ for CONDSTORE extension (change tracking)
|
|
138
|
+
status.highestModseq = mailboxInfo.highestModseq ? mailboxInfo.highestModseq : false;
|
|
139
|
+
// UIDVALIDITY changes when mailbox is recreated
|
|
140
|
+
status.uidValidity = validUidValidity(mailboxInfo.uidValidity) ? mailboxInfo.uidValidity : false;
|
|
141
|
+
// Next UID to be assigned
|
|
142
|
+
status.uidNext = mailboxInfo.uidNext ? mailboxInfo.uidNext : false;
|
|
143
|
+
// Total message count
|
|
144
|
+
status.messages = mailboxInfo.exists ? mailboxInfo.exists : 0;
|
|
145
|
+
|
|
146
|
+
return status;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Loads last known mailbox state from Redis
|
|
151
|
+
* @returns {Object} mailbox state with stored metadata
|
|
152
|
+
*/
|
|
153
|
+
async getStoredStatus() {
|
|
154
|
+
let data = await this.connection.redis.hgetall(this.getMailboxKey());
|
|
155
|
+
data = data || {};
|
|
156
|
+
return {
|
|
157
|
+
path: data.path || this.path,
|
|
158
|
+
uidValidity: validUidValidity(data.uidValidity) ? BigInt(data.uidValidity) : false,
|
|
159
|
+
highestModseq: data.highestModseq && !isNaN(data.highestModseq) ? BigInt(data.highestModseq) : false,
|
|
160
|
+
messages: data.messages && !isNaN(data.messages) ? Number(data.messages) : false,
|
|
161
|
+
uidNext: data.uidNext && !isNaN(data.uidNext) ? Number(data.uidNext) : false,
|
|
162
|
+
// First UID when mailbox was initially synced (used to detect old messages)
|
|
163
|
+
initialUidNext: data.initialUidNext && !isNaN(data.initialUidNext) ? Number(data.initialUidNext) : false,
|
|
164
|
+
noInferiors: !!data.noInferiors,
|
|
165
|
+
lastFullSync: data.lastFullSync ? new Date(data.lastFullSync) : false
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Updates known mailbox state in Redis
|
|
171
|
+
* @param {Object} data - Status data to store
|
|
172
|
+
*/
|
|
173
|
+
async updateStoredStatus(data) {
|
|
174
|
+
if (!data || typeof data !== 'object') {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Convert all values to strings for Redis storage
|
|
179
|
+
let list = Object.keys(data)
|
|
180
|
+
.map(key => {
|
|
181
|
+
switch (key) {
|
|
182
|
+
case 'path':
|
|
183
|
+
case 'uidValidity':
|
|
184
|
+
case 'highestModseq':
|
|
185
|
+
case 'messages':
|
|
186
|
+
case 'uidNext':
|
|
187
|
+
return [key, data[key].toString()];
|
|
188
|
+
|
|
189
|
+
case 'lastFullSync':
|
|
190
|
+
return [key, data[key].toISOString()];
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
.filter(entry => entry);
|
|
194
|
+
|
|
195
|
+
if (!list.length) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let op = this.connection.redis.multi();
|
|
200
|
+
|
|
201
|
+
// Store initial UID on first sync
|
|
202
|
+
if (data.uidNext) {
|
|
203
|
+
op = op.hSetNew(this.getMailboxKey(), 'initialUidNext', data.uidNext.toString());
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await op.hmset(this.getMailboxKey(), Object.fromEntries(list)).exec();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Sets message entry object in Redis sorted set. Entries are ordered by `uid` property
|
|
211
|
+
* @param {Object} data - Message data with uid, flags, etc.
|
|
212
|
+
* @returns {Number} Sequence number for the added entry
|
|
213
|
+
*/
|
|
214
|
+
async entryListSet(data) {
|
|
215
|
+
if (isNaN(data.uid)) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Store in sorted set with UID as score for efficient range queries
|
|
220
|
+
return await this.connection.redis.zSet(this.getMessagesKey(), Number(data.uid), serialize(data));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Retrieves message entry object for the provided sequence value or UID
|
|
225
|
+
* @param {Number} seq - Sequence number or UID
|
|
226
|
+
* @param {Object} options - Options object
|
|
227
|
+
* @param {Boolean} options.uid - If true, seq is treated as UID
|
|
228
|
+
* @returns {Object|null} Message entry object with uid, entry, and seq
|
|
229
|
+
*/
|
|
230
|
+
async entryListGet(seq, options) {
|
|
231
|
+
let range = Number(seq);
|
|
232
|
+
options = options || {};
|
|
233
|
+
// Use UID-based or sequence-based retrieval
|
|
234
|
+
let command = options.uid ? 'zGetByUidBuffer' : 'zGetBuffer';
|
|
235
|
+
let response = await this.connection.redis[command](this.getMessagesKey(), range);
|
|
236
|
+
if (response) {
|
|
237
|
+
try {
|
|
238
|
+
return {
|
|
239
|
+
uid: Number(response[0]),
|
|
240
|
+
entry: unserialize(response[1]),
|
|
241
|
+
seq: Number(response[2])
|
|
242
|
+
};
|
|
243
|
+
} catch (err) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Deletes entry from message list for the provided sequence value or UID
|
|
252
|
+
* @param {Number} seq - Sequence number (0 if using UID)
|
|
253
|
+
* @param {Number} uid - UID number (0 if using sequence)
|
|
254
|
+
* @returns {Object|null} Message entry object that was deleted
|
|
255
|
+
*/
|
|
256
|
+
async entryListExpunge(seq, uid) {
|
|
257
|
+
// Custom Redis command that removes and returns the entry
|
|
258
|
+
let response = await this.connection.redis.zExpungeBuffer(this.getMessagesKey(), this.getMailboxKey(), seq || 0, uid || 0);
|
|
259
|
+
if (response) {
|
|
260
|
+
try {
|
|
261
|
+
return unserialize(response[1]);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Checks if this mailbox is currently selected in the IMAP connection
|
|
271
|
+
* @returns {Boolean} True if selected
|
|
272
|
+
*/
|
|
273
|
+
isSelected() {
|
|
274
|
+
return (
|
|
275
|
+
this.selected &&
|
|
276
|
+
this.connection.imapClient &&
|
|
277
|
+
this.connection.imapClient.mailbox &&
|
|
278
|
+
normalizePath(this.connection.imapClient.mailbox.path) === normalizePath(this.path)
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Redis key generators for different data types
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Gets Redis key for storing message list (sorted set)
|
|
286
|
+
*/
|
|
287
|
+
getMessagesKey() {
|
|
288
|
+
return `${REDIS_PREFIX}iam:${this.connection.account}:l:${this.redisKey}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Gets Redis key for storing mailbox metadata (hash)
|
|
293
|
+
*/
|
|
294
|
+
getMailboxKey() {
|
|
295
|
+
return `${REDIS_PREFIX}iam:${this.connection.account}:h:${this.redisKey}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Gets Redis key for storing bounce information
|
|
300
|
+
*/
|
|
301
|
+
getBounceKey() {
|
|
302
|
+
return `${REDIS_PREFIX}iar:b:${this.connection.account}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Gets Redis key for tracking seen messages (HyperLogLog)
|
|
307
|
+
*/
|
|
308
|
+
getSeenMessagesKey() {
|
|
309
|
+
return `${REDIS_PREFIX}iar:s:${this.connection.account}`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Gets Redis key for queued notifications (sorted set)
|
|
314
|
+
*/
|
|
315
|
+
getNotificationsKey() {
|
|
316
|
+
return `${REDIS_PREFIX}iam:${this.connection.account}:n:${this.redisKey}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Starts IDLE mode to receive real-time updates
|
|
321
|
+
*/
|
|
322
|
+
startIdle() {
|
|
323
|
+
if (!this.isSelected() || !this.connection.imapClient || this.connection.imapClient.idling) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
this.connection.imapClient.idle().catch(err => {
|
|
327
|
+
this.logger.error({ msg: 'IDLE error', err });
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Clears all mailbox records from Redis and notifies about deletion
|
|
333
|
+
* @param {Object} opts - Options
|
|
334
|
+
* @param {Boolean} opts.skipNotify - Skip sending deletion notification
|
|
335
|
+
*/
|
|
336
|
+
async clear(opts) {
|
|
337
|
+
opts = opts || {};
|
|
338
|
+
|
|
339
|
+
clearTimeout(this.runPartialSyncTimer);
|
|
340
|
+
|
|
341
|
+
// Delete all Redis keys for this mailbox
|
|
342
|
+
await this.connection.redis.del(this.getMailboxKey());
|
|
343
|
+
await this.connection.redis.del(this.getMessagesKey());
|
|
344
|
+
await this.connection.redis.del(this.getNotificationsKey());
|
|
345
|
+
|
|
346
|
+
// Remove from connection's mailbox cache
|
|
347
|
+
this.connection.mailboxes.delete(normalizePath(this.path));
|
|
348
|
+
|
|
349
|
+
this.logger.debug({ msg: 'Deleted mailbox', path: this.listingEntry.path });
|
|
350
|
+
|
|
351
|
+
if (!opts.skipNotify) {
|
|
352
|
+
this.connection.notify(this, MAILBOX_DELETED_NOTIFY, {
|
|
353
|
+
path: this.listingEntry.path,
|
|
354
|
+
name: this.listingEntry.name,
|
|
355
|
+
specialUse: this.listingEntry.specialUse || false
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Syncs mailbox without selecting it (using STATUS command)
|
|
362
|
+
* @param {Boolean} forceEmpty - Force sync even if mailbox appears unchanged
|
|
363
|
+
* @returns {Boolean} True if synced
|
|
364
|
+
*/
|
|
365
|
+
async sync(forceEmpty) {
|
|
366
|
+
if (this.selected || !this.connection.imapClient) {
|
|
367
|
+
// expect current folder to be already synced
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let status;
|
|
372
|
+
try {
|
|
373
|
+
// Get status without selecting the mailbox
|
|
374
|
+
status = await this.connection.imapClient.status(this.path, {
|
|
375
|
+
uidNext: true,
|
|
376
|
+
messages: true,
|
|
377
|
+
highestModseq: true,
|
|
378
|
+
uidValidity: true
|
|
379
|
+
});
|
|
380
|
+
} catch (err) {
|
|
381
|
+
if (err.code === 'NotFound') {
|
|
382
|
+
// folder is missing, refresh folder listing
|
|
383
|
+
await this.connection.getCurrentListing();
|
|
384
|
+
return;
|
|
385
|
+
} else {
|
|
386
|
+
throw err;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (!status) {
|
|
391
|
+
// nothing to do here
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
status.highestModseq = status.highestModseq || false;
|
|
396
|
+
|
|
397
|
+
if (this.syncDisabled) {
|
|
398
|
+
// only update counters, don't fetch messages
|
|
399
|
+
await this.updateStoredStatus(status);
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Check if we have unprocessed notifications that need to be sent
|
|
404
|
+
let hasQueuedNotifications = await this.connection.redis.exists(this.getNotificationsKey());
|
|
405
|
+
|
|
406
|
+
// Determine if we need to sync based on various conditions
|
|
407
|
+
if (!hasQueuedNotifications && !forceEmpty) {
|
|
408
|
+
let storedStatus = await this.getStoredStatus();
|
|
409
|
+
if (status.uidValidity === storedStatus.uidValidity) {
|
|
410
|
+
// Check if nothing has changed
|
|
411
|
+
if (
|
|
412
|
+
status.uidNext === storedStatus.uidNext &&
|
|
413
|
+
status.messages === storedStatus.messages &&
|
|
414
|
+
storedStatus.lastFullSync > new Date(Date.now() - FULL_SYNC_DELAY)
|
|
415
|
+
) {
|
|
416
|
+
// no reason to sync - no new messages and recent full sync
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Check MODSEQ for CONDSTORE-enabled servers
|
|
421
|
+
if ((!status.messages && !storedStatus.messages) || (status.highestModseq && status.highestModseq === storedStatus.highestModseq)) {
|
|
422
|
+
// no reason to sync - empty or no changes
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Need to sync - create promise that resolves when sync is complete
|
|
429
|
+
let syncedPromise = new Promise((resolve, reject) => {
|
|
430
|
+
this.synced = resolve;
|
|
431
|
+
this.select(true).catch(err => reject(err));
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
await syncedPromise;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Selects mailbox and optionally starts IDLE
|
|
439
|
+
* @param {Boolean} skipIdle - Don't start IDLE after selecting
|
|
440
|
+
*/
|
|
441
|
+
async select(skipIdle) {
|
|
442
|
+
const currentLock = this.connection.imapClient.currentLock;
|
|
443
|
+
// Avoid interfering with any active operations
|
|
444
|
+
if (currentLock) {
|
|
445
|
+
if (this.path === currentLock.path) {
|
|
446
|
+
// Already on the correct mailbox with an active lock
|
|
447
|
+
this.logger.trace({
|
|
448
|
+
msg: 'Skip extra lock on active mailbox',
|
|
449
|
+
activeLock: {
|
|
450
|
+
lockId: currentLock.lockId,
|
|
451
|
+
path: currentLock.path,
|
|
452
|
+
...(currentLock.options?.description && { description: currentLock.options?.description })
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
// Different mailbox is locked - queue this select to run after lock is released
|
|
458
|
+
// This ensures we don't interfere with ongoing operations
|
|
459
|
+
this.logger.trace({
|
|
460
|
+
msg: 'Queueing select - another mailbox is locked',
|
|
461
|
+
requestedPath: this.path,
|
|
462
|
+
lockedPath: currentLock.path,
|
|
463
|
+
activeLock: currentLock.lockId
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Get lock and wait our turn - this will queue if another operation is active
|
|
468
|
+
let lock = await this.getMailboxLock(null, { description: `Select mailbox: ${this.path}` });
|
|
469
|
+
|
|
470
|
+
// Check if we still need to select after getting the lock
|
|
471
|
+
// Another operation might have already selected this mailbox while we were waiting
|
|
472
|
+
if (this.connection.imapClient.mailbox && this.connection.imapClient.mailbox.path === this.path) {
|
|
473
|
+
this.logger.trace({
|
|
474
|
+
msg: 'Mailbox already selected after lock acquired',
|
|
475
|
+
path: this.path
|
|
476
|
+
});
|
|
477
|
+
lock.release();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Keep the lock briefly to ensure IDLE can start without interference
|
|
482
|
+
// Release after a short delay to allow IDLE to initialize
|
|
483
|
+
setTimeout(() => {
|
|
484
|
+
lock.release();
|
|
485
|
+
}, 100);
|
|
486
|
+
|
|
487
|
+
if (!skipIdle) {
|
|
488
|
+
// Do not wait until command finishes before proceeding
|
|
489
|
+
this.startIdle();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Acquires exclusive lock on mailbox to prevent concurrent operations
|
|
495
|
+
* @param {Object} connectionClient - IMAP client to use
|
|
496
|
+
* @param {Object} options - Lock options
|
|
497
|
+
* @returns {Object} Lock object with release() method
|
|
498
|
+
*/
|
|
499
|
+
async getMailboxLock(connectionClient, options) {
|
|
500
|
+
connectionClient = connectionClient || this.connection.imapClient;
|
|
501
|
+
|
|
502
|
+
if (!connectionClient) {
|
|
503
|
+
throw new Error('IMAP connection not available');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
let lock = await connectionClient.getMailboxLock(this.path, options || {});
|
|
507
|
+
|
|
508
|
+
// Reset idle timer when using main connection
|
|
509
|
+
if (connectionClient === this.connection.imapClient) {
|
|
510
|
+
clearTimeout(this.connection.completedTimer);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return lock;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Marks task as completed and potentially starts idle timer
|
|
518
|
+
* @param {Object} connectionClient - IMAP client that completed the task
|
|
519
|
+
*/
|
|
520
|
+
onTaskCompleted(connectionClient) {
|
|
521
|
+
connectionClient = connectionClient || this.connection.imapClient;
|
|
522
|
+
if (connectionClient === this.connection.imapClient) {
|
|
523
|
+
this.connection.onTaskCompleted();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Helper to log IMAP events with proper formatting
|
|
529
|
+
* @param {String} msg - Log message
|
|
530
|
+
* @param {Object} event - Event data to log
|
|
531
|
+
*/
|
|
532
|
+
logEvent(msg, event) {
|
|
533
|
+
const logObj = Object.assign({ msg }, event);
|
|
534
|
+
// Convert BigInts and Sets to loggable formats
|
|
535
|
+
Object.keys(logObj).forEach(key => {
|
|
536
|
+
if (typeof logObj[key] === 'bigint') {
|
|
537
|
+
logObj[key] = logObj[key].toString();
|
|
538
|
+
}
|
|
539
|
+
if (typeof logObj[key].has === 'function') {
|
|
540
|
+
logObj[key] = Array.from(logObj[key]);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
this.logger.trace(logObj);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Handles untagged EXISTS response indicating new messages
|
|
548
|
+
* @param {Object} event - EXISTS event data
|
|
549
|
+
*/
|
|
550
|
+
async onExists(event) {
|
|
551
|
+
this.logEvent('Untagged EXISTS', event);
|
|
552
|
+
|
|
553
|
+
// Debounce partial sync to avoid multiple syncs for rapid changes
|
|
554
|
+
clearTimeout(this.runPartialSyncTimer);
|
|
555
|
+
this.runPartialSyncTimer = setTimeout(() => {
|
|
556
|
+
this.runPartialSyncTimer = null;
|
|
557
|
+
this.shouldRunPartialSyncAfterExists()
|
|
558
|
+
.then(shouldRun => {
|
|
559
|
+
if (shouldRun) {
|
|
560
|
+
return this.partialSync();
|
|
561
|
+
}
|
|
562
|
+
return false;
|
|
563
|
+
})
|
|
564
|
+
.then(() => this.select())
|
|
565
|
+
.catch(err => this.logger.error({ msg: 'Sync error', err }));
|
|
566
|
+
}, 1000);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Handles untagged EXPUNGE/VANISHED response indicating deleted messages
|
|
571
|
+
* @param {Object} event - EXPUNGE event data
|
|
572
|
+
*/
|
|
573
|
+
async onExpunge(event) {
|
|
574
|
+
const imapIndexer = this.imapIndexer;
|
|
575
|
+
event.imapIndexer = imapIndexer;
|
|
576
|
+
this.logEvent('Untagged EXPUNGE', event);
|
|
577
|
+
|
|
578
|
+
if (imapIndexer !== 'full') {
|
|
579
|
+
// ignore as we can not compare this value against the index
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
let deletedEntry;
|
|
584
|
+
|
|
585
|
+
if (event.seq) {
|
|
586
|
+
// * 123 EXPUNGE - sequence-based expunge
|
|
587
|
+
deletedEntry = await this.entryListExpunge(event.seq);
|
|
588
|
+
} else if (event.uid) {
|
|
589
|
+
// * VANISHED 123 - UID-based expunge (QRESYNC)
|
|
590
|
+
deletedEntry = await this.entryListExpunge(false, event.uid);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (deletedEntry) {
|
|
594
|
+
await this.processDeleted(deletedEntry);
|
|
595
|
+
await this.markUpdated();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Handles untagged FETCH response indicating flag changes
|
|
601
|
+
* @param {Object} event - FETCH event data with flags
|
|
602
|
+
*/
|
|
603
|
+
async onFlags(event) {
|
|
604
|
+
const imapIndexer = this.imapIndexer;
|
|
605
|
+
event.imapIndexer = imapIndexer;
|
|
606
|
+
this.logEvent('Untagged FETCH', event);
|
|
607
|
+
|
|
608
|
+
if (imapIndexer !== 'full') {
|
|
609
|
+
// ignore as we can not compare this value against the index
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
let storedMessage = await this.entryListGet(event.uid || event.seq, { uid: !!event.uid });
|
|
614
|
+
let changes;
|
|
615
|
+
|
|
616
|
+
// ignore Recent flag as it's session-specific
|
|
617
|
+
event.flags.delete('\\Recent');
|
|
618
|
+
|
|
619
|
+
if (!storedMessage) {
|
|
620
|
+
// New! There should not be new messages in a flags update.
|
|
621
|
+
// What should we do? Currently triggering partial sync.
|
|
622
|
+
return await this.onExists();
|
|
623
|
+
} else if ((changes = compareExisting(storedMessage.entry, event, ['flags']))) {
|
|
624
|
+
// Update stored flags
|
|
625
|
+
let messageData = storedMessage.entry;
|
|
626
|
+
messageData.flags = event.flags;
|
|
627
|
+
let seq = await this.entryListSet(messageData);
|
|
628
|
+
|
|
629
|
+
if (seq) {
|
|
630
|
+
await this.processChanges(storedMessage, changes);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Checks if partial sync should run after EXISTS event
|
|
637
|
+
* @returns {Boolean} True if message count differs from stored count
|
|
638
|
+
*/
|
|
639
|
+
async shouldRunPartialSyncAfterExists() {
|
|
640
|
+
let storedStatus = await this.getStoredStatus();
|
|
641
|
+
let mailboxStatus = this.getMailboxStatus();
|
|
642
|
+
return mailboxStatus.messages !== storedStatus.messages;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Processes a deleted message - clears bounces and sends notification
|
|
647
|
+
* @param {Object} messageData - Deleted message data
|
|
648
|
+
*/
|
|
649
|
+
async processDeleted(messageData) {
|
|
650
|
+
this.logger.debug({ msg: 'Deleted', uid: messageData.uid });
|
|
651
|
+
|
|
652
|
+
//FIXME: does not work as there is no messageId property
|
|
653
|
+
/*
|
|
654
|
+
if (messageData.messageId) {
|
|
655
|
+
try {
|
|
656
|
+
let deleted = await appendList.clear(this.connection.redis, this.getBounceKey(), messageData.messageId);
|
|
657
|
+
if (deleted) {
|
|
658
|
+
this.logger.error({
|
|
659
|
+
msg: 'Cleared bounce log for message',
|
|
660
|
+
id: messageData.id,
|
|
661
|
+
uid: messageData.uid,
|
|
662
|
+
messageId: messageData.messageId
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
} catch (err) {
|
|
666
|
+
this.logger.error({
|
|
667
|
+
msg: 'Failed to clear bounce log',
|
|
668
|
+
id: messageData.id,
|
|
669
|
+
uid: messageData.uid,
|
|
670
|
+
messageId: messageData.messageId,
|
|
671
|
+
err
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
*/
|
|
676
|
+
|
|
677
|
+
// Generate packed UID for external reference
|
|
678
|
+
let packedUid = await this.connection.packUid(this, messageData.uid);
|
|
679
|
+
await this.connection.notify(this, MESSAGE_DELETED_NOTIFY, {
|
|
680
|
+
id: packedUid,
|
|
681
|
+
uid: messageData.uid
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
try {
|
|
685
|
+
// Remove from notifications queue since message no longer exists
|
|
686
|
+
await this.connection.redis.zremrangebyscore(this.getNotificationsKey(), messageData.uid, messageData.uid);
|
|
687
|
+
} catch (err) {
|
|
688
|
+
this.logger.error({ msg: 'Failed removing deleted message from notifications set', uid: messageData.uid, err });
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Processes a new message - fetches details, detects bounces/complaints, sends notifications
|
|
694
|
+
* @param {Object} messageData - Basic message data (uid, flags, etc)
|
|
695
|
+
* @param {Object} options - Processing options
|
|
696
|
+
* @param {Boolean} canSync - Whether message should be synced to document store
|
|
697
|
+
* @param {Object} storedStatus - Current mailbox status
|
|
698
|
+
*/
|
|
699
|
+
async processNew(messageData, options, canSync, storedStatus) {
|
|
700
|
+
this.logger.debug({ msg: 'New message', uid: messageData.uid, flags: Array.from(messageData.flags) });
|
|
701
|
+
|
|
702
|
+
options.skipLock = true;
|
|
703
|
+
|
|
704
|
+
// Handle header fetching options
|
|
705
|
+
let requestedHeaders = options.headers;
|
|
706
|
+
if (options.fetchHeaders) {
|
|
707
|
+
options.headers = options.fetchHeaders;
|
|
708
|
+
} else {
|
|
709
|
+
options.headers = 'headers' in options ? options.headers : false;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
let messageInfo;
|
|
713
|
+
|
|
714
|
+
// Retry logic for messages that might not be immediately available (replication lag)
|
|
715
|
+
let missingDelay = 0;
|
|
716
|
+
let missingRetries = 0;
|
|
717
|
+
let maxRetries = 3;
|
|
718
|
+
|
|
719
|
+
while (!messageInfo) {
|
|
720
|
+
messageInfo = await this.getMessage(messageData, options);
|
|
721
|
+
if (!messageInfo) {
|
|
722
|
+
// NB! could be a replication lag with specific servers, so retry a few times
|
|
723
|
+
if (missingRetries < maxRetries) {
|
|
724
|
+
// Exponential backoff: 1.7^n seconds
|
|
725
|
+
let delay = Math.round(1000 * Math.pow(1.7, missingRetries));
|
|
726
|
+
|
|
727
|
+
this.logger.debug({ msg: 'Missing message', status: 'not found', uid: messageData.uid, missingRetries, missingDelay, nextRetry: delay });
|
|
728
|
+
await new Promise(r => setTimeout(r, delay));
|
|
729
|
+
|
|
730
|
+
missingRetries++;
|
|
731
|
+
missingDelay += delay;
|
|
732
|
+
} else {
|
|
733
|
+
this.logger.debug({ msg: 'Missing message', status: 'not found', uid: messageData.uid, missingRetries, missingDelay, nextRetry: null });
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (!messageInfo) {
|
|
740
|
+
// Message not found after retries - send missing notification
|
|
741
|
+
let packedUid = await this.connection.packUid(this, messageData.uid);
|
|
742
|
+
await this.connection.notify(this, MESSAGE_MISSING_NOTIFY, {
|
|
743
|
+
id: packedUid,
|
|
744
|
+
uid: messageData.uid,
|
|
745
|
+
missingRetries,
|
|
746
|
+
missingDelay
|
|
747
|
+
});
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (missingRetries) {
|
|
752
|
+
// Log retry statistics if message was eventually found
|
|
753
|
+
messageInfo.missingDelay = missingDelay;
|
|
754
|
+
messageInfo.missingRetries = missingRetries;
|
|
755
|
+
|
|
756
|
+
this.logger.debug({ msg: 'Missing message', status: 'found', uid: messageData.uid, missingRetries, missingDelay });
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Filter headers based on what was requested
|
|
760
|
+
if (options.headers && Array.isArray(requestedHeaders)) {
|
|
761
|
+
let filteredHeaders = {};
|
|
762
|
+
for (let key of Object.keys(messageInfo.headers)) {
|
|
763
|
+
if (requestedHeaders.includes(key)) {
|
|
764
|
+
filteredHeaders[key] = messageInfo.headers[key];
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
messageInfo.headers = filteredHeaders;
|
|
768
|
+
} else if (options.headers && requestedHeaders === false) {
|
|
769
|
+
delete messageInfo.headers;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Check if message is too old to notify about
|
|
773
|
+
let date = new Date(messageInfo.date);
|
|
774
|
+
if (this.connection.notifyFrom && date < this.connection.notifyFrom && !canSync) {
|
|
775
|
+
// skip too old messages
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Skip old messages in non-INBOX folders on reconnect
|
|
780
|
+
if (this.previouslyConnected && this.path !== 'INBOX' && !this.isAllMail && storedStatus.initialUidNext > messageData.uid) {
|
|
781
|
+
this.logger.debug({
|
|
782
|
+
msg: 'Skip old message',
|
|
783
|
+
action: 'webhook_ignore',
|
|
784
|
+
initialUidNext: storedStatus.initialUidNext,
|
|
785
|
+
id: messageInfo.id,
|
|
786
|
+
uid: messageInfo.uid,
|
|
787
|
+
connectCount: this.previouslyConnected
|
|
788
|
+
});
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
let bounceNotifyInfo;
|
|
793
|
+
let complaintNotifyInfo;
|
|
794
|
+
let content;
|
|
795
|
+
|
|
796
|
+
// Check if this might be an ARF (Abuse Reporting Format) complaint
|
|
797
|
+
if (this.mightBeAComplaint(messageInfo)) {
|
|
798
|
+
try {
|
|
799
|
+
// Download relevant attachments for ARF parsing
|
|
800
|
+
for (let attachment of messageInfo.attachments) {
|
|
801
|
+
if (!['message/feedback-report', 'message/rfc822-headers', 'message/rfc822'].includes(attachment.contentType)) {
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
let buf = Buffer.from(attachment.id, 'base64url');
|
|
806
|
+
let part = buf.subarray(8).toString();
|
|
807
|
+
|
|
808
|
+
let { content: sourceStream } = await this.connection.imapClient.download(messageInfo.uid, part, {
|
|
809
|
+
uid: true,
|
|
810
|
+
// headers should fit into 1MB, don't need all contents
|
|
811
|
+
maxBytes: Math.min(1 * 1024 * 1024, MAX_ALLOWED_DOWNLOAD_SIZE),
|
|
812
|
+
chunkSize: options.chunkSize
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
if (sourceStream) {
|
|
816
|
+
Object.defineProperty(attachment, 'content', {
|
|
817
|
+
value: (await download(sourceStream)).toString(),
|
|
818
|
+
enumerable: false
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Parse ARF report
|
|
824
|
+
const report = await arfDetect(messageInfo);
|
|
825
|
+
|
|
826
|
+
if (report && report.arf && report.arf['original-rcpt-to'] && report.arf['original-rcpt-to'].length) {
|
|
827
|
+
// Valid complaint found - prepare notification data
|
|
828
|
+
let complaint = {};
|
|
829
|
+
for (let subKey of ['arf', 'headers']) {
|
|
830
|
+
for (let key of Object.keys(report[subKey])) {
|
|
831
|
+
if (!complaint[subKey]) {
|
|
832
|
+
complaint[subKey] = {};
|
|
833
|
+
}
|
|
834
|
+
// Convert kebab-case to camelCase
|
|
835
|
+
complaint[subKey][key.replace(/-(.)/g, (o, c) => c.toUpperCase())] = report[subKey][key];
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
complaintNotifyInfo = Object.assign({ complaintMessage: messageInfo.id }, complaint);
|
|
840
|
+
|
|
841
|
+
messageInfo.isComplaint = true;
|
|
842
|
+
|
|
843
|
+
if (complaint.headers && complaint.headers.messageId) {
|
|
844
|
+
messageInfo.relatedMessageId = complaint.headers.messageId;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
} catch (err) {
|
|
848
|
+
this.logger.error({
|
|
849
|
+
msg: 'Failed to process ARF',
|
|
850
|
+
id: messageInfo.id,
|
|
851
|
+
uid: messageInfo.uid,
|
|
852
|
+
messageId: messageInfo.messageId,
|
|
853
|
+
err
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Check if this might be a DSN (Delivery Status Notification)
|
|
859
|
+
if (this.mightBeDSNResponse(messageInfo)) {
|
|
860
|
+
try {
|
|
861
|
+
let { content: sourceStream } = await this.connection.imapClient.download(messageInfo.uid, false, {
|
|
862
|
+
uid: true,
|
|
863
|
+
chunkSize: options.chunkSize,
|
|
864
|
+
maxBytes: MAX_ALLOWED_DOWNLOAD_SIZE
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
let parsed = await simpleParser(sourceStream, { keepDeliveryStatus: true });
|
|
868
|
+
if (parsed) {
|
|
869
|
+
content = { parsed };
|
|
870
|
+
|
|
871
|
+
// Extract delivery status information
|
|
872
|
+
let deliveryStatus = parsed.attachments.find(attachment => attachment.contentType === 'message/delivery-status');
|
|
873
|
+
if (deliveryStatus) {
|
|
874
|
+
let deliveryEntries = libmime.decodeHeaders((deliveryStatus.content || '').toString().trim());
|
|
875
|
+
let structured = {};
|
|
876
|
+
|
|
877
|
+
// Parse delivery status headers
|
|
878
|
+
for (let key of Object.keys(deliveryEntries)) {
|
|
879
|
+
if (!key) {
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
let displayKey = key.replace(/-(.)/g, (m, c) => c.toUpperCase());
|
|
883
|
+
let value = deliveryEntries[key].at(-1);
|
|
884
|
+
if (typeof value === 'string') {
|
|
885
|
+
// Parse structured values like "rfc822;example.com"
|
|
886
|
+
let m = value.match(/^([^\s;]+);/);
|
|
887
|
+
if (m) {
|
|
888
|
+
value = {
|
|
889
|
+
label: m[1],
|
|
890
|
+
value: value.substring(m[0].length).trim()
|
|
891
|
+
};
|
|
892
|
+
} else {
|
|
893
|
+
switch (key) {
|
|
894
|
+
case 'arrival-date': {
|
|
895
|
+
value.trim();
|
|
896
|
+
let date = new Date(value);
|
|
897
|
+
if (date.toString() !== 'Invalid Date') {
|
|
898
|
+
value = date.toISOString();
|
|
899
|
+
}
|
|
900
|
+
structured[displayKey] = value;
|
|
901
|
+
break;
|
|
902
|
+
}
|
|
903
|
+
default:
|
|
904
|
+
structured[displayKey] = value.trim();
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
} else {
|
|
908
|
+
// ???
|
|
909
|
+
structured[displayKey] = value;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Only consider as delivery report if action indicates delivery or delay
|
|
914
|
+
if (/^delivered|^delayed/i.test(structured.action)) {
|
|
915
|
+
this.logger.debug({
|
|
916
|
+
msg: 'Detected delivery report',
|
|
917
|
+
id: messageInfo.id,
|
|
918
|
+
uid: messageInfo.uid,
|
|
919
|
+
messageId: messageInfo.messageId,
|
|
920
|
+
report: structured
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
messageInfo.deliveryReport = structured;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
} catch (err) {
|
|
928
|
+
this.logger.error({
|
|
929
|
+
msg: 'Failed to process DSN',
|
|
930
|
+
id: messageInfo.id,
|
|
931
|
+
uid: messageInfo.uid,
|
|
932
|
+
messageId: messageInfo.messageId,
|
|
933
|
+
err
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Check if this could be a bounce message
|
|
939
|
+
if (this.mightBeABounce(messageInfo)) {
|
|
940
|
+
// Parse for bounce information
|
|
941
|
+
try {
|
|
942
|
+
if (!content) {
|
|
943
|
+
let result = await this.connection.imapClient.download(messageInfo.uid, false, {
|
|
944
|
+
uid: true,
|
|
945
|
+
chunkSize: options.chunkSize,
|
|
946
|
+
maxBytes: MAX_ALLOWED_DOWNLOAD_SIZE
|
|
947
|
+
});
|
|
948
|
+
content = result.content;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if (content) {
|
|
952
|
+
// Detect bounce details
|
|
953
|
+
let bounce = await bounceDetect(content);
|
|
954
|
+
|
|
955
|
+
if (bounce?.response?.message) {
|
|
956
|
+
try {
|
|
957
|
+
let bounceType = await this.connection.call({
|
|
958
|
+
cmd: 'bounceClassify',
|
|
959
|
+
data: {
|
|
960
|
+
message: bounce?.response?.message
|
|
961
|
+
},
|
|
962
|
+
timeout: 2 * 60 * 1000
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
if (bounceType?.label) {
|
|
966
|
+
bounce.response.category = bounceType.label;
|
|
967
|
+
}
|
|
968
|
+
if (bounceType?.action) {
|
|
969
|
+
bounce.response.recommendedAction = bounceType.action;
|
|
970
|
+
}
|
|
971
|
+
if (bounceType?.blocklist) {
|
|
972
|
+
bounce.response.blocklist = bounceType.blocklist;
|
|
973
|
+
}
|
|
974
|
+
if (bounceType?.retryAfter) {
|
|
975
|
+
bounce.response.retryAfter = bounceType.retryAfter;
|
|
976
|
+
}
|
|
977
|
+
} catch (err) {
|
|
978
|
+
// ignore, just do not include this information
|
|
979
|
+
this.logger.error({
|
|
980
|
+
msg: 'Failed to classify bounce response',
|
|
981
|
+
bounceResponse: bounce?.response?.message,
|
|
982
|
+
err
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
let stored = 0;
|
|
988
|
+
if (bounce.action && bounce.recipient && bounce.messageId) {
|
|
989
|
+
// Store bounce information for later retrieval
|
|
990
|
+
let storedBounce = {
|
|
991
|
+
i: messageInfo.id,
|
|
992
|
+
r: bounce.recipient,
|
|
993
|
+
t: Date.now(),
|
|
994
|
+
a: bounce.action
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
if (bounce.response && bounce.response.message) {
|
|
998
|
+
storedBounce.m = bounce.response.message;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (bounce.response && bounce.response.status) {
|
|
1002
|
+
storedBounce.s = bounce.response.status;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Store bounce info associated with original message ID
|
|
1006
|
+
stored = await appendList.append(this.connection.redis, this.getBounceKey(), bounce.messageId, storedBounce);
|
|
1007
|
+
|
|
1008
|
+
bounceNotifyInfo = Object.assign({ bounceMessage: messageInfo.id }, bounce);
|
|
1009
|
+
|
|
1010
|
+
messageInfo.isBounce = true;
|
|
1011
|
+
messageInfo.relatedMessageId = bounce.messageId;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
this.logger.debug({
|
|
1015
|
+
msg: 'Detected bounce message',
|
|
1016
|
+
id: messageInfo.id,
|
|
1017
|
+
uid: messageInfo.uid,
|
|
1018
|
+
messageId: messageInfo.messageId,
|
|
1019
|
+
bounce,
|
|
1020
|
+
stored
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
} catch (err) {
|
|
1024
|
+
this.logger.error({
|
|
1025
|
+
msg: 'Failed to process potential bounce',
|
|
1026
|
+
id: messageInfo.id,
|
|
1027
|
+
uid: messageInfo.uid,
|
|
1028
|
+
messageId: messageInfo.messageId,
|
|
1029
|
+
err
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Resolve Gmail category for inbox messages
|
|
1035
|
+
if (
|
|
1036
|
+
this.connection.imapClient.capabilities.has('X-GM-EXT-1') &&
|
|
1037
|
+
this.isAllMail &&
|
|
1038
|
+
messageInfo.labels &&
|
|
1039
|
+
messageInfo.labels.includes('\\Inbox') &&
|
|
1040
|
+
(await settings.get('resolveGmailCategories'))
|
|
1041
|
+
) {
|
|
1042
|
+
this.logger.trace({
|
|
1043
|
+
msg: 'Resolving category for incoming email',
|
|
1044
|
+
uid: messageData.uid,
|
|
1045
|
+
id: messageInfo.id
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
// Try each Gmail category in order
|
|
1049
|
+
for (let category of ['primary', 'social', 'promotions', 'updates', 'forums', 'reservations', 'purchases']) {
|
|
1050
|
+
try {
|
|
1051
|
+
let results = await this.connection.imapClient.search(
|
|
1052
|
+
{
|
|
1053
|
+
uid: messageInfo.uid,
|
|
1054
|
+
gmraw: `category:${category}`
|
|
1055
|
+
},
|
|
1056
|
+
{ uid: true }
|
|
1057
|
+
);
|
|
1058
|
+
if (results && results.includes(messageInfo.uid)) {
|
|
1059
|
+
messageInfo.category = category;
|
|
1060
|
+
this.logger.debug({
|
|
1061
|
+
msg: 'Resolved category for incoming email',
|
|
1062
|
+
category,
|
|
1063
|
+
uid: messageData.uid,
|
|
1064
|
+
id: messageInfo.id
|
|
1065
|
+
});
|
|
1066
|
+
break;
|
|
1067
|
+
}
|
|
1068
|
+
} catch (err) {
|
|
1069
|
+
this.logger.error({ msg: 'Failed to resolve category for message', err, category, uid: messageData.uid, id: messageInfo.id });
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Download attachment content if configured
|
|
1076
|
+
let notifyAttachments = await settings.get('notifyAttachments');
|
|
1077
|
+
let notifyAttachmentSize = await settings.get('notifyAttachmentSize');
|
|
1078
|
+
if (notifyAttachments && messageInfo.attachments?.length) {
|
|
1079
|
+
for (let attachment of messageInfo.attachments || []) {
|
|
1080
|
+
if (notifyAttachmentSize && attachment.encodedSize && attachment.encodedSize > notifyAttachmentSize) {
|
|
1081
|
+
// skip large attachments
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
if (!attachment.content) {
|
|
1085
|
+
try {
|
|
1086
|
+
let buf = Buffer.from(attachment.id, 'base64url');
|
|
1087
|
+
let part = buf.subarray(8).toString();
|
|
1088
|
+
|
|
1089
|
+
let { content: downloadStream } = await this.connection.imapClient.download(messageInfo.uid, part, {
|
|
1090
|
+
uid: true,
|
|
1091
|
+
chunkSize: options.chunkSize,
|
|
1092
|
+
maxBytes: MAX_ALLOWED_DOWNLOAD_SIZE
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
if (downloadStream) {
|
|
1096
|
+
attachment.content = (await download(downloadStream)).toString('base64');
|
|
1097
|
+
}
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
this.logger.error({ msg: 'Failed to load attachment content', attachment, err });
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Fetch inline attachments referenced in HTML
|
|
1106
|
+
if (messageInfo.attachments?.length && messageInfo.text?.html) {
|
|
1107
|
+
// fetch inline attachments
|
|
1108
|
+
for (let attachment of messageInfo.attachments) {
|
|
1109
|
+
if (attachment.encodedSize && attachment.encodedSize > MAX_INLINE_ATTACHMENT_SIZE) {
|
|
1110
|
+
// skip large attachments
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Check if attachment is referenced by Content-ID in HTML
|
|
1115
|
+
if (!attachment.content && attachment.contentId && messageInfo.text.html.indexOf(`cid:${attachment.contentId.replace(/^<|>$/g, '')}`) >= 0) {
|
|
1116
|
+
try {
|
|
1117
|
+
let buf = Buffer.from(attachment.id, 'base64url');
|
|
1118
|
+
let part = buf.subarray(8).toString();
|
|
1119
|
+
|
|
1120
|
+
let { content: downloadStream } = await this.connection.imapClient.download(messageInfo.uid, part, {
|
|
1121
|
+
uid: true,
|
|
1122
|
+
chunkSize: options.chunkSize,
|
|
1123
|
+
maxBytes: MAX_ALLOWED_DOWNLOAD_SIZE
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
if (downloadStream) {
|
|
1127
|
+
attachment.content = (await download(downloadStream)).toString('base64');
|
|
1128
|
+
}
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
this.logger.error({ msg: 'Failed to load attachment content', attachment, err });
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// Fetch and process calendar events if needed
|
|
1137
|
+
let notifyCalendarEvents = await settings.get('notifyCalendarEvents');
|
|
1138
|
+
if (notifyCalendarEvents && messageInfo.attachments && messageInfo.attachments.length) {
|
|
1139
|
+
let calendarEventMap = new Map();
|
|
1140
|
+
|
|
1141
|
+
// Process text/calendar before application/ics
|
|
1142
|
+
let sortCalendarAttachments = (a, b) => {
|
|
1143
|
+
if (a.contentType !== b.contentType) {
|
|
1144
|
+
if (a.contentType === 'text/calendar') {
|
|
1145
|
+
return -1;
|
|
1146
|
+
}
|
|
1147
|
+
if (b.contentType === 'text/calendar') {
|
|
1148
|
+
return 1;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return a.contentType.localeCompare(b.contentType);
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
for (let attachment of [...messageInfo.attachments].sort(sortCalendarAttachments)) {
|
|
1155
|
+
if (['text/calendar', 'application/ics'].includes(attachment.contentType)) {
|
|
1156
|
+
// Download calendar attachment if not already loaded
|
|
1157
|
+
if (!attachment.content) {
|
|
1158
|
+
try {
|
|
1159
|
+
let buf = Buffer.from(attachment.id, 'base64url');
|
|
1160
|
+
let part = buf.subarray(8).toString();
|
|
1161
|
+
|
|
1162
|
+
let { content: downloadStream } = await this.connection.imapClient.download(messageInfo.uid, part, {
|
|
1163
|
+
uid: true,
|
|
1164
|
+
chunkSize: options.chunkSize,
|
|
1165
|
+
maxBytes: MAX_ALLOWED_DOWNLOAD_SIZE
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
if (downloadStream) {
|
|
1169
|
+
let contentBuf = await download(downloadStream);
|
|
1170
|
+
|
|
1171
|
+
if (contentBuf && contentBuf.length) {
|
|
1172
|
+
attachment.content = contentBuf.toString('base64');
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
} catch (err) {
|
|
1176
|
+
this.logger.error({ msg: 'Failed to load attachment content', attachment, err });
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (attachment.content) {
|
|
1180
|
+
let contentBuf = Buffer.from(attachment.content, 'base64');
|
|
1181
|
+
try {
|
|
1182
|
+
// Parse iCalendar data
|
|
1183
|
+
const jcalData = ical.parse(contentBuf.toString());
|
|
1184
|
+
|
|
1185
|
+
const comp = new ical.Component(jcalData);
|
|
1186
|
+
if (!comp) {
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const vevent = comp.getFirstSubcomponent('vevent');
|
|
1191
|
+
if (!vevent) {
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Extract method (REQUEST, CANCEL, etc.)
|
|
1196
|
+
let eventMethodProp = comp.getFirstProperty('method');
|
|
1197
|
+
let eventMethodValue = eventMethodProp ? eventMethodProp.getFirstValue() : null;
|
|
1198
|
+
|
|
1199
|
+
const event = new ical.Event(vevent);
|
|
1200
|
+
|
|
1201
|
+
if (!event || !event.uid) {
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Skip duplicate events, prefer ones with filenames
|
|
1206
|
+
if (calendarEventMap.has(event.uid)) {
|
|
1207
|
+
if (attachment.filename) {
|
|
1208
|
+
let existingEntry = calendarEventMap.get(event.uid);
|
|
1209
|
+
if (!existingEntry.filename) {
|
|
1210
|
+
// inject filename
|
|
1211
|
+
existingEntry.filename = attachment.filename;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Extract timezone information
|
|
1218
|
+
let timezone;
|
|
1219
|
+
const vtz = comp.getFirstSubcomponent('vtimezone');
|
|
1220
|
+
if (vtz) {
|
|
1221
|
+
const tz = new ical.Timezone(vtz);
|
|
1222
|
+
timezone = tz && tz.tzid;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
let startDate = event.startDate && event.startDate.toJSDate();
|
|
1226
|
+
let endDate = event.endDate && event.endDate.toJSDate();
|
|
1227
|
+
|
|
1228
|
+
// Store parsed calendar event
|
|
1229
|
+
calendarEventMap.set(
|
|
1230
|
+
event.uid,
|
|
1231
|
+
filterEmptyObjectValues({
|
|
1232
|
+
eventId: event.uid,
|
|
1233
|
+
attachment: attachment.id,
|
|
1234
|
+
method: attachment.method || eventMethodValue || null,
|
|
1235
|
+
|
|
1236
|
+
summary: event.summary || null,
|
|
1237
|
+
description: event.description || null,
|
|
1238
|
+
timezone: timezone || null,
|
|
1239
|
+
startDate: startDate ? startDate.toISOString() : null,
|
|
1240
|
+
endDate: endDate ? endDate.toISOString() : null,
|
|
1241
|
+
organizer: event.organizer && typeof event.organizer === 'string' ? event.organizer : null,
|
|
1242
|
+
|
|
1243
|
+
filename: attachment.filename,
|
|
1244
|
+
contentType: attachment.contentType,
|
|
1245
|
+
encoding: 'base64',
|
|
1246
|
+
content: attachment.content
|
|
1247
|
+
})
|
|
1248
|
+
);
|
|
1249
|
+
} catch (err) {
|
|
1250
|
+
this.logger.error({
|
|
1251
|
+
msg: 'Failed to parse calendar event',
|
|
1252
|
+
attachment: Object.assign({}, attachment, { content: `${contentBuf.length} bytes` }),
|
|
1253
|
+
err
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (calendarEventMap && calendarEventMap.size) {
|
|
1261
|
+
messageInfo.calendarEvents = Array.from(calendarEventMap.values()).map(calendarEvent => {
|
|
1262
|
+
// Generate default filename based on method
|
|
1263
|
+
if (!calendarEvent.filename) {
|
|
1264
|
+
switch (calendarEvent.method && calendarEvent.method.toUpperCase()) {
|
|
1265
|
+
case 'CANCEL':
|
|
1266
|
+
case 'REQUEST':
|
|
1267
|
+
calendarEvent.filename = 'invite.ics';
|
|
1268
|
+
break;
|
|
1269
|
+
default:
|
|
1270
|
+
calendarEvent.filename = 'event.ics';
|
|
1271
|
+
break;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return calendarEvent;
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// Check if we have seen this message before using HyperLogLog
|
|
1280
|
+
messageInfo.seemsLikeNew =
|
|
1281
|
+
this.listingEntry.specialUse !== '\\Sent' &&
|
|
1282
|
+
!(messageInfo.labels && messageInfo.labels.includes('\\Sent')) &&
|
|
1283
|
+
!!(await this.connection.redis.pfadd(this.getSeenMessagesKey(), messageInfo.emailId || messageInfo.messageId));
|
|
1284
|
+
|
|
1285
|
+
// Determine special use folder for the message
|
|
1286
|
+
for (let specialUseTag of ['\\Junk', '\\Sent', '\\Trash', '\\Inbox', '\\Drafts']) {
|
|
1287
|
+
if (this.listingEntry.specialUse === specialUseTag || (messageInfo.labels && messageInfo.labels.includes(specialUseTag))) {
|
|
1288
|
+
messageInfo.messageSpecialUse = specialUseTag;
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Process with LLM if configured for inbox messages
|
|
1294
|
+
if (messageInfo.messageSpecialUse === '\\Inbox' && (!this.connection.notifyFrom || messageData.internalDate >= this.connection.notifyFrom)) {
|
|
1295
|
+
let messageData = Object.assign({ account: this.connection.account }, messageInfo);
|
|
1296
|
+
|
|
1297
|
+
let canUseLLM = await llmPreProcess.run(messageData);
|
|
1298
|
+
|
|
1299
|
+
if (canUseLLM && (messageInfo.text.plain || messageInfo.text.html)) {
|
|
1300
|
+
// Generate AI summary if enabled
|
|
1301
|
+
if (canUseLLM.generateEmailSummary) {
|
|
1302
|
+
try {
|
|
1303
|
+
messageInfo.summary = await this.connection.call({
|
|
1304
|
+
cmd: 'generateSummary',
|
|
1305
|
+
data: {
|
|
1306
|
+
message: {
|
|
1307
|
+
headers: Object.keys(messageInfo.headers || {}).map(key => ({ key, value: [].concat(messageInfo.headers[key] || []) })),
|
|
1308
|
+
attachments: messageInfo.attachments,
|
|
1309
|
+
from: messageInfo.from,
|
|
1310
|
+
subject: messageInfo.subject,
|
|
1311
|
+
text: messageInfo.text.plain,
|
|
1312
|
+
html: messageInfo.text.html
|
|
1313
|
+
},
|
|
1314
|
+
account: this.connection.account
|
|
1315
|
+
},
|
|
1316
|
+
timeout: 2 * 60 * 1000
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
if (messageInfo.summary) {
|
|
1320
|
+
// Clean up summary output
|
|
1321
|
+
for (let key of Object.keys(messageInfo.summary)) {
|
|
1322
|
+
// remove meta keys from output
|
|
1323
|
+
if (key.charAt(0) === '_' || messageInfo.summary[key] === '') {
|
|
1324
|
+
delete messageInfo.summary[key];
|
|
1325
|
+
}
|
|
1326
|
+
if (key === 'riskAssessment') {
|
|
1327
|
+
messageInfo.riskAssessment = messageInfo.summary.riskAssessment;
|
|
1328
|
+
delete messageInfo.summary.riskAssessment;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
this.logger.trace({ msg: 'Fetched summary from OpenAI', summary: messageInfo.summary });
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
await this.connection.redis.del(`${REDIS_PREFIX}:openai:error`);
|
|
1336
|
+
} catch (err) {
|
|
1337
|
+
await this.connection.redis.set(
|
|
1338
|
+
`${REDIS_PREFIX}:openai:error`,
|
|
1339
|
+
JSON.stringify({
|
|
1340
|
+
message: err.message,
|
|
1341
|
+
code: err.code,
|
|
1342
|
+
statusCode: err.statusCode,
|
|
1343
|
+
created: Date.now()
|
|
1344
|
+
})
|
|
1345
|
+
);
|
|
1346
|
+
this.logger.error({ msg: 'Failed to fetch summary from OpenAI', err });
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// Generate embeddings if enabled
|
|
1351
|
+
if (canUseLLM.generateEmbeddings) {
|
|
1352
|
+
try {
|
|
1353
|
+
messageInfo.embeddings = await this.connection.call({
|
|
1354
|
+
cmd: 'generateEmbeddings',
|
|
1355
|
+
data: {
|
|
1356
|
+
message: {
|
|
1357
|
+
headers: Object.keys(messageInfo.headers || {}).map(key => ({ key, value: [].concat(messageInfo.headers[key] || []) })),
|
|
1358
|
+
attachments: messageInfo.attachments,
|
|
1359
|
+
from: messageInfo.from,
|
|
1360
|
+
subject: messageInfo.subject,
|
|
1361
|
+
text: messageInfo.text.plain,
|
|
1362
|
+
html: messageInfo.text.html
|
|
1363
|
+
},
|
|
1364
|
+
account: this.connection.account
|
|
1365
|
+
},
|
|
1366
|
+
timeout: 2 * 60 * 1000
|
|
1367
|
+
});
|
|
1368
|
+
} catch (err) {
|
|
1369
|
+
await this.connection.redis.set(
|
|
1370
|
+
`${REDIS_PREFIX}:openai:error`,
|
|
1371
|
+
JSON.stringify({
|
|
1372
|
+
message: err.message,
|
|
1373
|
+
code: err.code,
|
|
1374
|
+
statusCode: err.statusCode,
|
|
1375
|
+
time: Date.now()
|
|
1376
|
+
})
|
|
1377
|
+
);
|
|
1378
|
+
this.logger.error({ msg: 'Failed to fetch embeddings OpenAI', err });
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// Convert message HTML to web safe HTML
|
|
1385
|
+
let notifyWebSafeHtml = await settings.get('notifyWebSafeHtml');
|
|
1386
|
+
if (notifyWebSafeHtml && messageInfo.text && (messageInfo.text.html || messageInfo.text.plain)) {
|
|
1387
|
+
// convert to web safe
|
|
1388
|
+
messageInfo.text._generatedHtml = mimeHtml({
|
|
1389
|
+
html: messageInfo.text.html,
|
|
1390
|
+
text: messageInfo.text.plain
|
|
1391
|
+
});
|
|
1392
|
+
messageInfo.text.webSafe = true;
|
|
1393
|
+
|
|
1394
|
+
// Embed images referenced by Content-ID
|
|
1395
|
+
if (messageInfo.text.html && messageInfo.attachments) {
|
|
1396
|
+
let attachmentList = new Map();
|
|
1397
|
+
let partList = [];
|
|
1398
|
+
|
|
1399
|
+
// Collect CID-referenced attachments
|
|
1400
|
+
for (let attachment of messageInfo.attachments) {
|
|
1401
|
+
let contentId = attachment.contentId && attachment.contentId.replace(/^<|>$/g, '');
|
|
1402
|
+
if (contentId && messageInfo.text.html.indexOf(contentId) >= 0) {
|
|
1403
|
+
attachmentList.set(contentId, {
|
|
1404
|
+
attachment,
|
|
1405
|
+
content: attachment.content || null
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
if (attachment.content) {
|
|
1409
|
+
// already downloaded in a previous step
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
let buf = Buffer.from(attachment.id, 'base64url');
|
|
1414
|
+
let part = buf.subarray(8).toString();
|
|
1415
|
+
|
|
1416
|
+
Object.defineProperty(attachment, 'part', {
|
|
1417
|
+
value: part,
|
|
1418
|
+
enumerable: false
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
if (!partList.includes(part)) {
|
|
1422
|
+
partList.push(part);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
if (attachmentList.size) {
|
|
1428
|
+
if (partList.length) {
|
|
1429
|
+
// Download missing attachments in batch
|
|
1430
|
+
try {
|
|
1431
|
+
let contentParts = await this.connection.imapClient.downloadMany(messageInfo.uid, partList, {
|
|
1432
|
+
uid: true
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
if (contentParts) {
|
|
1436
|
+
for (let [contentId, { attachment }] of attachmentList) {
|
|
1437
|
+
if (attachment.part && contentParts[attachment.part] && contentParts[attachment.part].content) {
|
|
1438
|
+
attachmentList.set(contentId, { attachment, content: contentParts[attachment.part].content.toString('base64') });
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
} catch (err) {
|
|
1443
|
+
this.logger.error({ msg: 'Attachment error', uid: messageInfo.uid, partList, err });
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// Replace CID references with data URIs
|
|
1448
|
+
messageInfo.text.html = messageInfo.text.html.replace(/\bcid:([^"'\s>]+)/g, (fullMatch, cidMatch) => {
|
|
1449
|
+
if (attachmentList.has(cidMatch)) {
|
|
1450
|
+
let { attachment, content } = attachmentList.get(cidMatch);
|
|
1451
|
+
if (content) {
|
|
1452
|
+
return `data:${attachment.contentType || 'application/octet-stream'};base64,${content}`;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return fullMatch;
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Send new message notification
|
|
1462
|
+
await this.connection.notify(this, MESSAGE_NEW_NOTIFY, messageInfo, {
|
|
1463
|
+
skipWebhook: this.connection.notifyFrom && date < this.connection.notifyFrom,
|
|
1464
|
+
canSync
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
// Send bounce notification if detected
|
|
1468
|
+
if (bounceNotifyInfo) {
|
|
1469
|
+
let { index, client } = await getESClient(this.logger);
|
|
1470
|
+
if (client) {
|
|
1471
|
+
// Find the originating message this bounce applies for
|
|
1472
|
+
let searchResult = await client.search({
|
|
1473
|
+
index,
|
|
1474
|
+
size: 20,
|
|
1475
|
+
from: 0,
|
|
1476
|
+
query: {
|
|
1477
|
+
bool: {
|
|
1478
|
+
must: [
|
|
1479
|
+
{
|
|
1480
|
+
term: {
|
|
1481
|
+
account: this.connection.account
|
|
1482
|
+
}
|
|
1483
|
+
},
|
|
1484
|
+
{
|
|
1485
|
+
term: {
|
|
1486
|
+
messageId: bounceNotifyInfo.messageId
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
]
|
|
1490
|
+
}
|
|
1491
|
+
},
|
|
1492
|
+
sort: { uid: 'desc' },
|
|
1493
|
+
_source_excludes: 'headers,text'
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
if (searchResult && searchResult.hits && searchResult.hits.hits && searchResult.hits.hits.length) {
|
|
1497
|
+
// Prefer sent messages, then earliest message
|
|
1498
|
+
let message = searchResult.hits.hits
|
|
1499
|
+
.sort((a, b) => {
|
|
1500
|
+
if (a._source.specialUse === '\\Sent') {
|
|
1501
|
+
return -1;
|
|
1502
|
+
}
|
|
1503
|
+
if (b._source.specialUse === '\\Sent') {
|
|
1504
|
+
return 1;
|
|
1505
|
+
}
|
|
1506
|
+
return new Date(a._source.date || a._source.created) - new Date(b._source.date || b._source.created);
|
|
1507
|
+
})
|
|
1508
|
+
.shift()._source;
|
|
1509
|
+
bounceNotifyInfo = Object.assign({ id: message.id }, bounceNotifyInfo);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// send bounce notification _after_ bounce email notification
|
|
1514
|
+
await this.connection.notify(false, EMAIL_BOUNCE_NOTIFY, bounceNotifyInfo, {
|
|
1515
|
+
skipWebhook: this.connection.notifyFrom && date < this.connection.notifyFrom
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// Send complaint notification if detected
|
|
1520
|
+
if (complaintNotifyInfo) {
|
|
1521
|
+
// send complaint notification _after_ complaint email notification
|
|
1522
|
+
await this.connection.notify(false, EMAIL_COMPLAINT_NOTIFY, complaintNotifyInfo, {
|
|
1523
|
+
skipWebhook: this.connection.notifyFrom && date < this.connection.notifyFrom
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
/**
|
|
1529
|
+
* Builds message info object from raw IMAP data
|
|
1530
|
+
* @param {Object} messageData - Raw message data from IMAP
|
|
1531
|
+
* @param {Boolean} extended - Include extended information
|
|
1532
|
+
* @returns {Object} Formatted message info
|
|
1533
|
+
*/
|
|
1534
|
+
async getMessageInfo(messageData, extended) {
|
|
1535
|
+
if (!messageData) {
|
|
1536
|
+
return false;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// Generate unique message ID
|
|
1540
|
+
let packedUid = await this.connection.packUid(this, messageData.uid);
|
|
1541
|
+
|
|
1542
|
+
if (!packedUid) {
|
|
1543
|
+
let storedStatus;
|
|
1544
|
+
try {
|
|
1545
|
+
storedStatus = await this.getStoredStatus();
|
|
1546
|
+
} catch (err) {
|
|
1547
|
+
storedStatus = { err };
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
this.logger.error({
|
|
1551
|
+
msg: 'Failed to generate message ID',
|
|
1552
|
+
uid: messageData.uid,
|
|
1553
|
+
messageId: messageData.messageId,
|
|
1554
|
+
mailbox: this.path,
|
|
1555
|
+
storedStatus
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
throw new Error(
|
|
1559
|
+
`Failed to generate message ID (uid=${messageData.uid};uv=${storedStatus.uidValidity};path=${this.path};n=${
|
|
1560
|
+
storedStatus.err ? 'err' : storedStatus.uidNext
|
|
1561
|
+
})`
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// Extract attachment information from body structure
|
|
1566
|
+
let { attachments, textId, encodedTextSize } = this.getAttachmentList(packedUid, messageData.bodyStructure);
|
|
1567
|
+
|
|
1568
|
+
let envelope = messageData.envelope || {};
|
|
1569
|
+
|
|
1570
|
+
// Use envelope date or fall back to internal date
|
|
1571
|
+
let date =
|
|
1572
|
+
envelope.date && typeof envelope.date.toISOString === 'function' && envelope.date.toString() !== 'Invalid Date'
|
|
1573
|
+
? envelope.date
|
|
1574
|
+
: messageData.internalDate;
|
|
1575
|
+
|
|
1576
|
+
let isDraft = false;
|
|
1577
|
+
if (messageData.flags && messageData.flags.has('\\Draft')) {
|
|
1578
|
+
isDraft = true;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// Do not expose the \Recent flag as it is session specific
|
|
1582
|
+
if (messageData.flags && messageData.flags.has('\\Recent')) {
|
|
1583
|
+
messageData.flags.delete('\\Recent');
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
if (messageData.labels && messageData.labels.has('\\Draft')) {
|
|
1587
|
+
isDraft = true;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
let headers;
|
|
1591
|
+
|
|
1592
|
+
// This section is needed for Lark Mail as some address fields might be missing
|
|
1593
|
+
// from the ENVELOPE section, so fall back to the header instead.
|
|
1594
|
+
// Normally, these headers are not fetched from the server and only ENVELOPE is used
|
|
1595
|
+
let parsedAddresses = {};
|
|
1596
|
+
if (messageData.headers) {
|
|
1597
|
+
headers = libmime.decodeHeaders(messageData.headers.toString().trim());
|
|
1598
|
+
for (let key of ['from', 'to', 'cc', 'bcc']) {
|
|
1599
|
+
if (headers[key]?.length) {
|
|
1600
|
+
try {
|
|
1601
|
+
const addressList = addressparser(headers[key])
|
|
1602
|
+
.filter(address => !!address.address)
|
|
1603
|
+
.map(address => {
|
|
1604
|
+
let name = address.name;
|
|
1605
|
+
try {
|
|
1606
|
+
name = libmime.decodeWords(name);
|
|
1607
|
+
} catch (err) {
|
|
1608
|
+
// ignore
|
|
1609
|
+
}
|
|
1610
|
+
return {
|
|
1611
|
+
name,
|
|
1612
|
+
address: address.address
|
|
1613
|
+
};
|
|
1614
|
+
});
|
|
1615
|
+
if (addressList?.length) {
|
|
1616
|
+
parsedAddresses[key] = addressList;
|
|
1617
|
+
}
|
|
1618
|
+
} catch (err) {
|
|
1619
|
+
// just ignore
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// Build message info object
|
|
1626
|
+
const result = {
|
|
1627
|
+
id: packedUid,
|
|
1628
|
+
uid: messageData.uid,
|
|
1629
|
+
|
|
1630
|
+
path: (extended && this.path && normalizePath(this.path)) || undefined,
|
|
1631
|
+
|
|
1632
|
+
emailId: messageData.emailId || undefined,
|
|
1633
|
+
threadId: messageData.threadId || undefined,
|
|
1634
|
+
|
|
1635
|
+
date: (date && typeof date.toISOString === 'function' && date.toISOString()) || undefined,
|
|
1636
|
+
|
|
1637
|
+
flags: messageData.flags ? Array.from(messageData.flags) : undefined,
|
|
1638
|
+
labels: messageData.labels ? Array.from(messageData.labels) : undefined,
|
|
1639
|
+
|
|
1640
|
+
unseen: messageData.flags && !messageData.flags.has('\\Seen') ? true : undefined,
|
|
1641
|
+
flagged: messageData.flags && messageData.flags.has('\\Flagged') ? true : undefined,
|
|
1642
|
+
answered: messageData.flags && messageData.flags.has('\\Answered') ? true : undefined,
|
|
1643
|
+
|
|
1644
|
+
draft: isDraft ? true : undefined,
|
|
1645
|
+
|
|
1646
|
+
size: messageData.size || undefined,
|
|
1647
|
+
subject: envelope.subject || undefined,
|
|
1648
|
+
// Prefer envelope from, fall back to parsed header
|
|
1649
|
+
from: envelope.from?.[0] ? envelope.from[0] : parsedAddresses.from?.[0] || undefined,
|
|
1650
|
+
|
|
1651
|
+
replyTo: envelope.replyTo && envelope.replyTo.length ? envelope.replyTo : undefined,
|
|
1652
|
+
sender: extended && envelope.sender && envelope.sender[0] ? envelope.sender[0] : undefined,
|
|
1653
|
+
|
|
1654
|
+
to: envelope.to?.length ? envelope.to : parsedAddresses.to || undefined,
|
|
1655
|
+
cc: envelope.cc?.length ? envelope.cc : parsedAddresses.cc || undefined,
|
|
1656
|
+
|
|
1657
|
+
bcc: extended && envelope.bcc && envelope.bcc.length ? envelope.bcc : undefined,
|
|
1658
|
+
|
|
1659
|
+
attachments: attachments && attachments.length ? attachments : undefined,
|
|
1660
|
+
messageId: (envelope.messageId && envelope.messageId.toString().trim()) || undefined,
|
|
1661
|
+
inReplyTo: envelope.inReplyTo || undefined,
|
|
1662
|
+
|
|
1663
|
+
headers: (extended && headers) || undefined,
|
|
1664
|
+
text: textId
|
|
1665
|
+
? {
|
|
1666
|
+
id: textId,
|
|
1667
|
+
encodedSize: encodedTextSize
|
|
1668
|
+
}
|
|
1669
|
+
: undefined
|
|
1670
|
+
};
|
|
1671
|
+
|
|
1672
|
+
// Remove undefined properties
|
|
1673
|
+
Object.keys(result).forEach(key => {
|
|
1674
|
+
if (typeof result[key] === 'undefined') {
|
|
1675
|
+
delete result[key];
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
// Check if message is an auto-reply
|
|
1680
|
+
if (result.headers && this.connection.isAutoreply(result)) {
|
|
1681
|
+
result.isAutoReply = true;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// Fetch associated bounce information
|
|
1685
|
+
try {
|
|
1686
|
+
if (result.messageId) {
|
|
1687
|
+
let bounces = await appendList.list(this.connection.redis, this.getBounceKey(), result.messageId);
|
|
1688
|
+
if (bounces && bounces.length) {
|
|
1689
|
+
result.bounces = bounces.map(row => {
|
|
1690
|
+
let bounce = {
|
|
1691
|
+
message: row.i,
|
|
1692
|
+
recipient: row.r,
|
|
1693
|
+
action: row.a
|
|
1694
|
+
};
|
|
1695
|
+
if (row.m || row.s) {
|
|
1696
|
+
bounce.response = {};
|
|
1697
|
+
}
|
|
1698
|
+
if (row.m) {
|
|
1699
|
+
bounce.response.message = row.m;
|
|
1700
|
+
}
|
|
1701
|
+
if (row.s) {
|
|
1702
|
+
bounce.response.status = row.s;
|
|
1703
|
+
}
|
|
1704
|
+
bounce.date = new Date(row.t).toISOString();
|
|
1705
|
+
return bounce;
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
} catch (E) {
|
|
1710
|
+
this.logger.error({
|
|
1711
|
+
msg: 'Failed to fetch bounces',
|
|
1712
|
+
id: messageData.id,
|
|
1713
|
+
uid: messageData.uid,
|
|
1714
|
+
messageId: messageData.messageId,
|
|
1715
|
+
err: E
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
return result;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* Parses body structure to extract attachment and text part information
|
|
1724
|
+
* @param {String} packedUid - Packed UID for generating attachment IDs
|
|
1725
|
+
* @param {Object} bodyStructure - IMAP BODYSTRUCTURE
|
|
1726
|
+
* @returns {Object} Attachments list and text part information
|
|
1727
|
+
*/
|
|
1728
|
+
getAttachmentList(packedUid, bodyStructure) {
|
|
1729
|
+
let attachments = [];
|
|
1730
|
+
let textParts = [[], [], []]; // [plain, html, other]
|
|
1731
|
+
if (!bodyStructure) {
|
|
1732
|
+
return attachments;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
let idBuf = Buffer.from(packedUid, 'base64url');
|
|
1736
|
+
|
|
1737
|
+
let encodedTextSize = {};
|
|
1738
|
+
|
|
1739
|
+
// Recursively walk body structure tree
|
|
1740
|
+
let walk = (node, isRelated) => {
|
|
1741
|
+
if (node.type === 'multipart/related') {
|
|
1742
|
+
isRelated = true;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
if (!/^multipart\//.test(node.type)) {
|
|
1746
|
+
// Leaf node - either attachment or text
|
|
1747
|
+
if (node.disposition === 'attachment' || !/^text\/(plain|html)/.test(node.type)) {
|
|
1748
|
+
// Attachment
|
|
1749
|
+
let attachment = {
|
|
1750
|
+
// Append body part number to message ID
|
|
1751
|
+
id: Buffer.concat([idBuf, Buffer.from(node.part || '1')]).toString('base64url'),
|
|
1752
|
+
contentType: node.type,
|
|
1753
|
+
encodedSize: node.size,
|
|
1754
|
+
|
|
1755
|
+
embedded: isRelated,
|
|
1756
|
+
inline: node.disposition === 'inline' || (!node.disposition && isRelated)
|
|
1757
|
+
};
|
|
1758
|
+
|
|
1759
|
+
// Extract filename from disposition or content-type parameters
|
|
1760
|
+
let filename = (node.dispositionParameters && node.dispositionParameters.filename) || (node.parameters && node.parameters.name) || false;
|
|
1761
|
+
if (filename) {
|
|
1762
|
+
attachment.filename = filename;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
if (node.id) {
|
|
1766
|
+
attachment.contentId = node.id;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// Calendar method parameter
|
|
1770
|
+
if (node.parameters && node.parameters.method && typeof node.parameters.method === 'string') {
|
|
1771
|
+
attachment.method = node.parameters.method;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
attachments.push(attachment);
|
|
1775
|
+
} else if ((!node.disposition || node.disposition === 'inline') && /^text\/(plain|html)/.test(node.type)) {
|
|
1776
|
+
// Text part
|
|
1777
|
+
let type = node.type.substr(5);
|
|
1778
|
+
if (!encodedTextSize[type]) {
|
|
1779
|
+
encodedTextSize[type] = 0;
|
|
1780
|
+
}
|
|
1781
|
+
encodedTextSize[type] += node.size;
|
|
1782
|
+
|
|
1783
|
+
// Group by type
|
|
1784
|
+
switch (type) {
|
|
1785
|
+
case 'plain':
|
|
1786
|
+
textParts[0].push(node.part || '1');
|
|
1787
|
+
break;
|
|
1788
|
+
case 'html':
|
|
1789
|
+
textParts[1].push(node.part || '1');
|
|
1790
|
+
break;
|
|
1791
|
+
default:
|
|
1792
|
+
textParts[2].push(node.part || '1');
|
|
1793
|
+
break;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
// Recursively process multipart children
|
|
1799
|
+
if (node.childNodes) {
|
|
1800
|
+
node.childNodes.forEach(childNode => walk(childNode, isRelated));
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1804
|
+
walk(bodyStructure, false);
|
|
1805
|
+
|
|
1806
|
+
return {
|
|
1807
|
+
attachments,
|
|
1808
|
+
// Encode text parts array into ID
|
|
1809
|
+
textId: Buffer.concat([idBuf, msgpack.encode(textParts)]).toString('base64url'),
|
|
1810
|
+
encodedTextSize
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
/**
|
|
1815
|
+
* Processes flag changes for a message
|
|
1816
|
+
* @param {Object} messageData - Message data with changes
|
|
1817
|
+
* @param {Object} changes - What changed
|
|
1818
|
+
*/
|
|
1819
|
+
async processChanges(messageData, changes) {
|
|
1820
|
+
let packedUid = await this.connection.packUid(this, messageData.uid);
|
|
1821
|
+
await this.connection.notify(this, MESSAGE_UPDATED_NOTIFY, {
|
|
1822
|
+
id: packedUid,
|
|
1823
|
+
uid: messageData.uid,
|
|
1824
|
+
changes
|
|
1825
|
+
});
|
|
1826
|
+
await this.markUpdated();
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
/**
|
|
1830
|
+
* Performs full synchronization based on indexer type
|
|
1831
|
+
*/
|
|
1832
|
+
async fullSync() {
|
|
1833
|
+
const imapIndexer = this.imapIndexer;
|
|
1834
|
+
|
|
1835
|
+
this.logger.trace({ msg: 'Running full sync', imapIndexer });
|
|
1836
|
+
|
|
1837
|
+
switch (imapIndexer) {
|
|
1838
|
+
case 'fast':
|
|
1839
|
+
return this.runFastSync();
|
|
1840
|
+
case 'full':
|
|
1841
|
+
default:
|
|
1842
|
+
return this.runFullSync();
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/**
|
|
1847
|
+
* Performs partial synchronization based on indexer type
|
|
1848
|
+
* @param {Object} storedStatus - Current stored mailbox status
|
|
1849
|
+
*/
|
|
1850
|
+
async partialSync(storedStatus) {
|
|
1851
|
+
const imapIndexer = this.imapIndexer;
|
|
1852
|
+
|
|
1853
|
+
this.logger.trace({ msg: 'Running partial sync', imapIndexer });
|
|
1854
|
+
|
|
1855
|
+
switch (imapIndexer) {
|
|
1856
|
+
case 'fast':
|
|
1857
|
+
return this.runFastSync(storedStatus);
|
|
1858
|
+
case 'full':
|
|
1859
|
+
default:
|
|
1860
|
+
return this.runPartialSync(storedStatus);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
/**
|
|
1865
|
+
* Fast sync mode - only tracks new messages, doesn't maintain full message list
|
|
1866
|
+
* More efficient for large mailboxes where we only care about new messages
|
|
1867
|
+
* @param {Object} storedStatus - Current stored mailbox status
|
|
1868
|
+
*/
|
|
1869
|
+
async runFastSync(storedStatus) {
|
|
1870
|
+
storedStatus = storedStatus || (await this.getStoredStatus());
|
|
1871
|
+
let mailboxStatus = this.getMailboxStatus();
|
|
1872
|
+
|
|
1873
|
+
let lock = await this.getMailboxLock(null, { description: 'Fast sync' });
|
|
1874
|
+
this.connection.syncing = true;
|
|
1875
|
+
this.syncing = true;
|
|
1876
|
+
try {
|
|
1877
|
+
if (!this.connection.imapClient) {
|
|
1878
|
+
throw new Error('IMAP connection not available');
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
let knownUidNext = typeof storedStatus.uidNext === 'number' ? storedStatus.uidNext || 1 : 1;
|
|
1882
|
+
|
|
1883
|
+
if (knownUidNext && mailboxStatus.messages) {
|
|
1884
|
+
// detected new emails
|
|
1885
|
+
let fields = { uid: true, flags: true, modseq: true, emailId: true, labels: true, internalDate: true };
|
|
1886
|
+
|
|
1887
|
+
let imapClient = this.connection.imapClient;
|
|
1888
|
+
|
|
1889
|
+
// If we have not yet scanned this folder, then start by finding the earliest matching email
|
|
1890
|
+
if (typeof storedStatus.uidNext !== 'number' && this.connection.notifyFrom && this.connection.notifyFrom < new Date()) {
|
|
1891
|
+
// Find first message after notifyFrom date
|
|
1892
|
+
let matchingMessages = await imapClient.search({ since: this.connection.notifyFrom }, { uid: true });
|
|
1893
|
+
if (matchingMessages) {
|
|
1894
|
+
let earliestUid = matchingMessages[0];
|
|
1895
|
+
if (earliestUid) {
|
|
1896
|
+
knownUidNext = earliestUid;
|
|
1897
|
+
} else if (mailboxStatus.uidNext) {
|
|
1898
|
+
// no match, start from newest
|
|
1899
|
+
knownUidNext = mailboxStatus.uidNext;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
let range = `${knownUidNext}:*`;
|
|
1905
|
+
let opts = {
|
|
1906
|
+
uid: true
|
|
1907
|
+
};
|
|
1908
|
+
|
|
1909
|
+
// Fetch messages with retry logic
|
|
1910
|
+
let fetchCompleted = false;
|
|
1911
|
+
let fetchRetryCount = 0;
|
|
1912
|
+
|
|
1913
|
+
while (!fetchCompleted) {
|
|
1914
|
+
try {
|
|
1915
|
+
let messages = [];
|
|
1916
|
+
|
|
1917
|
+
// Fetch all messages in range
|
|
1918
|
+
for await (let messageData of imapClient.fetch(range, fields, opts)) {
|
|
1919
|
+
if (!messageData || !messageData.uid) {
|
|
1920
|
+
//TODO: support partial responses
|
|
1921
|
+
this.logger.debug({ msg: 'Partial FETCH response', code: 'partial_fetch', query: { range, fields, opts } });
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// ignore Recent flag
|
|
1926
|
+
messageData.flags.delete('\\Recent');
|
|
1927
|
+
|
|
1928
|
+
messages.push(messageData);
|
|
1929
|
+
}
|
|
1930
|
+
// ensure that messages are sorted by UID
|
|
1931
|
+
messages = messages.sort((a, b) => a.uid - b.uid);
|
|
1932
|
+
|
|
1933
|
+
// Process each new message
|
|
1934
|
+
for (let messageData of messages) {
|
|
1935
|
+
// Update uidNext if this is a new message
|
|
1936
|
+
let updated = await this.connection.redis.hUpdateBigger(this.getMailboxKey(), 'uidNext', messageData.uid + 1, messageData.uid + 1);
|
|
1937
|
+
|
|
1938
|
+
if (updated) {
|
|
1939
|
+
// new email! Queue for processing
|
|
1940
|
+
await this.connection.redis.zadd(
|
|
1941
|
+
this.getNotificationsKey(),
|
|
1942
|
+
messageData.uid,
|
|
1943
|
+
JSON.stringify({
|
|
1944
|
+
uid: messageData.uid,
|
|
1945
|
+
flags: messageData.flags,
|
|
1946
|
+
internalDate:
|
|
1947
|
+
(messageData.internalDate &&
|
|
1948
|
+
typeof messageData.internalDate.toISOString === 'function' &&
|
|
1949
|
+
messageData.internalDate.toISOString()) ||
|
|
1950
|
+
null
|
|
1951
|
+
})
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
try {
|
|
1957
|
+
// clear failure flag
|
|
1958
|
+
await this.connection.redis.hdel(this.connection.getAccountKey(), 'syncError');
|
|
1959
|
+
} catch (err) {
|
|
1960
|
+
// ignore
|
|
1961
|
+
}
|
|
1962
|
+
fetchCompleted = true;
|
|
1963
|
+
} catch (err) {
|
|
1964
|
+
try {
|
|
1965
|
+
// set failure flag
|
|
1966
|
+
await this.connection.redis.hSetExists(
|
|
1967
|
+
this.connection.getAccountKey(),
|
|
1968
|
+
'syncError',
|
|
1969
|
+
JSON.stringify({
|
|
1970
|
+
path: this.path,
|
|
1971
|
+
time: new Date().toISOString(),
|
|
1972
|
+
error: {
|
|
1973
|
+
error: err.message,
|
|
1974
|
+
responseStatus: err.responseStatus,
|
|
1975
|
+
responseText: err.responseText
|
|
1976
|
+
}
|
|
1977
|
+
})
|
|
1978
|
+
);
|
|
1979
|
+
} catch (err) {
|
|
1980
|
+
// ignore
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// Retry with exponential backoff
|
|
1984
|
+
if (!imapClient.usable) {
|
|
1985
|
+
// nothing to do here, connection closed
|
|
1986
|
+
this.logger.error({ msg: `FETCH failed, connection already closed, not retrying`, err });
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
const fetchRetryDelay = calculateFetchBackoff(++fetchRetryCount);
|
|
1991
|
+
this.logger.error({ msg: `FETCH failed, retrying in ${Math.round(fetchRetryDelay / 1000)}s`, err });
|
|
1992
|
+
await new Promise(r => setTimeout(r, fetchRetryDelay));
|
|
1993
|
+
|
|
1994
|
+
if (!imapClient.usable) {
|
|
1995
|
+
// nothing to do here, connection closed
|
|
1996
|
+
this.logger.error({ msg: `FETCH failed, connection already closed, not retrying`, err });
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
await this.updateStoredStatus(this.getMailboxStatus());
|
|
2004
|
+
|
|
2005
|
+
await this.publishSyncedEvents(storedStatus);
|
|
2006
|
+
} finally {
|
|
2007
|
+
lock.release();
|
|
2008
|
+
this.connection.syncing = false;
|
|
2009
|
+
this.syncing = false;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
/**
|
|
2014
|
+
* Partial sync - fetches only changed messages using MODSEQ or UID range
|
|
2015
|
+
* Used for incremental updates when we know something changed
|
|
2016
|
+
* @param {Object} storedStatus - Current stored mailbox status
|
|
2017
|
+
*/
|
|
2018
|
+
async runPartialSync(storedStatus) {
|
|
2019
|
+
storedStatus = storedStatus || (await this.getStoredStatus());
|
|
2020
|
+
let mailboxStatus = this.getMailboxStatus();
|
|
2021
|
+
|
|
2022
|
+
let lock = await this.getMailboxLock(null, { description: 'Partial sync' });
|
|
2023
|
+
this.connection.syncing = true;
|
|
2024
|
+
this.syncing = true;
|
|
2025
|
+
try {
|
|
2026
|
+
if (!this.connection.imapClient) {
|
|
2027
|
+
throw new Error('IMAP connection not available');
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
let fields = { uid: true, flags: true, modseq: true, emailId: true, labels: true, internalDate: true };
|
|
2031
|
+
let range = '1:*';
|
|
2032
|
+
let opts = {
|
|
2033
|
+
uid: true
|
|
2034
|
+
};
|
|
2035
|
+
|
|
2036
|
+
// Use CONDSTORE if available for efficient change detection
|
|
2037
|
+
if (this.connection.imapClient.enabled.has('CONDSTORE') && storedStatus.highestModseq) {
|
|
2038
|
+
// Only fetch messages changed since last known MODSEQ
|
|
2039
|
+
opts.changedSince = storedStatus.highestModseq;
|
|
2040
|
+
} else if (storedStatus.uidNext) {
|
|
2041
|
+
// Fall back to fetching new messages only
|
|
2042
|
+
range = `${storedStatus.uidNext}:*`;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
if (mailboxStatus.messages) {
|
|
2046
|
+
// only fetch messages if there are some
|
|
2047
|
+
let fetchCompleted = false;
|
|
2048
|
+
let fetchRetryCount = 0;
|
|
2049
|
+
while (!fetchCompleted) {
|
|
2050
|
+
// Get fresh imapClient reference inside retry loop
|
|
2051
|
+
let imapClient = this.connection.imapClient;
|
|
2052
|
+
if (!imapClient || !imapClient.usable) {
|
|
2053
|
+
this.logger.error({ msg: 'IMAP client not available for partial sync' });
|
|
2054
|
+
throw new Error('IMAP connection not available');
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
try {
|
|
2058
|
+
// Fetch and process each message
|
|
2059
|
+
for await (let messageData of imapClient.fetch(range, fields, opts)) {
|
|
2060
|
+
if (!messageData || !messageData.uid) {
|
|
2061
|
+
//TODO: support partial responses
|
|
2062
|
+
this.logger.debug({ msg: 'Partial FETCH response', code: 'partial_fetch', query: { range, fields, opts } });
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
// ignore Recent flag
|
|
2067
|
+
messageData.flags.delete('\\Recent');
|
|
2068
|
+
|
|
2069
|
+
let storedMessage = await this.entryListGet(messageData.uid, { uid: true });
|
|
2070
|
+
|
|
2071
|
+
let changes;
|
|
2072
|
+
if (!storedMessage) {
|
|
2073
|
+
// New message
|
|
2074
|
+
let seq = await this.entryListSet(messageData);
|
|
2075
|
+
if (seq) {
|
|
2076
|
+
// Queue for processing
|
|
2077
|
+
await this.connection.redis.zadd(
|
|
2078
|
+
this.getNotificationsKey(),
|
|
2079
|
+
messageData.uid,
|
|
2080
|
+
JSON.stringify({
|
|
2081
|
+
uid: messageData.uid,
|
|
2082
|
+
flags: messageData.flags,
|
|
2083
|
+
internalDate:
|
|
2084
|
+
(messageData.internalDate &&
|
|
2085
|
+
typeof messageData.internalDate.toISOString === 'function' &&
|
|
2086
|
+
messageData.internalDate.toISOString()) ||
|
|
2087
|
+
null
|
|
2088
|
+
})
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
} else if ((changes = compareExisting(storedMessage.entry, messageData))) {
|
|
2092
|
+
// Existing message with changes
|
|
2093
|
+
let seq = await this.entryListSet(messageData);
|
|
2094
|
+
if (seq) {
|
|
2095
|
+
await this.processChanges(messageData, changes);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
try {
|
|
2100
|
+
// clear failure flag
|
|
2101
|
+
await this.connection.redis.hdel(this.connection.getAccountKey(), 'syncError');
|
|
2102
|
+
} catch (err) {
|
|
2103
|
+
// ignore
|
|
2104
|
+
}
|
|
2105
|
+
fetchCompleted = true;
|
|
2106
|
+
} catch (err) {
|
|
2107
|
+
try {
|
|
2108
|
+
// set failure flag
|
|
2109
|
+
await this.connection.redis.hSetExists(
|
|
2110
|
+
this.connection.getAccountKey(),
|
|
2111
|
+
'syncError',
|
|
2112
|
+
JSON.stringify({
|
|
2113
|
+
path: this.path,
|
|
2114
|
+
time: new Date().toISOString(),
|
|
2115
|
+
error: {
|
|
2116
|
+
error: err.message,
|
|
2117
|
+
responseStatus: err.responseStatus,
|
|
2118
|
+
responseText: err.responseText
|
|
2119
|
+
}
|
|
2120
|
+
})
|
|
2121
|
+
);
|
|
2122
|
+
} catch (err) {
|
|
2123
|
+
// ignore
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
// Retry with exponential backoff
|
|
2127
|
+
if (!imapClient.usable) {
|
|
2128
|
+
// nothing to do here, connection closed
|
|
2129
|
+
this.logger.error({ msg: `FETCH failed, connection already closed, not retrying` });
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
const fetchRetryDelay = calculateFetchBackoff(++fetchRetryCount);
|
|
2134
|
+
this.logger.error({ msg: `FETCH failed, retrying in ${Math.round(fetchRetryDelay / 1000)}s` });
|
|
2135
|
+
await new Promise(r => setTimeout(r, fetchRetryDelay));
|
|
2136
|
+
|
|
2137
|
+
if (!imapClient.usable) {
|
|
2138
|
+
// nothing to do here, connection closed
|
|
2139
|
+
this.logger.error({ msg: `FETCH failed, connection already closed, not retrying` });
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
await this.updateStoredStatus(this.getMailboxStatus());
|
|
2147
|
+
|
|
2148
|
+
await this.publishSyncedEvents(storedStatus);
|
|
2149
|
+
} finally {
|
|
2150
|
+
lock.release();
|
|
2151
|
+
this.connection.syncing = false;
|
|
2152
|
+
this.syncing = false;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
/**
|
|
2157
|
+
* Full sync - fetches all messages and detects additions, deletions, and changes
|
|
2158
|
+
* Most thorough but slowest sync method
|
|
2159
|
+
*/
|
|
2160
|
+
async runFullSync() {
|
|
2161
|
+
let fields = { uid: true, flags: true, modseq: true, emailId: true, labels: true, internalDate: true };
|
|
2162
|
+
let opts = {};
|
|
2163
|
+
|
|
2164
|
+
let lock = await this.getMailboxLock(null, { description: 'Full sync' });
|
|
2165
|
+
this.connection.syncing = true;
|
|
2166
|
+
this.syncing = true;
|
|
2167
|
+
try {
|
|
2168
|
+
// Generate unique ID for this sync loop to track batch ordering
|
|
2169
|
+
const loopId = crypto.randomUUID();
|
|
2170
|
+
|
|
2171
|
+
// Wait for next tick to ensure ImapFlow has processed all untagged responses from SELECT
|
|
2172
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
2173
|
+
|
|
2174
|
+
let mailboxStatus = this.getMailboxStatus();
|
|
2175
|
+
|
|
2176
|
+
this.logger.debug({
|
|
2177
|
+
msg: 'Starting full sync',
|
|
2178
|
+
code: 'full_sync_start',
|
|
2179
|
+
loopId,
|
|
2180
|
+
mailboxStatus,
|
|
2181
|
+
imapClientExists: mailboxStatus.messages
|
|
2182
|
+
});
|
|
2183
|
+
|
|
2184
|
+
// Track highest sequence number seen
|
|
2185
|
+
let seqMax = 0;
|
|
2186
|
+
let changes;
|
|
2187
|
+
|
|
2188
|
+
// Get current message count for deletion detection
|
|
2189
|
+
let storedMaxSeqOld = await this.connection.redis.zcard(this.getMessagesKey());
|
|
2190
|
+
|
|
2191
|
+
let responseCounters = {
|
|
2192
|
+
empty: 0,
|
|
2193
|
+
partial: 0,
|
|
2194
|
+
messages: 0
|
|
2195
|
+
};
|
|
2196
|
+
|
|
2197
|
+
if (mailboxStatus.messages) {
|
|
2198
|
+
this.logger.debug({
|
|
2199
|
+
msg: 'Running FETCH',
|
|
2200
|
+
code: 'run_fetch',
|
|
2201
|
+
query: { fields, opts },
|
|
2202
|
+
expectedMessages: mailboxStatus.messages,
|
|
2203
|
+
mailbox: mailboxStatus,
|
|
2204
|
+
maxBatchSize: FETCH_BATCH_SIZE,
|
|
2205
|
+
expectedBatches: Math.ceil(mailboxStatus.messages / FETCH_BATCH_SIZE)
|
|
2206
|
+
});
|
|
2207
|
+
|
|
2208
|
+
// Process messages in batches to avoid memory issues
|
|
2209
|
+
let range = false;
|
|
2210
|
+
let lastHighestUid = 0;
|
|
2211
|
+
let batchNumber = 0;
|
|
2212
|
+
// process messages in batches
|
|
2213
|
+
while ((range = getFetchRange(mailboxStatus.messages, range))) {
|
|
2214
|
+
batchNumber++;
|
|
2215
|
+
this.logger.debug({
|
|
2216
|
+
msg: 'Processing batch',
|
|
2217
|
+
code: 'fetch_batch',
|
|
2218
|
+
loopId,
|
|
2219
|
+
batchNumber,
|
|
2220
|
+
range,
|
|
2221
|
+
totalMessages: mailboxStatus.messages,
|
|
2222
|
+
previousRange: batchNumber > 1 ? 'calculated' : 'initial'
|
|
2223
|
+
});
|
|
2224
|
+
let fetchCompleted = false;
|
|
2225
|
+
let fetchRetryCount = 0;
|
|
2226
|
+
while (!fetchCompleted) {
|
|
2227
|
+
// Get fresh imapClient reference inside retry loop
|
|
2228
|
+
// This ensures we use the current connection state
|
|
2229
|
+
const imapClient = this.connection.imapClient;
|
|
2230
|
+
if (!imapClient || !imapClient.usable) {
|
|
2231
|
+
this.logger.error({ msg: 'IMAP client not available for FETCH' });
|
|
2232
|
+
throw new Error('IMAP connection not available');
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
try {
|
|
2236
|
+
this.logger.debug({
|
|
2237
|
+
msg: 'Starting FETCH command',
|
|
2238
|
+
code: 'fetch_start',
|
|
2239
|
+
loopId,
|
|
2240
|
+
batchNumber,
|
|
2241
|
+
range,
|
|
2242
|
+
retryCount: fetchRetryCount,
|
|
2243
|
+
totalMessages: mailboxStatus.messages
|
|
2244
|
+
});
|
|
2245
|
+
|
|
2246
|
+
for await (let messageData of imapClient.fetch(range, fields, opts)) {
|
|
2247
|
+
if (!messageData) {
|
|
2248
|
+
this.logger.debug({ msg: 'Empty FETCH response', code: 'empty_fetch', query: { range, fields, opts } });
|
|
2249
|
+
responseCounters.empty++;
|
|
2250
|
+
continue;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
if (!messageData.uid || (fields.flags && !messageData.flags)) {
|
|
2254
|
+
// TODO: support partial responses
|
|
2255
|
+
// For now, without UID or FLAGS there's nothing to do
|
|
2256
|
+
this.logger.debug({
|
|
2257
|
+
msg: 'Partial FETCH response',
|
|
2258
|
+
code: 'partial_fetch',
|
|
2259
|
+
query: { range, fields, opts },
|
|
2260
|
+
responseKeys: Object.keys(messageData)
|
|
2261
|
+
});
|
|
2262
|
+
responseCounters.partial++;
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
if (messageData.uid <= lastHighestUid) {
|
|
2267
|
+
// already processed in the previous batch
|
|
2268
|
+
// probably an older email was deleted which shifted message entries
|
|
2269
|
+
continue;
|
|
2270
|
+
}
|
|
2271
|
+
lastHighestUid = messageData.uid;
|
|
2272
|
+
|
|
2273
|
+
responseCounters.messages++;
|
|
2274
|
+
|
|
2275
|
+
if (fields.internalDate && !messageData.internalDate) {
|
|
2276
|
+
this.logger.debug({
|
|
2277
|
+
msg: 'Missing INTERNALDATE',
|
|
2278
|
+
code: 'fetch_date_missing',
|
|
2279
|
+
query: { range, fields, opts },
|
|
2280
|
+
responseKeys: Object.keys(messageData)
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// ignore Recent flag
|
|
2285
|
+
messageData.flags.delete('\\Recent');
|
|
2286
|
+
|
|
2287
|
+
if (messageData.seq > seqMax) {
|
|
2288
|
+
seqMax = messageData.seq;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
let storedMessage = await this.entryListGet(messageData.uid, { uid: true });
|
|
2292
|
+
if (!storedMessage) {
|
|
2293
|
+
// New message
|
|
2294
|
+
let seq = await this.entryListSet(messageData);
|
|
2295
|
+
if (seq) {
|
|
2296
|
+
await this.connection.redis.zadd(
|
|
2297
|
+
this.getNotificationsKey(),
|
|
2298
|
+
messageData.uid,
|
|
2299
|
+
JSON.stringify({
|
|
2300
|
+
uid: messageData.uid,
|
|
2301
|
+
flags: messageData.flags,
|
|
2302
|
+
internalDate:
|
|
2303
|
+
(messageData.internalDate &&
|
|
2304
|
+
typeof messageData.internalDate.toISOString === 'function' &&
|
|
2305
|
+
messageData.internalDate.toISOString()) ||
|
|
2306
|
+
null
|
|
2307
|
+
})
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2310
|
+
} else {
|
|
2311
|
+
// Check for deleted messages between stored and current sequence
|
|
2312
|
+
let diff = storedMessage.seq - messageData.seq;
|
|
2313
|
+
if (diff) {
|
|
2314
|
+
this.logger.trace({ msg: 'Deleted range', inloop: true, diff, start: messageData.seq });
|
|
2315
|
+
}
|
|
2316
|
+
// Process deletions
|
|
2317
|
+
for (let i = diff - 1; i >= 0; i--) {
|
|
2318
|
+
let seq = messageData.seq + i;
|
|
2319
|
+
let deletedEntry = await this.entryListExpunge(seq);
|
|
2320
|
+
if (deletedEntry) {
|
|
2321
|
+
await this.processDeleted(deletedEntry);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
// Check for changes
|
|
2326
|
+
if ((changes = compareExisting(storedMessage.entry, messageData))) {
|
|
2327
|
+
let seq = await this.entryListSet(messageData);
|
|
2328
|
+
if (seq) {
|
|
2329
|
+
await this.processChanges(messageData, changes);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
try {
|
|
2336
|
+
// clear failure flag
|
|
2337
|
+
await this.connection.redis.hdel(this.connection.getAccountKey(), 'syncError');
|
|
2338
|
+
} catch (err) {
|
|
2339
|
+
// ignore
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
this.logger.debug({
|
|
2343
|
+
msg: 'FETCH completed successfully',
|
|
2344
|
+
code: 'fetch_success',
|
|
2345
|
+
loopId,
|
|
2346
|
+
batchNumber,
|
|
2347
|
+
range,
|
|
2348
|
+
retryCount: fetchRetryCount
|
|
2349
|
+
});
|
|
2350
|
+
|
|
2351
|
+
fetchCompleted = true;
|
|
2352
|
+
} catch (err) {
|
|
2353
|
+
this.logger.error({
|
|
2354
|
+
msg: 'FETCH failed',
|
|
2355
|
+
code: 'fetch_error',
|
|
2356
|
+
loopId,
|
|
2357
|
+
batchNumber,
|
|
2358
|
+
range,
|
|
2359
|
+
retryCount: fetchRetryCount,
|
|
2360
|
+
totalMessages: mailboxStatus.messages,
|
|
2361
|
+
error: err.message,
|
|
2362
|
+
responseStatus: err.responseStatus,
|
|
2363
|
+
responseText: err.responseText
|
|
2364
|
+
});
|
|
2365
|
+
|
|
2366
|
+
if (!imapClient.usable) {
|
|
2367
|
+
// nothing to do here, connection closed
|
|
2368
|
+
this.logger.error({ msg: `FETCH failed, connection already closed, not retrying`, loopId });
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
try {
|
|
2373
|
+
// set failure flag
|
|
2374
|
+
await this.connection.redis.hSetExists(
|
|
2375
|
+
this.connection.getAccountKey(),
|
|
2376
|
+
'syncError',
|
|
2377
|
+
JSON.stringify({
|
|
2378
|
+
path: this.path,
|
|
2379
|
+
time: new Date().toISOString(),
|
|
2380
|
+
error: {
|
|
2381
|
+
error: err.message,
|
|
2382
|
+
responseStatus: err.responseStatus,
|
|
2383
|
+
responseText: err.responseText
|
|
2384
|
+
}
|
|
2385
|
+
})
|
|
2386
|
+
);
|
|
2387
|
+
} catch (err) {
|
|
2388
|
+
// ignore
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
// Retry with exponential backoff
|
|
2392
|
+
const fetchRetryDelay = calculateFetchBackoff(++fetchRetryCount);
|
|
2393
|
+
this.logger.error({ msg: `FETCH failed, retrying in ${Math.round(fetchRetryDelay / 1000)}s`, loopId, batchNumber });
|
|
2394
|
+
await new Promise(r => setTimeout(r, fetchRetryDelay));
|
|
2395
|
+
|
|
2396
|
+
if (!imapClient.usable) {
|
|
2397
|
+
// nothing to do here, connection closed
|
|
2398
|
+
this.logger.error({ msg: `FETCH failed, connection already closed, not retrying`, loopId });
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
// Verify we're still on the correct mailbox after the delay
|
|
2403
|
+
// Another operation might have changed the mailbox while we were waiting
|
|
2404
|
+
const currentMailbox = this.connection.imapClient.mailbox;
|
|
2405
|
+
if (!currentMailbox || currentMailbox.path !== this.path) {
|
|
2406
|
+
this.logger.error({
|
|
2407
|
+
msg: 'Mailbox changed during retry delay, aborting sync',
|
|
2408
|
+
expectedPath: this.path,
|
|
2409
|
+
currentPath: currentMailbox ? currentMailbox.path : 'none',
|
|
2410
|
+
loopId
|
|
2411
|
+
});
|
|
2412
|
+
throw new Error('Mailbox changed during sync operation');
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
// Refresh mailbox status in case it changed
|
|
2416
|
+
const oldMailboxMessages = mailboxStatus.messages;
|
|
2417
|
+
mailboxStatus = this.getMailboxStatus();
|
|
2418
|
+
|
|
2419
|
+
this.logger.debug({
|
|
2420
|
+
msg: 'Refreshed mailbox status after error',
|
|
2421
|
+
code: 'mailbox_status_refresh',
|
|
2422
|
+
loopId,
|
|
2423
|
+
batchNumber,
|
|
2424
|
+
oldMessages: oldMailboxMessages,
|
|
2425
|
+
newMessages: mailboxStatus.messages,
|
|
2426
|
+
range
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
// Delete any messages that weren't seen in this sync
|
|
2434
|
+
let storedMaxSeq = await this.connection.redis.zcard(this.getMessagesKey());
|
|
2435
|
+
let diff = storedMaxSeq - seqMax;
|
|
2436
|
+
if (diff) {
|
|
2437
|
+
this.logger.trace({
|
|
2438
|
+
msg: 'Deleted range',
|
|
2439
|
+
inloop: false,
|
|
2440
|
+
diff,
|
|
2441
|
+
start: seqMax + 1,
|
|
2442
|
+
messagesKey: this.getMessagesKey(),
|
|
2443
|
+
zcard: storedMaxSeq,
|
|
2444
|
+
zcardOld: storedMaxSeqOld,
|
|
2445
|
+
responseCounters
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
// Process remaining deletions
|
|
2450
|
+
for (let i = diff - 1; i >= 0; i--) {
|
|
2451
|
+
let seq = seqMax + i + 1;
|
|
2452
|
+
let deletedEntry = await this.entryListExpunge(seq);
|
|
2453
|
+
if (deletedEntry) {
|
|
2454
|
+
await this.processDeleted(deletedEntry);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
// Update status with full sync timestamp
|
|
2459
|
+
let status = this.getMailboxStatus();
|
|
2460
|
+
status.lastFullSync = new Date();
|
|
2461
|
+
|
|
2462
|
+
await this.updateStoredStatus(status);
|
|
2463
|
+
let storedStatus = await this.getStoredStatus();
|
|
2464
|
+
|
|
2465
|
+
await this.publishSyncedEvents(storedStatus);
|
|
2466
|
+
} finally {
|
|
2467
|
+
this.connection.syncing = false;
|
|
2468
|
+
this.syncing = false;
|
|
2469
|
+
lock.release();
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
/**
|
|
2474
|
+
* Processes queued notification events after sync
|
|
2475
|
+
* Fetches full message details and sends notifications
|
|
2476
|
+
* @param {Object} storedStatus - Current mailbox status
|
|
2477
|
+
*/
|
|
2478
|
+
async publishSyncedEvents(storedStatus) {
|
|
2479
|
+
let messageFetchOptions = {};
|
|
2480
|
+
|
|
2481
|
+
let documentStoreEnabled = await settings.get('documentStoreEnabled');
|
|
2482
|
+
|
|
2483
|
+
// Configure text fetching
|
|
2484
|
+
let notifyText = await settings.get('notifyText');
|
|
2485
|
+
if (documentStoreEnabled || notifyText) {
|
|
2486
|
+
messageFetchOptions.textType = '*';
|
|
2487
|
+
let notifyTextSize = await settings.get('notifyTextSize');
|
|
2488
|
+
|
|
2489
|
+
if (documentStoreEnabled && notifyTextSize) {
|
|
2490
|
+
// Ensure at least 1MB for document store
|
|
2491
|
+
notifyTextSize = Math.max(notifyTextSize, 1024 * 1024);
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
if (notifyTextSize) {
|
|
2495
|
+
messageFetchOptions.maxBytes = notifyTextSize;
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
// Configure header fetching
|
|
2500
|
+
let notifyHeaders = (await settings.get('notifyHeaders')) || [];
|
|
2501
|
+
if (documentStoreEnabled || notifyHeaders.length) {
|
|
2502
|
+
messageFetchOptions.headers = notifyHeaders.includes('*') || documentStoreEnabled ? true : notifyHeaders.length ? notifyHeaders : false;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
// Also request autoresponse headers
|
|
2506
|
+
if (messageFetchOptions.headers !== true) {
|
|
2507
|
+
let fetchHeaders = new Set(messageFetchOptions.headers || []);
|
|
2508
|
+
|
|
2509
|
+
// Auto-reply detection headers
|
|
2510
|
+
fetchHeaders.add('x-autoreply');
|
|
2511
|
+
fetchHeaders.add('x-autorespond');
|
|
2512
|
+
fetchHeaders.add('auto-submitted');
|
|
2513
|
+
fetchHeaders.add('precedence');
|
|
2514
|
+
|
|
2515
|
+
// Threading headers
|
|
2516
|
+
fetchHeaders.add('in-reply-to');
|
|
2517
|
+
fetchHeaders.add('references');
|
|
2518
|
+
|
|
2519
|
+
// Content type for bounce/complaint detection
|
|
2520
|
+
fetchHeaders.add('content-type');
|
|
2521
|
+
|
|
2522
|
+
if (this.isLarkSuite) {
|
|
2523
|
+
// Add address headers as a fallback for unreliable ENVELOPE
|
|
2524
|
+
fetchHeaders.add('from');
|
|
2525
|
+
fetchHeaders.add('to');
|
|
2526
|
+
fetchHeaders.add('cc');
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
messageFetchOptions.fetchHeaders = Array.from(fetchHeaders);
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// Process queued notifications
|
|
2533
|
+
let queuedEntry;
|
|
2534
|
+
let hadUpdates = false;
|
|
2535
|
+
while ((queuedEntry = await this.connection.redis.zpopmin(this.getNotificationsKey(), 1)) && queuedEntry.length) {
|
|
2536
|
+
hadUpdates = true;
|
|
2537
|
+
|
|
2538
|
+
let [messageData, uid] = queuedEntry;
|
|
2539
|
+
uid = Number(uid);
|
|
2540
|
+
try {
|
|
2541
|
+
messageData = JSON.parse(messageData);
|
|
2542
|
+
if (typeof messageData.internalDate === 'string') {
|
|
2543
|
+
messageData.internalDate = new Date(messageData.internalDate);
|
|
2544
|
+
}
|
|
2545
|
+
} catch (err) {
|
|
2546
|
+
continue;
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
// Check if message should be synced to document store
|
|
2550
|
+
let canSync = documentStoreEnabled && (!this.connection.syncFrom || messageData.internalDate >= this.connection.syncFrom);
|
|
2551
|
+
|
|
2552
|
+
if (this.connection.notifyFrom && messageData.internalDate < this.connection.notifyFrom && !canSync) {
|
|
2553
|
+
// skip too old messages
|
|
2554
|
+
continue;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
await this.processNew(messageData, messageFetchOptions, canSync, storedStatus);
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
if (hadUpdates) {
|
|
2561
|
+
await this.markUpdated();
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
/**
|
|
2566
|
+
* Called when mailbox is opened/selected
|
|
2567
|
+
* Determines what type of sync is needed based on current state
|
|
2568
|
+
*/
|
|
2569
|
+
async onOpen() {
|
|
2570
|
+
clearTimeout(this.runPartialSyncTimer);
|
|
2571
|
+
this.selected = true;
|
|
2572
|
+
|
|
2573
|
+
// Track connection count to detect reconnects
|
|
2574
|
+
this.previouslyConnected = Number(await this.connection.redis.hget(this.connection.getAccountKey(), `state:count:connected`)) || 0;
|
|
2575
|
+
|
|
2576
|
+
let mailboxStatus = this.getMailboxStatus();
|
|
2577
|
+
|
|
2578
|
+
try {
|
|
2579
|
+
let storedStatus = await this.getStoredStatus();
|
|
2580
|
+
|
|
2581
|
+
// Store initial UID on first sync
|
|
2582
|
+
if (storedStatus.uidNext === false && typeof mailboxStatus.uidNext === 'number') {
|
|
2583
|
+
// update first UID
|
|
2584
|
+
await this.connection.redis.hSetNew(this.getMailboxKey(), 'initialUidNext', mailboxStatus.uidNext.toString());
|
|
2585
|
+
storedStatus = await this.getStoredStatus();
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
// Process any queued notifications first
|
|
2589
|
+
let hasQueuedNotifications = await this.connection.redis.exists(this.getNotificationsKey());
|
|
2590
|
+
if (hasQueuedNotifications) {
|
|
2591
|
+
return await this.fullSync();
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
// Check for UIDVALIDITY change (mailbox recreated)
|
|
2595
|
+
if ('uidValidity' in storedStatus && mailboxStatus.uidValidity !== storedStatus.uidValidity) {
|
|
2596
|
+
// UIDVALIDITY has changed, full sync is required!
|
|
2597
|
+
// delete mailbox status
|
|
2598
|
+
let result = await this.connection.redis.multi().zcard(this.getMessagesKey()).del(this.getMessagesKey()).del(this.getMailboxKey()).exec();
|
|
2599
|
+
|
|
2600
|
+
let deletedMessages = (result[0] && Number(result[0][1])) || 0;
|
|
2601
|
+
this.logger.info({
|
|
2602
|
+
msg: 'UIDVALIDITY change',
|
|
2603
|
+
deleted: deletedMessages,
|
|
2604
|
+
prevUidValidity: validUidValidity(storedStatus.uidValidity) ? storedStatus.uidValidity.toString() : false,
|
|
2605
|
+
uidValidity: validUidValidity(mailboxStatus.uidValidity) ? mailboxStatus.uidValidity.toString() : false
|
|
2606
|
+
});
|
|
2607
|
+
|
|
2608
|
+
this.logger.debug({ msg: 'Mailbox reset', path: this.listingEntry.path });
|
|
2609
|
+
await this.connection.notify(this, MAILBOX_RESET_NOTIFY, {
|
|
2610
|
+
path: this.listingEntry.path,
|
|
2611
|
+
name: this.listingEntry.name,
|
|
2612
|
+
specialUse: this.listingEntry.specialUse || false,
|
|
2613
|
+
uidValidity: validUidValidity(mailboxStatus.uidValidity) ? mailboxStatus.uidValidity.toString() : false,
|
|
2614
|
+
prevUidValidity: validUidValidity(storedStatus.uidValidity) ? storedStatus.uidValidity.toString() : false
|
|
2615
|
+
});
|
|
2616
|
+
|
|
2617
|
+
// do not advertise messages as new
|
|
2618
|
+
this.listingEntry.isNew = true;
|
|
2619
|
+
|
|
2620
|
+
// generates blank stored status as the Redis key was deleted
|
|
2621
|
+
storedStatus = await this.getStoredStatus();
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
// Determine sync strategy based on various conditions
|
|
2625
|
+
|
|
2626
|
+
// No changes if MODSEQ hasn't changed
|
|
2627
|
+
if (storedStatus.highestModseq && storedStatus.highestModseq === mailboxStatus.highestModseq) {
|
|
2628
|
+
return false;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
// No changes if mailbox is empty
|
|
2632
|
+
if (storedStatus.messages === 0 && mailboxStatus.messages === 0) {
|
|
2633
|
+
return false;
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
// Partial sync if we can detect only new messages or flag changes
|
|
2637
|
+
if (
|
|
2638
|
+
this.connection.imapClient.enabled.has('CONDSTORE') &&
|
|
2639
|
+
storedStatus.highestModseq < mailboxStatus.highestModseq &&
|
|
2640
|
+
storedStatus.messages <= mailboxStatus.messages &&
|
|
2641
|
+
mailboxStatus.uidNext - storedStatus.uidNext === mailboxStatus.messages - storedStatus.messages
|
|
2642
|
+
) {
|
|
2643
|
+
// search for flag changes and new messages
|
|
2644
|
+
return await this.partialSync(storedStatus);
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
// Partial sync if only new messages
|
|
2648
|
+
if (
|
|
2649
|
+
storedStatus.messages < mailboxStatus.messages &&
|
|
2650
|
+
mailboxStatus.uidNext - storedStatus.uidNext === mailboxStatus.messages - storedStatus.messages
|
|
2651
|
+
) {
|
|
2652
|
+
// seem to have new messages only
|
|
2653
|
+
return await this.partialSync(storedStatus);
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
// Skip if nothing changed and recent full sync
|
|
2657
|
+
if (
|
|
2658
|
+
storedStatus.messages === mailboxStatus.messages &&
|
|
2659
|
+
storedStatus.uidNext === mailboxStatus.uidNext &&
|
|
2660
|
+
storedStatus.lastFullSync &&
|
|
2661
|
+
storedStatus.lastFullSync >= new Date(Date.now() - FULL_SYNC_DELAY)
|
|
2662
|
+
) {
|
|
2663
|
+
// too soon from last full sync, message count seems the same
|
|
2664
|
+
return false;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
// Perform full sync. Only way of getting flag changes from non-CONDSTORE servers
|
|
2668
|
+
return await this.fullSync();
|
|
2669
|
+
} catch (err) {
|
|
2670
|
+
if (err.mailboxMissing) {
|
|
2671
|
+
// this mailbox is missing, refresh listing
|
|
2672
|
+
try {
|
|
2673
|
+
await this.connection.getCurrentListing();
|
|
2674
|
+
} catch (E) {
|
|
2675
|
+
this.logger.error({ msg: 'Missing mailbox', err, E });
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
throw err;
|
|
2679
|
+
} finally {
|
|
2680
|
+
// Send new mailbox notification if this is the first sync
|
|
2681
|
+
if (this.listingEntry.isNew) {
|
|
2682
|
+
// fully synced, so not new anymore
|
|
2683
|
+
this.listingEntry.isNew = false;
|
|
2684
|
+
this.logger.debug({ msg: 'New mailbox', path: this.listingEntry.path });
|
|
2685
|
+
this.connection.notify(this, MAILBOX_NEW_NOTIFY, {
|
|
2686
|
+
path: this.listingEntry.path,
|
|
2687
|
+
name: this.listingEntry.name,
|
|
2688
|
+
specialUse: this.listingEntry.specialUse || false,
|
|
2689
|
+
uidValidity: mailboxStatus.uidValidity.toString()
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
// Resolve sync promise or start IDLE
|
|
2694
|
+
if (this.synced) {
|
|
2695
|
+
this.synced();
|
|
2696
|
+
} else {
|
|
2697
|
+
await this.select();
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
/**
|
|
2703
|
+
* Called when mailbox is closed/deselected
|
|
2704
|
+
*/
|
|
2705
|
+
async onClose() {
|
|
2706
|
+
clearTimeout(this.runPartialSyncTimer);
|
|
2707
|
+
this.selected = false;
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
// User methods
|
|
2711
|
+
// Call `clearTimeout(this.connection.completedTimer);` after locking mailbox
|
|
2712
|
+
// Call this.onTaskCompleted() after selected mailbox is processed and lock is released
|
|
2713
|
+
|
|
2714
|
+
/**
|
|
2715
|
+
* Fetches text content for a message
|
|
2716
|
+
* @param {Object} message - Message object with uid
|
|
2717
|
+
* @param {Array} textParts - Array of body part numbers to fetch
|
|
2718
|
+
* @param {Object} options - Fetch options
|
|
2719
|
+
* @param {Object} connectionOptions - Connection options
|
|
2720
|
+
* @returns {Object} Text content by type (plain, html, etc)
|
|
2721
|
+
*/
|
|
2722
|
+
async getText(message, textParts, options, connectionOptions) {
|
|
2723
|
+
options = options || {};
|
|
2724
|
+
|
|
2725
|
+
const connectionClient = await this.connection.getImapConnection(connectionOptions);
|
|
2726
|
+
|
|
2727
|
+
let result = {};
|
|
2728
|
+
|
|
2729
|
+
let maxBytes = options.maxBytes || Infinity;
|
|
2730
|
+
// Request extra bytes to ensure complete UTF-8 sequences
|
|
2731
|
+
let reqMaxBytes = options.maxBytes && !isNaN(options.maxBytes) ? Number(options.maxBytes) + 4 : maxBytes;
|
|
2732
|
+
|
|
2733
|
+
let hasMore = false;
|
|
2734
|
+
|
|
2735
|
+
let lock;
|
|
2736
|
+
if (!options.skipLock) {
|
|
2737
|
+
lock = await this.getMailboxLock(connectionClient, { description: `Get text: ${message.uid}` });
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
try {
|
|
2741
|
+
for (let part of textParts) {
|
|
2742
|
+
let { meta, content } = await connectionClient.download(message.uid, part, {
|
|
2743
|
+
uid: true,
|
|
2744
|
+
// make sure we request enough bytes so we would have complete utf-8 codepoints
|
|
2745
|
+
maxBytes: Math.min(reqMaxBytes, MAX_ALLOWED_DOWNLOAD_SIZE),
|
|
2746
|
+
chunkSize: options.chunkSize
|
|
2747
|
+
});
|
|
2748
|
+
|
|
2749
|
+
if (!content) {
|
|
2750
|
+
continue;
|
|
2751
|
+
}
|
|
2752
|
+
let text = await download(content);
|
|
2753
|
+
text = text.toString().replace(/\r?\n/g, '\n');
|
|
2754
|
+
|
|
2755
|
+
// Group by content type (plain, html, etc)
|
|
2756
|
+
let typeKey = (meta.contentType && meta.contentType.split('/')[1]) || 'plain';
|
|
2757
|
+
if (!result[typeKey]) {
|
|
2758
|
+
result[typeKey] = [];
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
// Check size limits
|
|
2762
|
+
let typeSize = result[typeKey].reduce((sum, entry) => sum + entry.length, 0);
|
|
2763
|
+
if (typeSize >= maxBytes) {
|
|
2764
|
+
hasMore = true;
|
|
2765
|
+
continue;
|
|
2766
|
+
}
|
|
2767
|
+
if (typeSize + text.length > maxBytes) {
|
|
2768
|
+
text = text.substr(0, maxBytes - typeSize);
|
|
2769
|
+
hasMore = true;
|
|
2770
|
+
}
|
|
2771
|
+
result[typeKey].push(text);
|
|
2772
|
+
}
|
|
2773
|
+
} finally {
|
|
2774
|
+
if (lock) {
|
|
2775
|
+
lock.release();
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
// Join multiple parts of same type
|
|
2780
|
+
Object.keys(result).forEach(key => {
|
|
2781
|
+
result[key] = result[key].join('\n');
|
|
2782
|
+
});
|
|
2783
|
+
|
|
2784
|
+
result.hasMore = hasMore;
|
|
2785
|
+
|
|
2786
|
+
if (!options.skipLock) {
|
|
2787
|
+
this.onTaskCompleted(connectionClient);
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
return result;
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
/**
|
|
2794
|
+
* Downloads an attachment
|
|
2795
|
+
* @param {Object} message - Message object with uid
|
|
2796
|
+
* @param {String} part - Body part number
|
|
2797
|
+
* @param {Object} options - Download options
|
|
2798
|
+
* @param {Object} connectionOptions - Connection options
|
|
2799
|
+
* @returns {Stream} Readable stream of attachment content
|
|
2800
|
+
*/
|
|
2801
|
+
async getAttachment(message, part, options, connectionOptions) {
|
|
2802
|
+
options = options || {};
|
|
2803
|
+
|
|
2804
|
+
const connectionClient = await this.connection.getImapConnection(connectionOptions);
|
|
2805
|
+
|
|
2806
|
+
let lock = await this.getMailboxLock(connectionClient, { description: `Get attachment: ${message.uid}/${part}` });
|
|
2807
|
+
|
|
2808
|
+
let streaming = false;
|
|
2809
|
+
let released = false;
|
|
2810
|
+
try {
|
|
2811
|
+
let { meta, content } = await connectionClient.download(message.uid, part, {
|
|
2812
|
+
uid: true,
|
|
2813
|
+
maxBytes: Math.min(options.maxBytes || 0, MAX_ALLOWED_DOWNLOAD_SIZE),
|
|
2814
|
+
chunkSize: options.chunkSize
|
|
2815
|
+
});
|
|
2816
|
+
|
|
2817
|
+
if (!meta) {
|
|
2818
|
+
return false;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
// Build content-disposition header with proper encoding
|
|
2822
|
+
let filenameParam = '';
|
|
2823
|
+
if (meta.filename) {
|
|
2824
|
+
let isCleartextFilename = meta.filename && /^[a-z0-9 _\-()^[\]~=,+*$]+$/i.test(meta.filename);
|
|
2825
|
+
if (isCleartextFilename) {
|
|
2826
|
+
filenameParam = `; filename=${JSON.stringify(meta.filename)}`;
|
|
2827
|
+
} else {
|
|
2828
|
+
// Use RFC 2231 encoding for non-ASCII filenames
|
|
2829
|
+
filenameParam = `; filename=${JSON.stringify(he.encode(meta.filename))}; filename*=utf-8''${encodeURIComponent(meta.filename)}`;
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
// Add HTTP headers to stream
|
|
2834
|
+
content.headers = {
|
|
2835
|
+
'content-type': meta.contentType || 'application/octet-stream',
|
|
2836
|
+
'content-disposition': 'attachment' + filenameParam
|
|
2837
|
+
};
|
|
2838
|
+
|
|
2839
|
+
content.contentType = meta.contentType;
|
|
2840
|
+
content.filename = meta.filename;
|
|
2841
|
+
content.disposition = meta.disposition;
|
|
2842
|
+
streaming = true;
|
|
2843
|
+
|
|
2844
|
+
// Release lock when stream ends
|
|
2845
|
+
content.once('end', () => {
|
|
2846
|
+
if (!released) {
|
|
2847
|
+
released = true;
|
|
2848
|
+
lock.release();
|
|
2849
|
+
}
|
|
2850
|
+
});
|
|
2851
|
+
|
|
2852
|
+
content.once('error', () => {
|
|
2853
|
+
if (!released) {
|
|
2854
|
+
released = true;
|
|
2855
|
+
lock.release();
|
|
2856
|
+
this.connection.onTaskCompleted(connectionClient);
|
|
2857
|
+
}
|
|
2858
|
+
});
|
|
2859
|
+
|
|
2860
|
+
return content;
|
|
2861
|
+
} finally {
|
|
2862
|
+
if (!streaming) {
|
|
2863
|
+
lock.release();
|
|
2864
|
+
this.connection.onTaskCompleted(connectionClient);
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
/**
|
|
2870
|
+
* Fetches complete message details
|
|
2871
|
+
* @param {Object} message - Message object with uid
|
|
2872
|
+
* @param {Object} options - Fetch options
|
|
2873
|
+
* @param {Object} connectionOptions - Connection options
|
|
2874
|
+
* @returns {Object} Complete message information
|
|
2875
|
+
*/
|
|
2876
|
+
async getMessage(message, options, connectionOptions) {
|
|
2877
|
+
options = options || {};
|
|
2878
|
+
|
|
2879
|
+
let messageInfo;
|
|
2880
|
+
|
|
2881
|
+
const connectionClient = await this.connection.getImapConnection(connectionOptions);
|
|
2882
|
+
|
|
2883
|
+
try {
|
|
2884
|
+
let lock;
|
|
2885
|
+
if (!options.skipLock) {
|
|
2886
|
+
lock = await this.getMailboxLock(connectionClient, { description: `Get message: ${message.uid}` });
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
try {
|
|
2890
|
+
// Configure which fields to fetch
|
|
2891
|
+
let fields = options.fields || {
|
|
2892
|
+
uid: true,
|
|
2893
|
+
flags: true,
|
|
2894
|
+
size: true,
|
|
2895
|
+
bodyStructure: true,
|
|
2896
|
+
envelope: true,
|
|
2897
|
+
internalDate: true,
|
|
2898
|
+
headers: 'headers' in options ? options.headers : true,
|
|
2899
|
+
emailId: true,
|
|
2900
|
+
threadId: true,
|
|
2901
|
+
labels: true
|
|
2902
|
+
};
|
|
2903
|
+
|
|
2904
|
+
let messageData = await connectionClient.fetchOne(message.uid, fields, { uid: true });
|
|
2905
|
+
|
|
2906
|
+
// Mark as seen if requested
|
|
2907
|
+
if (options.markAsSeen && (!messageData.flags || !messageData.flags.has('\\Seen'))) {
|
|
2908
|
+
try {
|
|
2909
|
+
let res = await connectionClient.messageFlagsAdd(message.uid, ['\\Seen'], { uid: true });
|
|
2910
|
+
if (res) {
|
|
2911
|
+
messageData.flags.add('\\Seen');
|
|
2912
|
+
}
|
|
2913
|
+
} catch (err) {
|
|
2914
|
+
this.logger.debug({ msg: 'Failed to mark message as Seen', message: message.uid, err });
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
if (!messageData || !messageData.uid) {
|
|
2919
|
+
//TODO: support partial responses
|
|
2920
|
+
this.logger.debug({ msg: 'Partial FETCH response', code: 'partial_fetch', query: { range: message.uid, fields, opts: { uid: true } } });
|
|
2921
|
+
return false;
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
messageInfo = await this.getMessageInfo(messageData, true);
|
|
2925
|
+
} finally {
|
|
2926
|
+
if (lock) {
|
|
2927
|
+
lock.release();
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
if (!messageInfo) {
|
|
2932
|
+
return false;
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
// Merge decoded text content with message data (if requested)
|
|
2936
|
+
if (options.textType && messageInfo.text && messageInfo.text.id) {
|
|
2937
|
+
let { textParts } = await this.connection.getMessageTextPaths(messageInfo.text.id);
|
|
2938
|
+
if (textParts && textParts.length) {
|
|
2939
|
+
// Select which text parts to fetch
|
|
2940
|
+
switch (options.textType) {
|
|
2941
|
+
case 'plain':
|
|
2942
|
+
textParts = textParts[0];
|
|
2943
|
+
break;
|
|
2944
|
+
case 'html':
|
|
2945
|
+
textParts = textParts[1];
|
|
2946
|
+
break;
|
|
2947
|
+
default:
|
|
2948
|
+
textParts = textParts.flatMap(entry => entry);
|
|
2949
|
+
break;
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
if (textParts && textParts.length) {
|
|
2953
|
+
let textContent = await this.getText(message, textParts, options, { connectionClient });
|
|
2954
|
+
if (options.textType && options.textType !== '*') {
|
|
2955
|
+
textContent = {
|
|
2956
|
+
[options.textType]: textContent[options.textType] || '',
|
|
2957
|
+
hasMore: textContent.hasMore
|
|
2958
|
+
};
|
|
2959
|
+
}
|
|
2960
|
+
messageInfo.text = Object.assign(messageInfo.text, textContent);
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
// Convert to web-safe HTML if requested
|
|
2966
|
+
if (options.preProcessHtml && messageInfo.text && (messageInfo.text.html || messageInfo.text.plain)) {
|
|
2967
|
+
messageInfo.text.html = mimeHtml({
|
|
2968
|
+
html: messageInfo.text.html,
|
|
2969
|
+
text: messageInfo.text.plain
|
|
2970
|
+
});
|
|
2971
|
+
messageInfo.text.webSafe = true;
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
// Embed attached images as data URIs if requested
|
|
2975
|
+
if (options.embedAttachedImages && messageInfo.text && messageInfo.text.html && messageInfo.attachments) {
|
|
2976
|
+
let attachmentList = new Map();
|
|
2977
|
+
let partList = [];
|
|
2978
|
+
|
|
2979
|
+
// Find images referenced by CID
|
|
2980
|
+
for (let attachment of messageInfo.attachments) {
|
|
2981
|
+
let contentId = attachment.contentId && attachment.contentId.replace(/^<|>$/g, '');
|
|
2982
|
+
if (contentId && messageInfo.text.html.indexOf(contentId) >= 0) {
|
|
2983
|
+
attachmentList.set(contentId, { attachment, content: null });
|
|
2984
|
+
|
|
2985
|
+
let buf = Buffer.from(attachment.id, 'base64url');
|
|
2986
|
+
let part = buf.subarray(8).toString();
|
|
2987
|
+
|
|
2988
|
+
Object.defineProperty(attachment, 'part', {
|
|
2989
|
+
value: part,
|
|
2990
|
+
enumerable: false
|
|
2991
|
+
});
|
|
2992
|
+
|
|
2993
|
+
if (!partList.includes(part)) {
|
|
2994
|
+
partList.push(part);
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
if (partList.length) {
|
|
3000
|
+
try {
|
|
3001
|
+
// Download all referenced images in batch
|
|
3002
|
+
let contentParts = await connectionClient.downloadMany(messageInfo.uid, partList, {
|
|
3003
|
+
uid: true
|
|
3004
|
+
});
|
|
3005
|
+
|
|
3006
|
+
if (contentParts) {
|
|
3007
|
+
for (let { attachment } of attachmentList.values()) {
|
|
3008
|
+
if (attachment.part && contentParts[attachment.part] && contentParts[attachment.part].content) {
|
|
3009
|
+
Object.defineProperty(attachment, 'content', {
|
|
3010
|
+
value: contentParts[attachment.part].content,
|
|
3011
|
+
enumerable: false
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
// Replace CID references with data URIs
|
|
3017
|
+
messageInfo.text.html = messageInfo.text.html.replace(/\bcid:([^"'\s>]+)/g, (fullMatch, cidMatch) => {
|
|
3018
|
+
if (attachmentList.has(cidMatch)) {
|
|
3019
|
+
let { attachment } = attachmentList.get(cidMatch);
|
|
3020
|
+
if (attachment.content) {
|
|
3021
|
+
return `data:${attachment.contentType || 'application/octet-stream'};base64,${attachment.content.toString('base64')}`;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
return fullMatch;
|
|
3025
|
+
});
|
|
3026
|
+
}
|
|
3027
|
+
} catch (err) {
|
|
3028
|
+
this.logger.error({ msg: 'Attachment error', uid: messageInfo.uid, partList, err });
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
// Add mailbox special use information
|
|
3034
|
+
if (this.listingEntry.specialUse) {
|
|
3035
|
+
messageInfo.specialUse = this.listingEntry.specialUse;
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
// Determine message's special use folder
|
|
3039
|
+
for (let specialUseTag of ['\\Junk', '\\Sent', '\\Trash', '\\Inbox', '\\Drafts']) {
|
|
3040
|
+
if (this.listingEntry.specialUse === specialUseTag || (messageInfo.labels && messageInfo.labels.includes(specialUseTag))) {
|
|
3041
|
+
messageInfo.messageSpecialUse = specialUseTag;
|
|
3042
|
+
break;
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
return messageInfo;
|
|
3047
|
+
} finally {
|
|
3048
|
+
if (!options.skipLock) {
|
|
3049
|
+
this.connection.onTaskCompleted(connectionClient);
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
/**
|
|
3055
|
+
* Updates message flags and labels
|
|
3056
|
+
* @param {Object} message - Message object with uid
|
|
3057
|
+
* @param {Object} updates - Updates to apply (flags, labels)
|
|
3058
|
+
* @param {Object} connectionOptions - Connection options
|
|
3059
|
+
* @returns {Object} Result of updates
|
|
3060
|
+
*/
|
|
3061
|
+
async updateMessage(message, updates, connectionOptions) {
|
|
3062
|
+
updates = updates || {};
|
|
3063
|
+
|
|
3064
|
+
const connectionClient = await this.connection.getImapConnection(connectionOptions);
|
|
3065
|
+
|
|
3066
|
+
let lock = await this.getMailboxLock(connectionClient, { description: `Update message: ${message.uid}` });
|
|
3067
|
+
|
|
3068
|
+
try {
|
|
3069
|
+
let result = {};
|
|
3070
|
+
|
|
3071
|
+
// Update flags
|
|
3072
|
+
if (updates.flags) {
|
|
3073
|
+
if (updates.flags.set) {
|
|
3074
|
+
// If set exists then ignore add/delete calls
|
|
3075
|
+
let value = await connectionClient.messageFlagsSet(message.uid, updates.flags.set, { uid: true });
|
|
3076
|
+
result.flags = {
|
|
3077
|
+
set: value
|
|
3078
|
+
};
|
|
3079
|
+
} else {
|
|
3080
|
+
if (updates.flags.add && updates.flags.add.length) {
|
|
3081
|
+
let value = await connectionClient.messageFlagsAdd(message.uid, updates.flags.add, { uid: true });
|
|
3082
|
+
if (!result.flags) {
|
|
3083
|
+
result.flags = {};
|
|
3084
|
+
}
|
|
3085
|
+
result.flags.add = value;
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
if (updates.flags.delete && updates.flags.delete.length) {
|
|
3089
|
+
let value = await connectionClient.messageFlagsRemove(message.uid, updates.flags.delete, { uid: true });
|
|
3090
|
+
if (!result.flags) {
|
|
3091
|
+
result.flags = {};
|
|
3092
|
+
}
|
|
3093
|
+
result.flags.delete = value;
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
// Update Gmail labels
|
|
3099
|
+
if (updates.labels && this.isGmail) {
|
|
3100
|
+
if (updates.labels.set) {
|
|
3101
|
+
// If set exists then ignore add/delete calls
|
|
3102
|
+
let value = await connectionClient.messageFlagsSet(message.uid, updates.labels.set, { uid: true, useLabels: true });
|
|
3103
|
+
result.labels = {
|
|
3104
|
+
set: value
|
|
3105
|
+
};
|
|
3106
|
+
} else {
|
|
3107
|
+
if (updates.labels.add && updates.labels.add.length) {
|
|
3108
|
+
let value = await connectionClient.messageFlagsAdd(message.uid, updates.labels.add, { uid: true, useLabels: true });
|
|
3109
|
+
if (!result.labels) {
|
|
3110
|
+
result.labels = {};
|
|
3111
|
+
}
|
|
3112
|
+
result.labels.add = value;
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
if (updates.labels.delete && updates.labels.delete.length) {
|
|
3116
|
+
let value = await connectionClient.messageFlagsRemove(message.uid, updates.labels.delete, { uid: true, useLabels: true });
|
|
3117
|
+
if (!result.labels) {
|
|
3118
|
+
result.labels = {};
|
|
3119
|
+
}
|
|
3120
|
+
result.labels.delete = value;
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
return result;
|
|
3126
|
+
} finally {
|
|
3127
|
+
lock.release();
|
|
3128
|
+
this.connection.onTaskCompleted(connectionClient);
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
/**
|
|
3133
|
+
* Updates multiple messages based on search criteria
|
|
3134
|
+
* @param {Object} search - Search criteria
|
|
3135
|
+
* @param {Object} updates - Updates to apply
|
|
3136
|
+
* @param {Object} connectionOptions - Connection options
|
|
3137
|
+
* @returns {Object} Result of updates
|
|
3138
|
+
*/
|
|
3139
|
+
async updateMessages(search, updates, connectionOptions) {
|
|
3140
|
+
updates = updates || {};
|
|
3141
|
+
|
|
3142
|
+
const connectionClient = await this.connection.getImapConnection(connectionOptions);
|
|
3143
|
+
|
|
3144
|
+
let lock = await this.getMailboxLock(connectionClient, { description: `Update messages` });
|
|
3145
|
+
|
|
3146
|
+
try {
|
|
3147
|
+
let result = {};
|
|
3148
|
+
|
|
3149
|
+
// Update flags for matching messages
|
|
3150
|
+
if (updates.flags) {
|
|
3151
|
+
if (updates.flags.set) {
|
|
3152
|
+
// If set exists then ignore add/delete calls
|
|
3153
|
+
let value = await connectionClient.messageFlagsSet(search, updates.flags.set, { uid: true });
|
|
3154
|
+
result.flags = {
|
|
3155
|
+
set: value
|
|
3156
|
+
};
|
|
3157
|
+
} else {
|
|
3158
|
+
if (updates.flags.add && updates.flags.add.length) {
|
|
3159
|
+
let value = await connectionClient.messageFlagsAdd(search, updates.flags.add, { uid: true });
|
|
3160
|
+
if (!result.flags) {
|
|
3161
|
+
result.flags = {};
|
|
3162
|
+
}
|
|
3163
|
+
result.flags.add = value;
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
if (updates.flags.delete && updates.flags.delete.length) {
|
|
3167
|
+
let value = await connectionClient.messageFlagsRemove(search, updates.flags.delete, { uid: true });
|
|
3168
|
+
if (!result.flags) {
|
|
3169
|
+
result.flags = {};
|
|
3170
|
+
}
|
|
3171
|
+
result.flags.delete = value;
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
// Update Gmail labels for matching messages
|
|
3177
|
+
if (updates.labels && this.isGmail) {
|
|
3178
|
+
if (updates.labels.set) {
|
|
3179
|
+
// If set exists then ignore add/delete calls
|
|
3180
|
+
let value = await connectionClient.messageFlagsSet(search, updates.labels.set, { uid: true, useLabels: true });
|
|
3181
|
+
result.labels = {
|
|
3182
|
+
set: value
|
|
3183
|
+
};
|
|
3184
|
+
} else {
|
|
3185
|
+
if (updates.labels.add && updates.labels.add.length) {
|
|
3186
|
+
let value = await connectionClient.messageFlagsAdd(search, updates.labels.add, { uid: true, useLabels: true });
|
|
3187
|
+
if (!result.labels) {
|
|
3188
|
+
result.labels = {};
|
|
3189
|
+
}
|
|
3190
|
+
result.labels.add = value;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
if (updates.labels.delete && updates.labels.delete.length) {
|
|
3194
|
+
let value = await connectionClient.messageFlagsRemove(search, updates.labels.delete, { uid: true, useLabels: true });
|
|
3195
|
+
if (!result.labels) {
|
|
3196
|
+
result.labels = {};
|
|
3197
|
+
}
|
|
3198
|
+
result.labels.delete = value;
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
return result;
|
|
3204
|
+
} finally {
|
|
3205
|
+
lock.release();
|
|
3206
|
+
this.connection.onTaskCompleted(connectionClient);
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
/**
|
|
3211
|
+
* Moves a message to another mailbox
|
|
3212
|
+
* @param {Object} message - Message object with uid
|
|
3213
|
+
* @param {Object} target - Target mailbox with path
|
|
3214
|
+
* @param {Object} options - Move options
|
|
3215
|
+
* @param {Object} connectionOptions - Connection options
|
|
3216
|
+
* @returns {Object} Result with new message ID and UID
|
|
3217
|
+
*/
|
|
3218
|
+
async moveMessage(message, target, options, connectionOptions) {
|
|
3219
|
+
target = target || {};
|
|
3220
|
+
|
|
3221
|
+
const connectionClient = await this.connection.getImapConnection(connectionOptions);
|
|
3222
|
+
|
|
3223
|
+
let lock = await this.getMailboxLock(connectionClient, { description: `Move message: ${message.uid} to: ${target.path}` });
|
|
3224
|
+
|
|
3225
|
+
try {
|
|
3226
|
+
let result = {};
|
|
3227
|
+
|
|
3228
|
+
if (target.path) {
|
|
3229
|
+
// Perform the move
|
|
3230
|
+
let value = await connectionClient.messageMove(message.uid, target.path, { uid: true });
|
|
3231
|
+
result.path = target.path;
|
|
3232
|
+
|
|
3233
|
+
// Get new UID in target mailbox
|
|
3234
|
+
if (value && value.uidMap && value.uidMap.has(message.uid)) {
|
|
3235
|
+
let uid = value.uidMap.get(message.uid);
|
|
3236
|
+
let packed = await this.connection.packUid(target.path, uid);
|
|
3237
|
+
result.id = packed;
|
|
3238
|
+
result.uid = uid;
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
return result;
|
|
3243
|
+
} finally {
|
|
3244
|
+
lock.release();
|
|
3245
|
+
this.connection.onTaskCompleted(connectionClient);
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
/**
|
|
3250
|
+
* Moves multiple messages to another mailbox
|
|
3251
|
+
* @param {Object} search - Search criteria
|
|
3252
|
+
* @param {Object} target - Target mailbox with path
|
|
3253
|
+
* @param {Object} connectionOptions - Connection options
|
|
3254
|
+
* @returns {Object} Result with ID mappings
|
|
3255
|
+
*/
|
|
3256
|
+
async moveMessages(search, target, connectionOptions) {
|
|
3257
|
+
target = target || {};
|
|
3258
|
+
|
|
3259
|
+
const connectionClient = await this.connection.getImapConnection(connectionOptions);
|
|
3260
|
+
|
|
3261
|
+
let lock = await this.getMailboxLock(connectionClient, { description: `Move messages to: ${target.path}` });
|
|
3262
|
+
|
|
3263
|
+
try {
|
|
3264
|
+
let result = {};
|
|
3265
|
+
|
|
3266
|
+
if (target.path) {
|
|
3267
|
+
// Perform the move
|
|
3268
|
+
let value = await connectionClient.messageMove(search, target.path, { uid: true });
|
|
3269
|
+
result.path = target.path;
|
|
3270
|
+
|
|
3271
|
+
// Build ID map for moved messages
|
|
3272
|
+
if (value && value.uidMap && value.uidMap.size) {
|
|
3273
|
+
let moveMap = [];
|
|
3274
|
+
for (let [suid, tuid] of value.uidMap) {
|
|
3275
|
+
moveMap.push([await this.connection.packUid(this.path, suid), await this.connection.packUid(target.path, tuid)]);
|
|
3276
|
+
}
|
|
3277
|
+
result.idMap = moveMap;
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
return result;
|
|
3282
|
+
} finally {
|
|
3283
|
+
lock.release();
|
|
3284
|
+
this.connection.onTaskCompleted(connectionClient);
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
/**
|
|
3289
|
+
* Deletes a message or moves it to trash
|
|
3290
|
+
* @param {Object} message - Message object with uid
|
|
3291
|
+
* @param {Boolean} force - Force permanent deletion
|
|
3292
|
+
* @param {Object} connectionOptions - Connection options
|
|
3293
|
+
* @returns {Object} Result of deletion
|
|
3294
|
+
*/
|
|
3295
|
+
async deleteMessage(message, force, connectionOptions) {
|
|
3296
|
+
const connectionClient = await this.connection.getImapConnection(connectionOptions);
|
|
3297
|
+
|
|
3298
|
+
let lock = await this.getMailboxLock(connectionClient, { description: `Delete message: ${message.uid}` });
|
|
3299
|
+
|
|
3300
|
+
try {
|
|
3301
|
+
let result = {};
|
|
3302
|
+
|
|
3303
|
+
// Permanently delete if in Trash/Junk or forced
|
|
3304
|
+
if (['\\Trash', '\\Junk'].includes(this.listingEntry.specialUse) || force) {
|
|
3305
|
+
// delete
|
|
3306
|
+
result.deleted = await connectionClient.messageDelete(message.uid, { uid: true });
|
|
3307
|
+
} else {
|
|
3308
|
+
// Move to trash
|
|
3309
|
+
// Find Trash folder path
|
|
3310
|
+
let trashMailbox = await this.connection.getSpecialUseMailbox('\\Trash');
|
|
3311
|
+
if (!trashMailbox || normalizePath(trashMailbox.path) === normalizePath(this.path)) {
|
|
3312
|
+
// No Trash found or already in trash - delete permanently
|
|
3313
|
+
result.deleted = await connectionClient.messageDelete(message.uid, { uid: true });
|
|
3314
|
+
} else {
|
|
3315
|
+
result.deleted = false;
|
|
3316
|
+
// We have a destination, so can move message to there
|
|
3317
|
+
let moved = await connectionClient.messageMove(message.uid, trashMailbox.path, { uid: true });
|
|
3318
|
+
if (moved) {
|
|
3319
|
+
result.moved = {
|
|
3320
|
+
destination: moved.destination
|
|
3321
|
+
};
|
|
3322
|
+
if (moved && moved.uidMap && moved.uidMap.has(message.uid)) {
|
|
3323
|
+
result.moved.message = await this.connection.packUid(trashMailbox.path, moved.uidMap.get(message.uid));
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
return result;
|
|
3330
|
+
} finally {
|
|
3331
|
+
lock.release();
|
|
3332
|
+
this.connection.onTaskCompleted(connectionClient);
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
/**
|
|
3337
|
+
* Deletes multiple messages or moves them to trash
|
|
3338
|
+
* @param {Object} search - Search criteria
|
|
3339
|
+
* @param {Boolean} force - Force permanent deletion
|
|
3340
|
+
* @param {Object} connectionOptions - Connection options
|
|
3341
|
+
* @returns {Object} Result of deletion
|
|
3342
|
+
*/
|
|
3343
|
+
async deleteMessages(search, force, connectionOptions) {
|
|
3344
|
+
const connectionClient = await this.connection.getImapConnection(connectionOptions);
|
|
3345
|
+
|
|
3346
|
+
let lock = await this.getMailboxLock(connectionClient, { description: `Delete messages` });
|
|
3347
|
+
|
|
3348
|
+
try {
|
|
3349
|
+
let result = {};
|
|
3350
|
+
|
|
3351
|
+
// Permanently delete if in Trash/Junk or forced
|
|
3352
|
+
if (['\\Trash', '\\Junk'].includes(this.listingEntry.specialUse) || force) {
|
|
3353
|
+
// delete
|
|
3354
|
+
result.deleted = await connectionClient.messageDelete(search, { uid: true });
|
|
3355
|
+
} else {
|
|
3356
|
+
// Move to trash
|
|
3357
|
+
// Find Trash folder path
|
|
3358
|
+
let trashMailbox = await this.connection.getSpecialUseMailbox('\\Trash');
|
|
3359
|
+
if (!trashMailbox || normalizePath(trashMailbox.path) === normalizePath(this.path)) {
|
|
3360
|
+
// No Trash found or already in trash - delete permanently
|
|
3361
|
+
result.deleted = await connectionClient.messageDelete(search, { uid: true });
|
|
3362
|
+
} else {
|
|
3363
|
+
result.deleted = false;
|
|
3364
|
+
// We have a destination, so can move messages to there
|
|
3365
|
+
let moved = await connectionClient.messageMove(search, trashMailbox.path, { uid: true });
|
|
3366
|
+
if (moved) {
|
|
3367
|
+
result.moved = {
|
|
3368
|
+
destination: moved.destination
|
|
3369
|
+
};
|
|
3370
|
+
if (moved && moved.uidMap && moved.uidMap.size) {
|
|
3371
|
+
let moveMap = [];
|
|
3372
|
+
for (let [suid, tuid] of moved.uidMap) {
|
|
3373
|
+
moveMap.push([await this.connection.packUid(this.path, suid), await this.connection.packUid(trashMailbox.path, tuid)]);
|
|
3374
|
+
}
|
|
3375
|
+
result.moved.idMap = moveMap;
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
return result;
|
|
3382
|
+
} finally {
|
|
3383
|
+
lock.release();
|
|
3384
|
+
this.connection.onTaskCompleted(connectionClient);
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
/**
|
|
3389
|
+
* Lists messages in the mailbox with pagination
|
|
3390
|
+
* @param {Object} options - List options
|
|
3391
|
+
* @param {Number} options.page - Page number (0-based)
|
|
3392
|
+
* @param {String} options.cursor - Cursor string for pagination
|
|
3393
|
+
* @param {Number} options.pageSize - Messages per page
|
|
3394
|
+
* @param {Object} options.search - Search criteria
|
|
3395
|
+
* @param {Object} connectionOptions - Connection options
|
|
3396
|
+
* @returns {Object} Paginated message list
|
|
3397
|
+
*/
|
|
3398
|
+
async listMessages(options, connectionOptions) {
|
|
3399
|
+
options = options || {};
|
|
3400
|
+
|
|
3401
|
+
let page = Number(options.page) || 0;
|
|
3402
|
+
|
|
3403
|
+
// Handle cursor-based pagination
|
|
3404
|
+
if (options.cursor) {
|
|
3405
|
+
let cursorPage = this.decodeCursorStr(options.cursor);
|
|
3406
|
+
if (typeof cursorPage === 'number' && cursorPage >= 0) {
|
|
3407
|
+
page = cursorPage;
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
let pageSize = Math.abs(Number(options.pageSize) || 20);
|
|
3412
|
+
|
|
3413
|
+
const connectionClient = await this.connection.getImapConnection(connectionOptions);
|
|
3414
|
+
|
|
3415
|
+
let lock = await this.getMailboxLock(connectionClient, { description: `List messages from: ${this.path}` });
|
|
3416
|
+
|
|
3417
|
+
try {
|
|
3418
|
+
let mailboxStatus = this.getMailboxStatus(connectionClient);
|
|
3419
|
+
|
|
3420
|
+
let messageCount = mailboxStatus.messages;
|
|
3421
|
+
let uidList;
|
|
3422
|
+
let opts = {};
|
|
3423
|
+
|
|
3424
|
+
// Apply search filter if provided
|
|
3425
|
+
if (options.search) {
|
|
3426
|
+
uidList = await connectionClient.search(options.search, { uid: true });
|
|
3427
|
+
uidList = !uidList ? [] : uidList.sort((a, b) => b - a); // newer first
|
|
3428
|
+
messageCount = uidList.length;
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
// Calculate pagination
|
|
3432
|
+
let pages = Math.ceil(messageCount / pageSize) || 1;
|
|
3433
|
+
|
|
3434
|
+
if (page < 0) {
|
|
3435
|
+
page = 0;
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
let messages = [];
|
|
3439
|
+
let seqMax, seqMin, range;
|
|
3440
|
+
|
|
3441
|
+
// Generate pagination cursors
|
|
3442
|
+
let nextPageCursor = page < pages - 1 ? this.encodeCursorString(page + 1) : null;
|
|
3443
|
+
let prevPageCursor = page > 0 ? this.encodeCursorString(Math.min(page - 1, pages - 1)) : null;
|
|
3444
|
+
|
|
3445
|
+
// Return empty result if no messages or page out of bounds
|
|
3446
|
+
if (!messageCount || page >= pages) {
|
|
3447
|
+
return {
|
|
3448
|
+
total: messageCount,
|
|
3449
|
+
page,
|
|
3450
|
+
pages,
|
|
3451
|
+
nextPageCursor,
|
|
3452
|
+
prevPageCursor,
|
|
3453
|
+
messages
|
|
3454
|
+
};
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
// Calculate range to fetch
|
|
3458
|
+
if (options.search && uidList) {
|
|
3459
|
+
// For search results, use specific UIDs
|
|
3460
|
+
let start = page * pageSize;
|
|
3461
|
+
let uidRange = uidList.slice(start, start + pageSize).reverse();
|
|
3462
|
+
range = uidRange.join(',');
|
|
3463
|
+
opts.uid = true;
|
|
3464
|
+
} else {
|
|
3465
|
+
// For full listing, use sequence range
|
|
3466
|
+
seqMax = messageCount - page * pageSize;
|
|
3467
|
+
seqMin = seqMax - pageSize + 1;
|
|
3468
|
+
|
|
3469
|
+
if (seqMax >= messageCount) {
|
|
3470
|
+
seqMax = '*';
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
if (seqMin < 1) {
|
|
3474
|
+
seqMin = 1;
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
range = seqMin === seqMax ? `${seqMin}` : `${seqMin}:${seqMax}`;
|
|
3478
|
+
}
|
|
3479
|
+
|
|
3480
|
+
// Configure fields to fetch
|
|
3481
|
+
let fields = {
|
|
3482
|
+
uid: true,
|
|
3483
|
+
flags: true,
|
|
3484
|
+
size: true,
|
|
3485
|
+
bodyStructure: true,
|
|
3486
|
+
envelope: true,
|
|
3487
|
+
internalDate: true,
|
|
3488
|
+
emailId: true,
|
|
3489
|
+
threadId: true,
|
|
3490
|
+
labels: true
|
|
3491
|
+
};
|
|
3492
|
+
|
|
3493
|
+
// LarkSuite specific handling - ensure address headers are fetched
|
|
3494
|
+
if (this.isLarkSuite) {
|
|
3495
|
+
if (!fields.headers && fields.headers !== true) {
|
|
3496
|
+
fields.headers = [];
|
|
3497
|
+
}
|
|
3498
|
+
if (Array.isArray(fields.headers)) {
|
|
3499
|
+
// ensure that the response includes header fields because Lark Mail ENVELOPE response is unreliable
|
|
3500
|
+
for (let key of ['from', 'to', 'cc']) {
|
|
3501
|
+
if (!fields.headers.includes(key)) {
|
|
3502
|
+
fields.headers.push(key);
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
// Fetch messages in the range
|
|
3509
|
+
for await (let messageData of connectionClient.fetch(range, fields, opts)) {
|
|
3510
|
+
if (!messageData || !messageData.uid) {
|
|
3511
|
+
//TODO: support partial responses
|
|
3512
|
+
this.logger.debug({ msg: 'Partial FETCH response', code: 'partial_fetch', query: { range, fields, opts } });
|
|
3513
|
+
continue;
|
|
3514
|
+
}
|
|
3515
|
+
let messageInfo;
|
|
3516
|
+
try {
|
|
3517
|
+
messageInfo = await this.getMessageInfo(messageData);
|
|
3518
|
+
} catch (err) {
|
|
3519
|
+
// Return error info for failed messages
|
|
3520
|
+
messageInfo = {
|
|
3521
|
+
uid: messageData.uid,
|
|
3522
|
+
status: 'failed',
|
|
3523
|
+
error: `Failed to process message entry ${err.message}`
|
|
3524
|
+
};
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
messages.push(messageInfo);
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
return {
|
|
3531
|
+
total: messageCount,
|
|
3532
|
+
page,
|
|
3533
|
+
pages,
|
|
3534
|
+
nextPageCursor,
|
|
3535
|
+
prevPageCursor,
|
|
3536
|
+
// List newer entries first. Servers like yahoo do not return ordered list, so we need to order manually
|
|
3537
|
+
messages: messages.sort((a, b) => b.uid - a.uid)
|
|
3538
|
+
};
|
|
3539
|
+
} finally {
|
|
3540
|
+
lock.release();
|
|
3541
|
+
this.connection.onTaskCompleted(connectionClient);
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
/**
|
|
3546
|
+
* Updates the last sync timestamp in Redis
|
|
3547
|
+
*/
|
|
3548
|
+
async markUpdated() {
|
|
3549
|
+
try {
|
|
3550
|
+
await this.connection.redis.hSetExists(this.connection.getAccountKey(), 'sync', new Date().toISOString());
|
|
3551
|
+
} catch (err) {
|
|
3552
|
+
this.logger.error({ msg: 'Redis error', err });
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
/**
|
|
3557
|
+
* Heuristic check if message might be a bounce
|
|
3558
|
+
* @param {Object} messageInfo - Message information
|
|
3559
|
+
* @returns {Boolean} True if likely a bounce
|
|
3560
|
+
*/
|
|
3561
|
+
mightBeABounce(messageInfo) {
|
|
3562
|
+
// Only check messages in Inbox or Junk
|
|
3563
|
+
if (
|
|
3564
|
+
!['\\Inbox', '\\Junk'].includes(this.listingEntry.specialUse) &&
|
|
3565
|
+
!(messageInfo.labels?.includes('\\Inbox') || messageInfo.labels?.includes('\\Junk'))
|
|
3566
|
+
) {
|
|
3567
|
+
return false;
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
// Skip if already identified as delivery report
|
|
3571
|
+
if (messageInfo.deliveryReport) {
|
|
3572
|
+
// already processed
|
|
3573
|
+
return false;
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
let name = (messageInfo.from && messageInfo.from.name) || '';
|
|
3577
|
+
let address = (messageInfo.from && messageInfo.from.address) || '';
|
|
3578
|
+
|
|
3579
|
+
// Check common bounce sender names
|
|
3580
|
+
if (/Mail Delivery System|Mail Delivery Subsystem|Internet Mail Delivery/i.test(name)) {
|
|
3581
|
+
return true;
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
// Check common bounce sender addresses
|
|
3585
|
+
if (/mailer-daemon@|postmaster@/i.test(address)) {
|
|
3586
|
+
return true;
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
// Check for delivery-status attachment + subject pattern
|
|
3590
|
+
let hasDeliveryStatus = false;
|
|
3591
|
+
for (let attachment of messageInfo.attachments || []) {
|
|
3592
|
+
if (attachment.contentType === 'message/delivery-status') {
|
|
3593
|
+
hasDeliveryStatus = true;
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
if (hasDeliveryStatus && /Undeliver(able|ed)/i.test(messageInfo.subject)) {
|
|
3598
|
+
return true;
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
return false;
|
|
3602
|
+
}
|
|
3603
|
+
|
|
3604
|
+
/**
|
|
3605
|
+
* Heuristic check if message might be an ARF complaint
|
|
3606
|
+
* @param {Object} messageInfo - Message information
|
|
3607
|
+
* @returns {Boolean} True if likely a complaint
|
|
3608
|
+
*/
|
|
3609
|
+
mightBeAComplaint(messageInfo) {
|
|
3610
|
+
// Only check inbox messages
|
|
3611
|
+
if (this.path !== 'INBOX' && !(this.isAllMail && messageInfo.labels && messageInfo.labels.includes('\\Inbox'))) {
|
|
3612
|
+
return false;
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
let hasEmbeddedMessage = false;
|
|
3616
|
+
for (let attachment of messageInfo.attachments || []) {
|
|
3617
|
+
// Direct ARF indicator
|
|
3618
|
+
if (attachment.contentType === 'message/feedback-report') {
|
|
3619
|
+
return true;
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
// Check for embedded message (complaint might contain original)
|
|
3623
|
+
if (['message/rfc822', 'message/rfc822-headers'].includes(attachment.contentType)) {
|
|
3624
|
+
hasEmbeddedMessage = true;
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
let fromAddress = (messageInfo.from && messageInfo.from.address) || '';
|
|
3629
|
+
|
|
3630
|
+
// Hotmail-specific complaint pattern
|
|
3631
|
+
if (hasEmbeddedMessage && fromAddress === 'staff@hotmail.com' && /complaint/i.test(messageInfo.subject)) {
|
|
3632
|
+
return true;
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
return false;
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
/**
|
|
3639
|
+
* Heuristic check if message might be a DSN (Delivery Status Notification)
|
|
3640
|
+
* @param {Object} messageInfo - Message information
|
|
3641
|
+
* @returns {Boolean} True if likely a DSN
|
|
3642
|
+
*/
|
|
3643
|
+
mightBeDSNResponse(messageInfo) {
|
|
3644
|
+
// Only check inbox messages
|
|
3645
|
+
if (this.path !== 'INBOX' && !(this.isAllMail && messageInfo.labels && messageInfo.labels.includes('\\Inbox'))) {
|
|
3646
|
+
return false;
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
// Check Content-Type header for multipart/report with delivery-status
|
|
3650
|
+
if (messageInfo.headers && messageInfo.headers['content-type'] && messageInfo.headers['content-type'].length) {
|
|
3651
|
+
let parsedContentType = libmime.parseHeaderValue(messageInfo.headers['content-type'].at(-1));
|
|
3652
|
+
if (
|
|
3653
|
+
parsedContentType &&
|
|
3654
|
+
parsedContentType.value &&
|
|
3655
|
+
parsedContentType.value.toLowerCase().trim() === 'multipart/report' &&
|
|
3656
|
+
parsedContentType.params['report-type'] === 'delivery-status'
|
|
3657
|
+
) {
|
|
3658
|
+
return true;
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3662
|
+
return false;
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
/**
|
|
3666
|
+
* Decodes a cursor string for pagination
|
|
3667
|
+
* @param {String} cursorStr - Base64-encoded cursor string
|
|
3668
|
+
* @returns {Number|null} Page number or null if invalid
|
|
3669
|
+
*/
|
|
3670
|
+
decodeCursorStr(cursorStr) {
|
|
3671
|
+
let type = 'imap';
|
|
3672
|
+
|
|
3673
|
+
if (cursorStr) {
|
|
3674
|
+
// Extract cursor type prefix
|
|
3675
|
+
let splitPos = cursorStr.indexOf('_');
|
|
3676
|
+
if (splitPos >= 0) {
|
|
3677
|
+
let cursorType = cursorStr.substring(0, splitPos);
|
|
3678
|
+
cursorStr = cursorStr.substring(splitPos + 1);
|
|
3679
|
+
if (cursorType && type !== cursorType) {
|
|
3680
|
+
let error = new Error('Invalid cursor');
|
|
3681
|
+
error.code = 'InvalidCursorType';
|
|
3682
|
+
throw error;
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
try {
|
|
3687
|
+
// Decode cursor data
|
|
3688
|
+
let { page: cursorPage } = JSON.parse(Buffer.from(cursorStr, 'base64url'));
|
|
3689
|
+
if (typeof cursorPage === 'number' && cursorPage >= 0) {
|
|
3690
|
+
return cursorPage;
|
|
3691
|
+
}
|
|
3692
|
+
} catch (err) {
|
|
3693
|
+
this.logger.error({ msg: 'Cursor parsing error', cursorStr, err });
|
|
3694
|
+
|
|
3695
|
+
let error = new Error('Invalid paging cursor');
|
|
3696
|
+
error.code = 'InvalidCursorValue';
|
|
3697
|
+
error.statusCode = 400;
|
|
3698
|
+
throw error;
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
return null;
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
/**
|
|
3706
|
+
* Encodes a page number into a cursor string
|
|
3707
|
+
* @param {Number} cursorPage - Page number to encode
|
|
3708
|
+
* @returns {String|null} Base64-encoded cursor string
|
|
3709
|
+
*/
|
|
3710
|
+
encodeCursorString(cursorPage) {
|
|
3711
|
+
if (typeof cursorPage !== 'number' || cursorPage < 0) {
|
|
3712
|
+
return null;
|
|
3713
|
+
}
|
|
3714
|
+
cursorPage = cursorPage || 0;
|
|
3715
|
+
let type = 'imap';
|
|
3716
|
+
// Prefix with type for future extensibility
|
|
3717
|
+
return `${type}_${Buffer.from(JSON.stringify({ page: cursorPage })).toString('base64url')}`;
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
module.exports.Mailbox = Mailbox;
|