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,3805 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { BaseClient, metricsMeta } = require('./base-client');
|
|
4
|
+
const { Account } = require('../account');
|
|
5
|
+
const settings = require('../settings');
|
|
6
|
+
const { oauth2Apps } = require('../oauth2-apps');
|
|
7
|
+
const getSecret = require('../get-secret');
|
|
8
|
+
const msgpack = require('msgpack5')();
|
|
9
|
+
const he = require('he');
|
|
10
|
+
const { emitChangeEvent, prepareUrl } = require('../tools');
|
|
11
|
+
const { mimeHtml } = require('@postalsys/email-text-tools');
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
const { Gateway } = require('../gateway');
|
|
14
|
+
const { detectMimeType, detectExtension } = require('nodemailer/lib/mime-funcs/mime-types');
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
REDIS_PREFIX,
|
|
18
|
+
AUTH_ERROR_NOTIFY,
|
|
19
|
+
AUTH_SUCCESS_NOTIFY,
|
|
20
|
+
EMAIL_SENT_NOTIFY,
|
|
21
|
+
OUTLOOK_EXPIRATION_TIME,
|
|
22
|
+
OUTLOOK_EXPIRATION_RENEW_TIME,
|
|
23
|
+
MESSAGE_UPDATED_NOTIFY,
|
|
24
|
+
MESSAGE_DELETED_NOTIFY,
|
|
25
|
+
MESSAGE_MISSING_NOTIFY
|
|
26
|
+
} = require('../consts');
|
|
27
|
+
|
|
28
|
+
// Maximum number of operations in a single batch request to Microsoft Graph API
|
|
29
|
+
const MAX_BATCH_SIZE = 20;
|
|
30
|
+
|
|
31
|
+
// Subscription is renewed automatically. But just in case, check once in an hour
|
|
32
|
+
const RENEW_WATCH_TTL = 60 * 60 * 1000; // 1h
|
|
33
|
+
|
|
34
|
+
/*
|
|
35
|
+
Supported operations status:
|
|
36
|
+
✅ listMessages - with paging (cursor + page nr) and search queries (no support for to/cc/bcc queries)
|
|
37
|
+
✅ getText
|
|
38
|
+
✅ getMessage
|
|
39
|
+
✅ updateMessage
|
|
40
|
+
✅ updateMessages
|
|
41
|
+
✅ listMailboxes
|
|
42
|
+
✅ moveMessage
|
|
43
|
+
✅ moveMessages
|
|
44
|
+
✅ deleteMessage
|
|
45
|
+
✅ deleteMessages
|
|
46
|
+
✅ getRawMessage
|
|
47
|
+
🟡 getQuota - not supported by MS Graph API
|
|
48
|
+
✅ createMailbox
|
|
49
|
+
✅ renameMailbox
|
|
50
|
+
✅ deleteMailbox
|
|
51
|
+
✅ getAttachment
|
|
52
|
+
✅ submitMessage
|
|
53
|
+
✅ uploadMessage - only drafts. Cannot change draft status
|
|
54
|
+
🟡 subconnections - not supported
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* OutlookClient handles email operations through Microsoft Graph API
|
|
59
|
+
* Extends BaseClient to provide OAuth2-based email access for Outlook/Office365 accounts
|
|
60
|
+
*/
|
|
61
|
+
class OutlookClient extends BaseClient {
|
|
62
|
+
constructor(account, options) {
|
|
63
|
+
super(account, options);
|
|
64
|
+
|
|
65
|
+
// Token caching to avoid repeated auth calls
|
|
66
|
+
this.cachedAccessToken = null;
|
|
67
|
+
this.cachedAccessTokenRaw = null;
|
|
68
|
+
|
|
69
|
+
// Pseudo path representing all messages across folders
|
|
70
|
+
this.path = '\\All';
|
|
71
|
+
this.listingEntry = { specialUse: '\\All' };
|
|
72
|
+
|
|
73
|
+
// Path for API calls - 'me' for current user or specific user email for delegated access
|
|
74
|
+
this.oauth2UserPath = 'me'; // can be changed to `users/${encodeURIComponent('shared@example.com')}` for delegated access
|
|
75
|
+
|
|
76
|
+
// Flags for background processing
|
|
77
|
+
this.processingHistory = null;
|
|
78
|
+
this.renewWatchTimer = null;
|
|
79
|
+
|
|
80
|
+
// MS Graph webhook subscription state (for metrics)
|
|
81
|
+
// Possible values: 'unset', 'valid', 'expired', 'failed', 'pending'
|
|
82
|
+
this.subscriptionState = 'unset';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Makes authenticated requests to Microsoft Graph API
|
|
87
|
+
* Handles token management and error responses
|
|
88
|
+
*/
|
|
89
|
+
async request(...args) {
|
|
90
|
+
let result, accessToken;
|
|
91
|
+
try {
|
|
92
|
+
accessToken = await this.getToken();
|
|
93
|
+
} catch (err) {
|
|
94
|
+
this.logger.error({ msg: 'Failed to load access token', account: this.account, err });
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let [url, method, payload, options = {}] = args;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
if (!this.oAuth2Client) {
|
|
102
|
+
await this.getClient();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
options.headers = options.headers || {};
|
|
106
|
+
|
|
107
|
+
// Build Prefer header with multiple preferences
|
|
108
|
+
// Request immutable IDs that don't change when messages are moved between folders
|
|
109
|
+
// https://learn.microsoft.com/en-us/graph/outlook-immutable-id
|
|
110
|
+
let preferValues = ['IdType="ImmutableId"'];
|
|
111
|
+
|
|
112
|
+
// If caller already set a Prefer header, merge it
|
|
113
|
+
if (options.headers.Prefer) {
|
|
114
|
+
preferValues.push(options.headers.Prefer);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
options.headers.Prefer = preferValues.join(', ');
|
|
118
|
+
|
|
119
|
+
// Construct full API URL if not already absolute
|
|
120
|
+
let apiUrl = /^https:/.test(url) ? url : new URL(`/v1.0${url}`, this.oAuth2Client.apiBase).href;
|
|
121
|
+
|
|
122
|
+
result = await this.oAuth2Client.request(accessToken, apiUrl, method, payload, options);
|
|
123
|
+
|
|
124
|
+
// Track successful API request
|
|
125
|
+
metricsMeta({ account: this.account }, this.logger, 'oauth2ApiRequest', 'inc', { status: 'success', provider: 'outlook', statusCode: '200' });
|
|
126
|
+
} catch (err) {
|
|
127
|
+
// Track failed API request
|
|
128
|
+
const statusCode = String(err.oauthRequest?.status || 0);
|
|
129
|
+
metricsMeta({ account: this.account }, this.logger, 'oauth2ApiRequest', 'inc', { status: 'failure', provider: 'outlook', statusCode });
|
|
130
|
+
|
|
131
|
+
// Handle specific Graph API error codes
|
|
132
|
+
switch (err.oauthRequest?.response?.error?.code) {
|
|
133
|
+
case 'ErrorExecuteSearchStaleData': {
|
|
134
|
+
// Search cursor has expired
|
|
135
|
+
this.logger.error({ msg: 'Invalid or expired paging cursor', account: this.account, err });
|
|
136
|
+
let error = new Error('Invalid or expired paging cursor');
|
|
137
|
+
error.code = 'InvalidPagingCursor';
|
|
138
|
+
error.statusCode = err.oauthRequest?.status || 500;
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Handle HTTP status codes
|
|
144
|
+
const status = err.oauthRequest?.status;
|
|
145
|
+
const isClientError = status >= 400 && status < 500;
|
|
146
|
+
|
|
147
|
+
switch (status) {
|
|
148
|
+
case 401:
|
|
149
|
+
this.logger.error({ msg: 'Failed to authenticate API request', account: this.account, accessToken, err });
|
|
150
|
+
throw err;
|
|
151
|
+
|
|
152
|
+
case 429:
|
|
153
|
+
// Rate limiting
|
|
154
|
+
this.logger.error({ msg: 'API request was throttled', account: this.account, err });
|
|
155
|
+
throw err;
|
|
156
|
+
|
|
157
|
+
default:
|
|
158
|
+
// Log client errors (4xx) at debug level - these are expected operational errors
|
|
159
|
+
// Log server errors (5xx) and other failures at error level
|
|
160
|
+
if (isClientError) {
|
|
161
|
+
this.logger.debug({ msg: 'API request failed with client error', account: this.account, err });
|
|
162
|
+
} else {
|
|
163
|
+
this.logger.error({ msg: 'Failed to run API request', account: this.account, err });
|
|
164
|
+
}
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// PUBLIC METHODS
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Initialize the Outlook client connection
|
|
176
|
+
* Sets up OAuth2 authentication, validates access, and starts background processes
|
|
177
|
+
*/
|
|
178
|
+
async init() {
|
|
179
|
+
this.state = 'connecting';
|
|
180
|
+
await this.setStateVal();
|
|
181
|
+
|
|
182
|
+
await this.getAccount();
|
|
183
|
+
await this.prepareDelegatedAccount();
|
|
184
|
+
await this.getClient(true);
|
|
185
|
+
|
|
186
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
187
|
+
|
|
188
|
+
// Check if send-only mode
|
|
189
|
+
// Note: Scopes are checked at initialization and after account updates. If scopes change
|
|
190
|
+
// during token refresh (rare - typically requires re-authorization), the account must be
|
|
191
|
+
// reinitialized to detect the change. Consider checking scopes periodically if dynamic
|
|
192
|
+
// scope changes become common.
|
|
193
|
+
const scopes = accountData.oauth2?.accessToken?.scope || accountData.oauth2?.scope || [];
|
|
194
|
+
const { hasSendScope, hasReadScope } = this.accountObject.checkAccountScopes('outlook', scopes);
|
|
195
|
+
const isSendOnly = hasSendScope && !hasReadScope;
|
|
196
|
+
|
|
197
|
+
this.logger.debug({
|
|
198
|
+
msg: 'Account scopes loaded',
|
|
199
|
+
account: this.account,
|
|
200
|
+
scopes,
|
|
201
|
+
hasSendScope,
|
|
202
|
+
hasReadScope,
|
|
203
|
+
isSendOnly
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Verify authentication by fetching user profile
|
|
207
|
+
let profileRes;
|
|
208
|
+
try {
|
|
209
|
+
profileRes = await this.request(`/${this.oauth2UserPath}`, 'get', {});
|
|
210
|
+
} catch (err) {
|
|
211
|
+
this.state = 'authenticationError';
|
|
212
|
+
await this.setStateVal();
|
|
213
|
+
|
|
214
|
+
err.authenticationFailed = true;
|
|
215
|
+
|
|
216
|
+
if (!err.errorNotified) {
|
|
217
|
+
err.errorNotified = true;
|
|
218
|
+
await this.notify(false, AUTH_ERROR_NOTIFY, {
|
|
219
|
+
response: err.oauthRequest?.response?.error?.message || err.response,
|
|
220
|
+
serverResponseCode: 'ApiRequestError'
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let updates = {};
|
|
228
|
+
|
|
229
|
+
// Set locale from user preferences if not already set
|
|
230
|
+
if (!accountData.locale && !isSendOnly) {
|
|
231
|
+
let locale = (profileRes.preferredLanguage || '').toString().split(/[-_]/).shift().trim().toLowerCase();
|
|
232
|
+
if (locale) {
|
|
233
|
+
updates.locale = locale;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Update username if it has changed (e.g., after email address change)
|
|
238
|
+
if (profileRes.userPrincipalName && accountData.oauth2.auth?.user !== profileRes.userPrincipalName && !accountData.oauth2.auth?.delegatedUser) {
|
|
239
|
+
updates.oauth2 = {
|
|
240
|
+
partial: true,
|
|
241
|
+
auth: Object.assign(accountData.oauth2.auth || {}, {
|
|
242
|
+
// update username
|
|
243
|
+
user: profileRes.userPrincipalName
|
|
244
|
+
})
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (Object.keys(updates).length) {
|
|
249
|
+
await this.accountObject.update(updates);
|
|
250
|
+
// Note: Metadata updates (name, locale) don't affect OAuth2 scopes
|
|
251
|
+
// No need to reload account data or recalculate scope-based flags
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
this.logger.debug({
|
|
255
|
+
msg: isSendOnly ? 'Initializing Outlook send-only account' : 'Initializing Outlook account',
|
|
256
|
+
provider: accountData.oauth2.provider,
|
|
257
|
+
user: accountData.oauth2.auth?.user,
|
|
258
|
+
sendOnly: isSendOnly
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
this.state = 'connected';
|
|
262
|
+
await this.setStateVal();
|
|
263
|
+
|
|
264
|
+
// Check if this is the first successful connection
|
|
265
|
+
let prevConnectedCount = await this.redis.hget(this.getAccountKey(), `state:count:connected`);
|
|
266
|
+
let isFirstSuccessfulConnection = prevConnectedCount === '0'; // string zero means the account has been initialized but not yet connected
|
|
267
|
+
|
|
268
|
+
let isiInitial = !!isFirstSuccessfulConnection;
|
|
269
|
+
|
|
270
|
+
if (!isFirstSuccessfulConnection) {
|
|
271
|
+
// check if the connection was previously in an errored state
|
|
272
|
+
let prevLastErrorState = await this.redis.hget(this.getAccountKey(), 'lastErrorState');
|
|
273
|
+
if (prevLastErrorState) {
|
|
274
|
+
try {
|
|
275
|
+
prevLastErrorState = JSON.parse(prevLastErrorState);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
// ignore
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (prevLastErrorState && typeof prevLastErrorState === 'object' && Object.keys(prevLastErrorState).length) {
|
|
282
|
+
// was previously errored
|
|
283
|
+
isFirstSuccessfulConnection = true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (isFirstSuccessfulConnection) {
|
|
288
|
+
this.logger.info({ msg: 'Successful login without a previous active session', account: this.account, isiInitial, prevActive: false });
|
|
289
|
+
await this.notify(false, AUTH_SUCCESS_NOTIFY, {
|
|
290
|
+
user: accountData.oauth2?.auth?.user
|
|
291
|
+
});
|
|
292
|
+
} else {
|
|
293
|
+
this.logger.info({ msg: 'Successful login with a previous active session', account: this.account, isiInitial, prevActive: true });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Clear any previous error states
|
|
297
|
+
await this.redis.hdel(this.getAccountKey(), 'lastErrorState', 'lastError:errorCount', 'lastError:first');
|
|
298
|
+
await emitChangeEvent(this.logger, this.account, 'state', this.state);
|
|
299
|
+
|
|
300
|
+
if (!isSendOnly) {
|
|
301
|
+
// additional operations for full access accounts
|
|
302
|
+
|
|
303
|
+
// Set up webhook subscription for real-time updates
|
|
304
|
+
await this.ensureSubscription();
|
|
305
|
+
this.setupRenewWatchTimer();
|
|
306
|
+
|
|
307
|
+
// Cache mailbox folder structure
|
|
308
|
+
try {
|
|
309
|
+
await this.listMailboxes();
|
|
310
|
+
} catch (err) {
|
|
311
|
+
this.logger.error({ msg: 'Failed to renew mailbox folder cache', err });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Start processing any pending webhook notifications
|
|
315
|
+
this.triggerSync();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Close the client connection and clean up resources
|
|
321
|
+
*/
|
|
322
|
+
async close() {
|
|
323
|
+
clearTimeout(this.renewWatchTimer);
|
|
324
|
+
this.closed = true;
|
|
325
|
+
|
|
326
|
+
if (['init', 'connecting', 'syncing', 'connected'].includes(this.state)) {
|
|
327
|
+
this.state = 'disconnected';
|
|
328
|
+
await this.setStateVal();
|
|
329
|
+
await emitChangeEvent(this.logger, this.account, 'state', this.state);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get the current connection state from Redis
|
|
337
|
+
*/
|
|
338
|
+
async currentState() {
|
|
339
|
+
return (await this.redis.hget(this.getAccountKey(), 'state')) || 'disconnected';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Delete the account and clean up resources
|
|
344
|
+
*/
|
|
345
|
+
async delete() {
|
|
346
|
+
clearTimeout(this.renewWatchTimer);
|
|
347
|
+
this.closed = true;
|
|
348
|
+
|
|
349
|
+
if (['init', 'connecting', 'syncing', 'connected'].includes(this.state)) {
|
|
350
|
+
this.state = 'disconnected';
|
|
351
|
+
await this.setStateVal();
|
|
352
|
+
await emitChangeEvent(this.logger, this.account, 'state', this.state);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Reconnect to the account (re-initialize)
|
|
360
|
+
*/
|
|
361
|
+
async reconnect() {
|
|
362
|
+
return await this.init();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* List all mailbox folders with optional status information
|
|
367
|
+
* Caches results and only refreshes when changes are detected
|
|
368
|
+
*/
|
|
369
|
+
async listMailboxes(options) {
|
|
370
|
+
await this.prepare();
|
|
371
|
+
|
|
372
|
+
let mailboxListing;
|
|
373
|
+
|
|
374
|
+
let cachedListing = await this.getCachedMailboxListing();
|
|
375
|
+
|
|
376
|
+
// Refresh cache if:
|
|
377
|
+
// 1. No cached listing exists
|
|
378
|
+
// 2. Message counts are requested
|
|
379
|
+
// 3. Folder structure has changed
|
|
380
|
+
if (!cachedListing || options?.statusQuery?.messages || (await this.renewMailboxFolderCache())) {
|
|
381
|
+
// Has changes or counters requested
|
|
382
|
+
mailboxListing = await this.getMailboxListing();
|
|
383
|
+
try {
|
|
384
|
+
await this.redis.hset(this.getAccountCacheKey(), 'outlookMailboxListing', JSON.stringify(mailboxListing));
|
|
385
|
+
} catch (err) {
|
|
386
|
+
this.logger.error({ msg: 'Failed to cache mailbox listing', err });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!cachedListing && !options?.statusQuery?.messages) {
|
|
390
|
+
// Force delta update as it was not called previously
|
|
391
|
+
await this.renewMailboxFolderCache();
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
// No changes, use cached listing
|
|
395
|
+
mailboxListing = cachedListing;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Format mailbox data for API response
|
|
399
|
+
let mailboxes = mailboxListing
|
|
400
|
+
.map(entry => {
|
|
401
|
+
let folderData = {
|
|
402
|
+
id: entry.id,
|
|
403
|
+
path: entry.pathName,
|
|
404
|
+
delimiter: '/',
|
|
405
|
+
parentPath: entry.parentPath,
|
|
406
|
+
name: entry.displayName,
|
|
407
|
+
listed: true,
|
|
408
|
+
subscribed: true
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
if (entry.specialUse) {
|
|
412
|
+
folderData.specialUse = entry.specialUse;
|
|
413
|
+
folderData.specialUseSource = 'extension';
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Include message counts if requested
|
|
417
|
+
if (options?.statusQuery?.messages) {
|
|
418
|
+
folderData.status = {
|
|
419
|
+
messages: entry.totalItemCount,
|
|
420
|
+
unseen: entry.unreadItemCount
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return folderData;
|
|
425
|
+
})
|
|
426
|
+
.sort((a, b) => {
|
|
427
|
+
// INBOX always comes first
|
|
428
|
+
if (a.path === 'INBOX' || a.specialUse === '\\Inbox') {
|
|
429
|
+
return -1;
|
|
430
|
+
} else if (b.path === 'INBOX' || b.specialUse === '\\Inbox') {
|
|
431
|
+
return 1;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Special use folders come before regular folders
|
|
435
|
+
if (a.specialUse && !b.specialUse) {
|
|
436
|
+
return -1;
|
|
437
|
+
} else if (!a.specialUse && b.specialUse) {
|
|
438
|
+
return 1;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Alphabetical sorting for the rest
|
|
442
|
+
return a.path.toLowerCase().localeCompare(b.path.toLowerCase());
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
return mailboxes;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* List messages in a folder with search and pagination support
|
|
450
|
+
* Supports both folder-specific and cross-folder searches
|
|
451
|
+
*/
|
|
452
|
+
async listMessages(query, options) {
|
|
453
|
+
options = options || {};
|
|
454
|
+
|
|
455
|
+
await this.prepare();
|
|
456
|
+
|
|
457
|
+
let path = [].concat(query.path || []).join('/');
|
|
458
|
+
|
|
459
|
+
let folder;
|
|
460
|
+
let cachedListing;
|
|
461
|
+
let mailboxListing;
|
|
462
|
+
|
|
463
|
+
// Handle special "\\All" path for cross-folder searches
|
|
464
|
+
if (path === '\\All') {
|
|
465
|
+
folder = null;
|
|
466
|
+
cachedListing = await this.getCachedMailboxListing();
|
|
467
|
+
mailboxListing = cachedListing || (await this.getMailboxListing());
|
|
468
|
+
} else {
|
|
469
|
+
folder = await this.resolveFolder(path);
|
|
470
|
+
if (!folder) {
|
|
471
|
+
let error = new Error('Listing failed');
|
|
472
|
+
error.info = {
|
|
473
|
+
response: 'Not able to find mailbox folder'
|
|
474
|
+
};
|
|
475
|
+
error.code = 'NotFound';
|
|
476
|
+
error.statusCode = 404;
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
479
|
+
path = folder.pathName;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Helper to resolve folder from parentFolderId
|
|
483
|
+
let quickResolveFolder = parentFolderId => {
|
|
484
|
+
if (folder) {
|
|
485
|
+
return folder;
|
|
486
|
+
}
|
|
487
|
+
return mailboxListing.find(entry => entry.id === parentFolderId);
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// Pagination setup
|
|
491
|
+
let page = Number(query.page) || 0;
|
|
492
|
+
let pageSize = Math.abs(Number(query.pageSize) || 20);
|
|
493
|
+
let $skiptoken;
|
|
494
|
+
|
|
495
|
+
// Decode cursor if provided
|
|
496
|
+
if (query.cursor) {
|
|
497
|
+
let { cursorPage, skipToken } = this.decodeCursorStr(query.cursor);
|
|
498
|
+
if (typeof cursorPage === 'number' && cursorPage >= 0) {
|
|
499
|
+
page = cursorPage;
|
|
500
|
+
}
|
|
501
|
+
if (skipToken) {
|
|
502
|
+
$skiptoken = skipToken;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Build Graph API query parameters
|
|
507
|
+
let requestQuery = {
|
|
508
|
+
$count: true,
|
|
509
|
+
$top: pageSize,
|
|
510
|
+
$skip: page * pageSize,
|
|
511
|
+
$skiptoken,
|
|
512
|
+
$orderBy: 'receivedDateTime desc',
|
|
513
|
+
$select: (options.metadataOnly
|
|
514
|
+
? ['id', 'conversationId', 'receivedDateTime', 'isRead', 'isDraft', 'flag', 'body', 'subject', 'from', 'replyTo', 'sender', 'internetMessageId']
|
|
515
|
+
: [
|
|
516
|
+
'id',
|
|
517
|
+
'conversationId',
|
|
518
|
+
'receivedDateTime',
|
|
519
|
+
'isRead',
|
|
520
|
+
'isDraft',
|
|
521
|
+
'flag',
|
|
522
|
+
'body',
|
|
523
|
+
'subject',
|
|
524
|
+
'from',
|
|
525
|
+
'replyTo',
|
|
526
|
+
'sender',
|
|
527
|
+
'toRecipients',
|
|
528
|
+
'ccRecipients',
|
|
529
|
+
'bccRecipients',
|
|
530
|
+
'internetMessageId',
|
|
531
|
+
'bodyPreview'
|
|
532
|
+
]
|
|
533
|
+
)
|
|
534
|
+
.concat(!folder ? 'parentFolderId' : [])
|
|
535
|
+
.join(','),
|
|
536
|
+
$expand: options.metadataOnly ? undefined : 'attachments($select=id,name,contentType,size,isInline,microsoft.graph.fileAttachment/contentId)'
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
let useOutlookSearch = false;
|
|
540
|
+
let skipToken = null;
|
|
541
|
+
|
|
542
|
+
// Handle search queries
|
|
543
|
+
if (query.search) {
|
|
544
|
+
if (query.useOutlookSearch) {
|
|
545
|
+
// Use Outlook's native search syntax
|
|
546
|
+
const $search = this.prepareSearchQuery(query.search);
|
|
547
|
+
if ($search) {
|
|
548
|
+
requestQuery.$search = `"${$search}"`;
|
|
549
|
+
// remove unsupported request arguments for search
|
|
550
|
+
for (let disabledParam of ['$skip', '$orderBy', '$count']) {
|
|
551
|
+
delete requestQuery[disabledParam];
|
|
552
|
+
}
|
|
553
|
+
useOutlookSearch = true;
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
// Use OData filters for more precise searching
|
|
557
|
+
const $filter = this.prepareFilterQuery(query.search);
|
|
558
|
+
if ($filter) {
|
|
559
|
+
// we need to have receivedDateTime as the first filtering property, otherwise ordering will fail
|
|
560
|
+
requestQuery.$filter = `receivedDateTime gt 1970-01-01T00:00:00.000Z and ${$filter}`;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
let messages = [];
|
|
566
|
+
let totalMessages;
|
|
567
|
+
|
|
568
|
+
// Execute the message list request
|
|
569
|
+
try {
|
|
570
|
+
let listing = await this.request(`/${this.oauth2UserPath}/${folder ? `mailFolders/${folder.id}/` : ''}messages`, 'get', requestQuery);
|
|
571
|
+
|
|
572
|
+
totalMessages = !isNaN(listing['@odata.count']) ? Number(listing['@odata.count']) : undefined;
|
|
573
|
+
|
|
574
|
+
// Extract continuation token for search results
|
|
575
|
+
if (useOutlookSearch && listing['@odata.nextLink']) {
|
|
576
|
+
let nextLinkObj = new URL(listing['@odata.nextLink']);
|
|
577
|
+
skipToken = nextLinkObj.searchParams.get('$skiptoken');
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
messages =
|
|
581
|
+
listing?.value?.map(messageData =>
|
|
582
|
+
this.formatMessage(messageData, { path: quickResolveFolder(messageData.parentFolderId)?.pathName, showPath: !folder })
|
|
583
|
+
) || [];
|
|
584
|
+
} catch (err) {
|
|
585
|
+
this.logger.error({
|
|
586
|
+
msg: 'Failed to list messages',
|
|
587
|
+
mailboxId: folder?.id,
|
|
588
|
+
path,
|
|
589
|
+
err
|
|
590
|
+
});
|
|
591
|
+
throw err;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Calculate pagination info
|
|
595
|
+
let pages = typeof totalMessages === 'number' ? Math.ceil(totalMessages / pageSize) || 1 : undefined;
|
|
596
|
+
|
|
597
|
+
if (page < 0) {
|
|
598
|
+
page = 0;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
let nextPageCursor = page < pages - 1 || skipToken ? this.encodeCursorString(page + 1, skipToken) : null;
|
|
602
|
+
// no previous page cursor if we are using skip token for paging
|
|
603
|
+
let prevPageCursor = skipToken ? undefined : page > 0 ? this.encodeCursorString(Math.min(page - 1, pages - 1)) : null;
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
total: totalMessages,
|
|
607
|
+
page,
|
|
608
|
+
pages,
|
|
609
|
+
nextPageCursor,
|
|
610
|
+
prevPageCursor,
|
|
611
|
+
messages
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Get the raw RFC822 message content
|
|
617
|
+
*/
|
|
618
|
+
async getRawMessage(emailId) {
|
|
619
|
+
await this.prepare();
|
|
620
|
+
|
|
621
|
+
let raw;
|
|
622
|
+
|
|
623
|
+
try {
|
|
624
|
+
raw = await this.request(`/${this.oauth2UserPath}/messages/${emailId}/$value`, 'get', Buffer.alloc(0), { returnText: true });
|
|
625
|
+
} catch (err) {
|
|
626
|
+
switch (err.oauthRequest?.status) {
|
|
627
|
+
case 404: {
|
|
628
|
+
let error = new Error('Unknown message');
|
|
629
|
+
error.info = {
|
|
630
|
+
response: `Message does not exist`
|
|
631
|
+
};
|
|
632
|
+
error.code = 'NotFound';
|
|
633
|
+
error.statusCode = 404;
|
|
634
|
+
throw error;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
case 400: {
|
|
638
|
+
let error = new Error('Invalid request');
|
|
639
|
+
error.info = {
|
|
640
|
+
response: err.oauthRequest?.response?.error?.message || `Invalid request`
|
|
641
|
+
};
|
|
642
|
+
error.code = err.oauthRequest?.response?.error?.code || 'InvalidRequest';
|
|
643
|
+
error.statusCode = 400;
|
|
644
|
+
throw error;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
default:
|
|
648
|
+
this.logger.error({
|
|
649
|
+
msg: 'Failed to fetch raw message',
|
|
650
|
+
emailId,
|
|
651
|
+
err
|
|
652
|
+
});
|
|
653
|
+
throw err;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return raw ? Buffer.from(raw) : null;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Delete a message either permanently or by moving to trash
|
|
662
|
+
* @param {string} emailId - Message ID to delete
|
|
663
|
+
* @param {boolean} force - If true, permanently delete; otherwise move to trash
|
|
664
|
+
*/
|
|
665
|
+
async deleteMessage(emailId, force) {
|
|
666
|
+
await this.prepare();
|
|
667
|
+
|
|
668
|
+
if (force) {
|
|
669
|
+
// Permanent deletion
|
|
670
|
+
try {
|
|
671
|
+
await this.request(`/${this.oauth2UserPath}/messages/${emailId}`, 'delete', Buffer.alloc(0), { returnText: true });
|
|
672
|
+
} catch (err) {
|
|
673
|
+
this.logger.error({
|
|
674
|
+
msg: 'Failed to delete message',
|
|
675
|
+
emailId,
|
|
676
|
+
err
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
switch (err?.oauthRequest?.response?.error?.code) {
|
|
680
|
+
case 'ErrorCannotDeleteObject':
|
|
681
|
+
// does not exist
|
|
682
|
+
return {
|
|
683
|
+
deleted: false
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
default:
|
|
687
|
+
throw err;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return {
|
|
692
|
+
deleted: true
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Move to trash (soft delete)
|
|
697
|
+
let messageData;
|
|
698
|
+
try {
|
|
699
|
+
messageData = await this.request(`/${this.oauth2UserPath}/messages/${emailId}/move`, 'post', {
|
|
700
|
+
destinationId: 'deleteditems'
|
|
701
|
+
});
|
|
702
|
+
if (!messageData) {
|
|
703
|
+
throw new Error('Failed to move message to Trash');
|
|
704
|
+
}
|
|
705
|
+
} catch (err) {
|
|
706
|
+
this.logger.error({
|
|
707
|
+
msg: 'Failed to move message to Trash',
|
|
708
|
+
emailId,
|
|
709
|
+
err
|
|
710
|
+
});
|
|
711
|
+
throw err;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Resolve the destination folder for the response
|
|
715
|
+
let folder;
|
|
716
|
+
try {
|
|
717
|
+
folder = await this.resolveFolder(messageData.parentFolderId, { byId: true });
|
|
718
|
+
} catch (err) {
|
|
719
|
+
this.logger.error({
|
|
720
|
+
msg: 'Failed to resolve folder for message',
|
|
721
|
+
emailId,
|
|
722
|
+
err
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
deleted: true,
|
|
728
|
+
moved: {
|
|
729
|
+
destination: folder?.pathName,
|
|
730
|
+
message: emailId
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Delete multiple messages matching search criteria
|
|
737
|
+
* Uses batch operations for efficiency
|
|
738
|
+
*/
|
|
739
|
+
async deleteMessages(path, search, force) {
|
|
740
|
+
await this.prepare();
|
|
741
|
+
|
|
742
|
+
let folder;
|
|
743
|
+
if (!force) {
|
|
744
|
+
try {
|
|
745
|
+
folder = await this.resolveFolder('\\Trash');
|
|
746
|
+
|
|
747
|
+
// If we're already in trash, force delete
|
|
748
|
+
if (!force && folder?.specialUse === '\\Trash') {
|
|
749
|
+
force = true;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (!force && !folder) {
|
|
753
|
+
throw new Error('Trash folder was not found');
|
|
754
|
+
}
|
|
755
|
+
} catch (err) {
|
|
756
|
+
this.logger.error({
|
|
757
|
+
msg: 'Failed to resolve folder for Trash',
|
|
758
|
+
err
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Step 1. Resolve matching messages
|
|
764
|
+
let emailIds = search.emailIds || (await this.searchEmailIds(path, search));
|
|
765
|
+
|
|
766
|
+
if (!emailIds?.length) {
|
|
767
|
+
// nothing to do here
|
|
768
|
+
return { deleted: false };
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Prepare batch operation variables
|
|
772
|
+
let batch = [];
|
|
773
|
+
let idGen = 0;
|
|
774
|
+
let updatedEmailIds = [];
|
|
775
|
+
let messageMap = new Map();
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Submit a batch of delete operations to Graph API
|
|
779
|
+
*/
|
|
780
|
+
let submitBatch = async () => {
|
|
781
|
+
let responseData;
|
|
782
|
+
try {
|
|
783
|
+
responseData = await this.request(`/$batch`, 'post', {
|
|
784
|
+
requests: batch
|
|
785
|
+
});
|
|
786
|
+
for (let response of responseData?.responses || []) {
|
|
787
|
+
if (response?.status >= 200 && response?.status < 300) {
|
|
788
|
+
let emailId = messageMap.get(response.id);
|
|
789
|
+
if (emailId) {
|
|
790
|
+
updatedEmailIds.push(emailId);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
} catch (err) {
|
|
795
|
+
this.logger.error({
|
|
796
|
+
msg: 'Failed to run batch operation',
|
|
797
|
+
err
|
|
798
|
+
});
|
|
799
|
+
throw err;
|
|
800
|
+
} finally {
|
|
801
|
+
batch = [];
|
|
802
|
+
messageMap = new Map();
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Format a single delete request for the batch
|
|
808
|
+
*/
|
|
809
|
+
let formatRequest = emailId => {
|
|
810
|
+
let reqId = `msg_${++idGen}`;
|
|
811
|
+
messageMap.set(reqId, emailId);
|
|
812
|
+
|
|
813
|
+
if (force) {
|
|
814
|
+
// Permanent delete
|
|
815
|
+
return {
|
|
816
|
+
id: reqId,
|
|
817
|
+
method: 'DELETE',
|
|
818
|
+
url: `/${this.oauth2UserPath}/messages/${emailId}`
|
|
819
|
+
};
|
|
820
|
+
} else {
|
|
821
|
+
// Move to trash
|
|
822
|
+
return {
|
|
823
|
+
id: reqId,
|
|
824
|
+
method: 'POST',
|
|
825
|
+
url: `/${this.oauth2UserPath}/messages/${emailId}/move`,
|
|
826
|
+
body: { destinationId: 'deleteditems' },
|
|
827
|
+
headers: {
|
|
828
|
+
'Content-Type': 'application/json'
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
// Process messages in batches
|
|
835
|
+
for (let emailId of emailIds) {
|
|
836
|
+
batch.push(formatRequest(emailId));
|
|
837
|
+
// submit batch when it reaches max size
|
|
838
|
+
if (batch.length >= MAX_BATCH_SIZE) {
|
|
839
|
+
await submitBatch(batch);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (batch.length) {
|
|
843
|
+
// submit remaining batch
|
|
844
|
+
await submitBatch(batch);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return Object.assign(
|
|
848
|
+
{ deleted: true },
|
|
849
|
+
!force
|
|
850
|
+
? {
|
|
851
|
+
moved: {
|
|
852
|
+
destination: folder.pathName,
|
|
853
|
+
emailIds: updatedEmailIds
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
: {
|
|
857
|
+
deletedMessages: {
|
|
858
|
+
emailIds: updatedEmailIds
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Update message flags (Graph API only supports \Seen and \Flagged)
|
|
866
|
+
*/
|
|
867
|
+
async updateMessage(emailId, updates) {
|
|
868
|
+
await this.prepare();
|
|
869
|
+
updates = updates || {};
|
|
870
|
+
|
|
871
|
+
let addFlags = updates.flags?.add || [];
|
|
872
|
+
let deleteFlags = updates.flags?.delete || [];
|
|
873
|
+
|
|
874
|
+
// Handle flag set operations
|
|
875
|
+
if (updates.flags?.set) {
|
|
876
|
+
for (let flag of ['\\Seen', '\\Flagged']) {
|
|
877
|
+
if (updates.flags.set.includes(flag)) {
|
|
878
|
+
addFlags.push(flag);
|
|
879
|
+
} else {
|
|
880
|
+
deleteFlags.push(flag);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Map IMAP flags to Graph API properties
|
|
886
|
+
let flagUpdates = {};
|
|
887
|
+
|
|
888
|
+
if (addFlags.includes('\\Seen')) {
|
|
889
|
+
flagUpdates.isRead = true;
|
|
890
|
+
}
|
|
891
|
+
if (deleteFlags.includes('\\Seen')) {
|
|
892
|
+
flagUpdates.isRead = false;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (addFlags.includes('\\Flagged')) {
|
|
896
|
+
flagUpdates.flag = { flagStatus: 'flagged' };
|
|
897
|
+
}
|
|
898
|
+
if (deleteFlags.includes('\\Flagged')) {
|
|
899
|
+
flagUpdates.flag = { flagStatus: 'notFlagged' };
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Handle label (category) operations
|
|
903
|
+
if (updates.labels) {
|
|
904
|
+
let categories = [];
|
|
905
|
+
|
|
906
|
+
if (updates.labels.set) {
|
|
907
|
+
// Set replaces all categories
|
|
908
|
+
categories = updates.labels.set;
|
|
909
|
+
} else {
|
|
910
|
+
// For add/delete, fetch current categories first
|
|
911
|
+
let currentMessage = await this.request(`/${this.oauth2UserPath}/messages/${emailId}`, 'get', {
|
|
912
|
+
$select: 'categories'
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
categories = currentMessage.categories || [];
|
|
916
|
+
|
|
917
|
+
if (updates.labels.add) {
|
|
918
|
+
for (let label of updates.labels.add) {
|
|
919
|
+
if (!categories.includes(label)) {
|
|
920
|
+
categories.push(label);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
if (updates.labels.delete) {
|
|
926
|
+
categories = categories.filter(c => !updates.labels.delete.includes(c));
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
flagUpdates.categories = categories;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
let modifyResult;
|
|
934
|
+
|
|
935
|
+
try {
|
|
936
|
+
modifyResult = await this.request(`/${this.oauth2UserPath}/messages/${emailId}`, 'patch', flagUpdates);
|
|
937
|
+
} catch (err) {
|
|
938
|
+
this.logger.error({
|
|
939
|
+
msg: 'Failed to update message',
|
|
940
|
+
emailId,
|
|
941
|
+
flagUpdates,
|
|
942
|
+
err
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
switch (err.oauthRequest?.status) {
|
|
946
|
+
case 400: {
|
|
947
|
+
let error = new Error('Invalid request');
|
|
948
|
+
error.info = {
|
|
949
|
+
response: err.oauthRequest?.response?.error?.message || `Invalid request`
|
|
950
|
+
};
|
|
951
|
+
error.code = err.oauthRequest?.response?.error?.code || 'InvalidRequest';
|
|
952
|
+
error.statusCode = 400;
|
|
953
|
+
throw error;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
default:
|
|
957
|
+
throw err;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Build the result flag set
|
|
962
|
+
const result = [];
|
|
963
|
+
if (modifyResult.isRead) {
|
|
964
|
+
result.push('\\Seen');
|
|
965
|
+
}
|
|
966
|
+
if (modifyResult.isDraft) {
|
|
967
|
+
result.push('\\Draft');
|
|
968
|
+
}
|
|
969
|
+
if (modifyResult.flag?.flagStatus === 'flagged') {
|
|
970
|
+
result.push('\\Flagged');
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
let response = {
|
|
974
|
+
flags: Object.assign({}, updates.flags || {}, { result })
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
// Include labels in response if they were updated
|
|
978
|
+
if (updates.labels) {
|
|
979
|
+
response.labels = Object.assign({}, updates.labels || {}, { result: modifyResult.categories || [] });
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
return response;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Update flags for multiple messages matching search criteria
|
|
987
|
+
* Uses batch operations for efficiency
|
|
988
|
+
*/
|
|
989
|
+
async updateMessages(path, search, updates) {
|
|
990
|
+
await this.prepare();
|
|
991
|
+
|
|
992
|
+
updates = updates || {};
|
|
993
|
+
|
|
994
|
+
let addFlags = updates.flags?.add || [];
|
|
995
|
+
let deleteFlags = updates.flags?.delete || [];
|
|
996
|
+
|
|
997
|
+
// Handle flag set operations
|
|
998
|
+
if (updates.flags?.set) {
|
|
999
|
+
for (let flag of ['\\Seen', '\\Flagged']) {
|
|
1000
|
+
if (updates.flags.set.includes(flag)) {
|
|
1001
|
+
addFlags.push(flag);
|
|
1002
|
+
} else {
|
|
1003
|
+
deleteFlags.push(flag);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Map IMAP flags to Graph API properties
|
|
1009
|
+
let flagUpdates = {};
|
|
1010
|
+
|
|
1011
|
+
if (addFlags.includes('\\Seen')) {
|
|
1012
|
+
flagUpdates.isRead = true;
|
|
1013
|
+
}
|
|
1014
|
+
if (deleteFlags.includes('\\Seen')) {
|
|
1015
|
+
flagUpdates.isRead = false;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (addFlags.includes('\\Flagged')) {
|
|
1019
|
+
flagUpdates.flag = { flagStatus: 'flagged' };
|
|
1020
|
+
}
|
|
1021
|
+
if (deleteFlags.includes('\\Flagged')) {
|
|
1022
|
+
flagUpdates.flag = { flagStatus: 'notFlagged' };
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// Step 1. Resolve matching messages
|
|
1026
|
+
let emailIds = search.emailIds || (await this.searchEmailIds(path, search));
|
|
1027
|
+
|
|
1028
|
+
if (!emailIds?.length) {
|
|
1029
|
+
// nothing to do here
|
|
1030
|
+
return updates;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// Handle label (category) operations
|
|
1034
|
+
// For add/delete, we need to fetch current categories first
|
|
1035
|
+
let needsCurrentCategories = updates.labels && !updates.labels.set;
|
|
1036
|
+
let currentCategories = new Map();
|
|
1037
|
+
|
|
1038
|
+
if (needsCurrentCategories) {
|
|
1039
|
+
// Fetch current categories for all messages in batches
|
|
1040
|
+
let fetchBatch = [];
|
|
1041
|
+
let fetchIdGen = 0;
|
|
1042
|
+
let fetchMessageMap = new Map();
|
|
1043
|
+
|
|
1044
|
+
let submitFetchBatch = async () => {
|
|
1045
|
+
let responseData;
|
|
1046
|
+
try {
|
|
1047
|
+
responseData = await this.request(`/$batch`, 'post', {
|
|
1048
|
+
requests: fetchBatch
|
|
1049
|
+
});
|
|
1050
|
+
for (let response of responseData?.responses || []) {
|
|
1051
|
+
if (response?.status >= 200 && response?.status < 300 && response.body) {
|
|
1052
|
+
let emailId = fetchMessageMap.get(response.id);
|
|
1053
|
+
if (emailId) {
|
|
1054
|
+
currentCategories.set(emailId, response.body.categories || []);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
this.logger.error({
|
|
1060
|
+
msg: 'Failed to fetch current categories',
|
|
1061
|
+
err
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
fetchBatch = [];
|
|
1065
|
+
fetchMessageMap = new Map();
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
for (let emailId of emailIds) {
|
|
1069
|
+
let reqId = `fetch_${++fetchIdGen}`;
|
|
1070
|
+
fetchMessageMap.set(reqId, emailId);
|
|
1071
|
+
fetchBatch.push({
|
|
1072
|
+
id: reqId,
|
|
1073
|
+
method: 'GET',
|
|
1074
|
+
url: `/${this.oauth2UserPath}/messages/${emailId}?$select=categories`
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
if (fetchBatch.length >= MAX_BATCH_SIZE) {
|
|
1078
|
+
await submitFetchBatch();
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
if (fetchBatch.length) {
|
|
1082
|
+
await submitFetchBatch();
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Batch operation setup
|
|
1087
|
+
let batch = [];
|
|
1088
|
+
let idGen = 0;
|
|
1089
|
+
let updatedEmailIds = [];
|
|
1090
|
+
let messageMap = new Map();
|
|
1091
|
+
|
|
1092
|
+
let submitBatch = async () => {
|
|
1093
|
+
let responseData;
|
|
1094
|
+
try {
|
|
1095
|
+
responseData = await this.request(`/$batch`, 'post', {
|
|
1096
|
+
requests: batch
|
|
1097
|
+
});
|
|
1098
|
+
for (let response of responseData?.responses || []) {
|
|
1099
|
+
if (response?.status >= 200 && response?.status < 300) {
|
|
1100
|
+
let emailId = messageMap.get(response.id);
|
|
1101
|
+
if (emailId) {
|
|
1102
|
+
updatedEmailIds.push(emailId);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
} catch (err) {
|
|
1107
|
+
this.logger.error({
|
|
1108
|
+
msg: 'Failed to run batch operation',
|
|
1109
|
+
err
|
|
1110
|
+
});
|
|
1111
|
+
throw err;
|
|
1112
|
+
} finally {
|
|
1113
|
+
batch = [];
|
|
1114
|
+
messageMap = new Map();
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
let formatRequest = emailId => {
|
|
1119
|
+
let reqId = `msg_${++idGen}`;
|
|
1120
|
+
messageMap.set(reqId, emailId);
|
|
1121
|
+
|
|
1122
|
+
let bodyUpdates = { ...flagUpdates };
|
|
1123
|
+
|
|
1124
|
+
// Handle categories for this specific message
|
|
1125
|
+
if (updates.labels) {
|
|
1126
|
+
if (updates.labels.set) {
|
|
1127
|
+
bodyUpdates.categories = updates.labels.set;
|
|
1128
|
+
} else {
|
|
1129
|
+
let categories = currentCategories.get(emailId) || [];
|
|
1130
|
+
|
|
1131
|
+
if (updates.labels.add) {
|
|
1132
|
+
for (let label of updates.labels.add) {
|
|
1133
|
+
if (!categories.includes(label)) {
|
|
1134
|
+
categories.push(label);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (updates.labels.delete) {
|
|
1140
|
+
categories = categories.filter(c => !updates.labels.delete.includes(c));
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
bodyUpdates.categories = categories;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
return {
|
|
1148
|
+
id: reqId,
|
|
1149
|
+
method: 'PATCH',
|
|
1150
|
+
url: `/${this.oauth2UserPath}/messages/${emailId}`,
|
|
1151
|
+
body: bodyUpdates,
|
|
1152
|
+
headers: {
|
|
1153
|
+
'Content-Type': 'application/json'
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
// Process messages in batches
|
|
1159
|
+
for (let emailId of emailIds) {
|
|
1160
|
+
batch.push(formatRequest(emailId));
|
|
1161
|
+
// submit batch when it reaches max size
|
|
1162
|
+
if (batch.length >= MAX_BATCH_SIZE) {
|
|
1163
|
+
await submitBatch(batch);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (batch.length) {
|
|
1167
|
+
// submit remaining batch
|
|
1168
|
+
await submitBatch(batch);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
return Object.assign({}, updates, { emailIds: updatedEmailIds });
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Move a single message to another folder
|
|
1176
|
+
*/
|
|
1177
|
+
async moveMessage(emailId, target) {
|
|
1178
|
+
await this.prepare();
|
|
1179
|
+
|
|
1180
|
+
let path = [].concat(target?.path || []).join('/');
|
|
1181
|
+
|
|
1182
|
+
let targetFolder = await this.resolveFolder(path);
|
|
1183
|
+
if (!targetFolder) {
|
|
1184
|
+
let error = new Error('Move failed');
|
|
1185
|
+
error.info = {
|
|
1186
|
+
response: 'Not able to find target folder'
|
|
1187
|
+
};
|
|
1188
|
+
error.code = 'NotFound';
|
|
1189
|
+
error.statusCode = 404;
|
|
1190
|
+
throw error;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
let messageData;
|
|
1194
|
+
try {
|
|
1195
|
+
messageData = await this.request(`/${this.oauth2UserPath}/messages/${emailId}/move`, 'post', {
|
|
1196
|
+
destinationId: targetFolder.id
|
|
1197
|
+
});
|
|
1198
|
+
if (!messageData) {
|
|
1199
|
+
throw new Error('Failed to move message');
|
|
1200
|
+
}
|
|
1201
|
+
} catch (err) {
|
|
1202
|
+
this.logger.error({
|
|
1203
|
+
msg: 'Failed to move message',
|
|
1204
|
+
emailId,
|
|
1205
|
+
target: targetFolder.pathName,
|
|
1206
|
+
err
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
switch (err?.oauthRequest?.response?.error?.code) {
|
|
1210
|
+
case 'ErrorItemNotFound': {
|
|
1211
|
+
let error = new Error('Move failed');
|
|
1212
|
+
error.info = {
|
|
1213
|
+
response: 'Not able to find source message'
|
|
1214
|
+
};
|
|
1215
|
+
error.code = 'NotFound';
|
|
1216
|
+
error.statusCode = 404;
|
|
1217
|
+
throw error;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
default: {
|
|
1221
|
+
let error = new Error('Move failed');
|
|
1222
|
+
error.info = {
|
|
1223
|
+
response: err?.oauthRequest?.response?.error?.message
|
|
1224
|
+
};
|
|
1225
|
+
error.code = err?.oauthRequest?.response?.error?.code;
|
|
1226
|
+
error.statusCode = 400;
|
|
1227
|
+
throw error;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
return {
|
|
1233
|
+
path: targetFolder.pathName,
|
|
1234
|
+
id: messageData.id
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Move multiple messages to another folder
|
|
1240
|
+
* Uses batch operations for efficiency
|
|
1241
|
+
*/
|
|
1242
|
+
async moveMessages(source, search, target) {
|
|
1243
|
+
await this.prepare();
|
|
1244
|
+
|
|
1245
|
+
let path = [].concat(target?.path || []).join('/');
|
|
1246
|
+
|
|
1247
|
+
let targetFolder = await this.resolveFolder(path);
|
|
1248
|
+
if (!targetFolder) {
|
|
1249
|
+
let error = new Error('Move failed');
|
|
1250
|
+
error.info = {
|
|
1251
|
+
response: 'Not able to find target folder'
|
|
1252
|
+
};
|
|
1253
|
+
error.code = 'NotFound';
|
|
1254
|
+
error.statusCode = 404;
|
|
1255
|
+
throw error;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Step 1. Resolve matching messages
|
|
1259
|
+
let emailIds = search.emailIds || (await this.searchEmailIds(source, search));
|
|
1260
|
+
|
|
1261
|
+
if (!emailIds?.length) {
|
|
1262
|
+
// nothing to do here
|
|
1263
|
+
return { path: targetFolder.pathName };
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Batch operation setup
|
|
1267
|
+
let batch = [];
|
|
1268
|
+
let idGen = 0;
|
|
1269
|
+
let updatedEmailIds = [];
|
|
1270
|
+
let messageMap = new Map();
|
|
1271
|
+
|
|
1272
|
+
let submitBatch = async () => {
|
|
1273
|
+
let responseData;
|
|
1274
|
+
try {
|
|
1275
|
+
responseData = await this.request(`/$batch`, 'post', {
|
|
1276
|
+
requests: batch
|
|
1277
|
+
});
|
|
1278
|
+
for (let response of responseData?.responses || []) {
|
|
1279
|
+
if (response?.status >= 200 && response?.status < 300) {
|
|
1280
|
+
let emailId = messageMap.get(response.id);
|
|
1281
|
+
if (emailId) {
|
|
1282
|
+
updatedEmailIds.push(emailId);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
} catch (err) {
|
|
1287
|
+
this.logger.error({
|
|
1288
|
+
msg: 'Failed to run batch operation',
|
|
1289
|
+
err
|
|
1290
|
+
});
|
|
1291
|
+
throw err;
|
|
1292
|
+
} finally {
|
|
1293
|
+
batch = [];
|
|
1294
|
+
messageMap = new Map();
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
|
|
1298
|
+
let formatRequest = emailId => {
|
|
1299
|
+
let reqId = `msg_${++idGen}`;
|
|
1300
|
+
messageMap.set(reqId, emailId);
|
|
1301
|
+
return {
|
|
1302
|
+
id: reqId,
|
|
1303
|
+
method: 'POST',
|
|
1304
|
+
url: `/${this.oauth2UserPath}/messages/${emailId}/move`,
|
|
1305
|
+
body: { destinationId: targetFolder.id },
|
|
1306
|
+
headers: {
|
|
1307
|
+
'Content-Type': 'application/json'
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
};
|
|
1311
|
+
|
|
1312
|
+
// Process messages in batches
|
|
1313
|
+
for (let emailId of emailIds) {
|
|
1314
|
+
batch.push(formatRequest(emailId));
|
|
1315
|
+
// submit batch
|
|
1316
|
+
if (batch.length >= MAX_BATCH_SIZE) {
|
|
1317
|
+
await submitBatch(batch);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
if (batch.length) {
|
|
1321
|
+
// submit batch
|
|
1322
|
+
await submitBatch(batch);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
return Object.assign({ path: targetFolder.pathName }, { emailIds: updatedEmailIds });
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* Download an attachment by its ID
|
|
1330
|
+
* Returns attachment data with appropriate headers for download
|
|
1331
|
+
*/
|
|
1332
|
+
async getAttachment(attachmentId) {
|
|
1333
|
+
let attachmentData = await this.getAttachmentContent(attachmentId);
|
|
1334
|
+
|
|
1335
|
+
if (!attachmentData || !attachmentData.content) {
|
|
1336
|
+
return false;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// Build content-disposition header with proper filename encoding
|
|
1340
|
+
let filenameParam = '';
|
|
1341
|
+
if (attachmentData.filename) {
|
|
1342
|
+
let isCleartextFilename = attachmentData.filename && /^[a-z0-9 _\-()^[\]~=,+*$]+$/i.test(attachmentData.filename);
|
|
1343
|
+
if (isCleartextFilename) {
|
|
1344
|
+
filenameParam = `; filename=${JSON.stringify(attachmentData.filename)}`;
|
|
1345
|
+
} else {
|
|
1346
|
+
// Use RFC 5987 encoding for non-ASCII filenames
|
|
1347
|
+
filenameParam = `; filename=${JSON.stringify(he.encode(attachmentData.filename))}; filename*=utf-8''${encodeURIComponent(
|
|
1348
|
+
attachmentData.filename
|
|
1349
|
+
)}`;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
const contentResponse = {
|
|
1354
|
+
headers: {
|
|
1355
|
+
'content-type': attachmentData.mimeType || 'application/octet-stream',
|
|
1356
|
+
'content-disposition': 'attachment' + filenameParam
|
|
1357
|
+
},
|
|
1358
|
+
contentType: attachmentData.contentType,
|
|
1359
|
+
filename: attachmentData.filename,
|
|
1360
|
+
data: attachmentData.content
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
return contentResponse;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* Get full message details with optional formatting
|
|
1368
|
+
* Supports web-safe HTML generation and embedded image processing
|
|
1369
|
+
*/
|
|
1370
|
+
async getMessage(emailId, options) {
|
|
1371
|
+
options = options || {};
|
|
1372
|
+
|
|
1373
|
+
if (options.webSafeHtml) {
|
|
1374
|
+
options.textType = '*';
|
|
1375
|
+
options.embedAttachedImages = true;
|
|
1376
|
+
options.preProcessHtml = true;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
await this.prepare();
|
|
1380
|
+
|
|
1381
|
+
let messageData, path, specialUse;
|
|
1382
|
+
|
|
1383
|
+
// Prepare request options with appropriate Prefer header for body content type
|
|
1384
|
+
let requestOptions = {};
|
|
1385
|
+
if (options.textType === 'plain') {
|
|
1386
|
+
// Request plain text format from MS Graph API
|
|
1387
|
+
requestOptions.headers = {
|
|
1388
|
+
Prefer: 'outlook.body-content-type="text"'
|
|
1389
|
+
};
|
|
1390
|
+
} else if (options.textType === 'html') {
|
|
1391
|
+
// Request HTML format from MS Graph API
|
|
1392
|
+
requestOptions.headers = {
|
|
1393
|
+
Prefer: 'outlook.body-content-type="html"'
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
// If textType is '*' or undefined, don't set the preference and let MS Graph return the default format
|
|
1397
|
+
|
|
1398
|
+
try {
|
|
1399
|
+
messageData = await this.request(
|
|
1400
|
+
`/${this.oauth2UserPath}/messages/${emailId}`,
|
|
1401
|
+
'get',
|
|
1402
|
+
{
|
|
1403
|
+
// 'internetMessageHeaders' is not included by default, so have to list all required fields
|
|
1404
|
+
$select: [
|
|
1405
|
+
'id',
|
|
1406
|
+
'conversationId',
|
|
1407
|
+
'receivedDateTime',
|
|
1408
|
+
'isRead',
|
|
1409
|
+
'isDraft',
|
|
1410
|
+
'flag',
|
|
1411
|
+
'body',
|
|
1412
|
+
'subject',
|
|
1413
|
+
'from',
|
|
1414
|
+
'replyTo',
|
|
1415
|
+
'sender',
|
|
1416
|
+
'toRecipients',
|
|
1417
|
+
'ccRecipients',
|
|
1418
|
+
'bccRecipients',
|
|
1419
|
+
'internetMessageId',
|
|
1420
|
+
'bodyPreview',
|
|
1421
|
+
'internetMessageHeaders',
|
|
1422
|
+
'parentFolderId',
|
|
1423
|
+
'categories'
|
|
1424
|
+
].join(','),
|
|
1425
|
+
$expand: 'attachments($select=id,name,contentType,size,isInline,microsoft.graph.fileAttachment/contentId)'
|
|
1426
|
+
},
|
|
1427
|
+
requestOptions
|
|
1428
|
+
);
|
|
1429
|
+
} catch (err) {
|
|
1430
|
+
switch (err.oauthRequest?.status) {
|
|
1431
|
+
case 404: {
|
|
1432
|
+
let error = new Error('Unknown message');
|
|
1433
|
+
error.info = {
|
|
1434
|
+
response: `Message does not exist`
|
|
1435
|
+
};
|
|
1436
|
+
error.code = 'NotFound';
|
|
1437
|
+
error.statusCode = 404;
|
|
1438
|
+
throw error;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
case 400: {
|
|
1442
|
+
let error = new Error('Invalid request');
|
|
1443
|
+
error.info = {
|
|
1444
|
+
response: err.oauthRequest?.response?.error?.message || `Invalid request`
|
|
1445
|
+
};
|
|
1446
|
+
error.code = err.oauthRequest?.response?.error?.code || 'InvalidRequest';
|
|
1447
|
+
error.statusCode = 400;
|
|
1448
|
+
throw error;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
default:
|
|
1452
|
+
this.logger.error({
|
|
1453
|
+
msg: 'Failed to fetch message data',
|
|
1454
|
+
emailId,
|
|
1455
|
+
err
|
|
1456
|
+
});
|
|
1457
|
+
throw err;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Resolve the folder path and special use
|
|
1462
|
+
if (messageData.parentFolderId) {
|
|
1463
|
+
let folder = await this.resolveFolder(messageData.parentFolderId, { byId: true });
|
|
1464
|
+
if (!folder) {
|
|
1465
|
+
let error = new Error('Listing failed');
|
|
1466
|
+
error.info = {
|
|
1467
|
+
response: 'Not able to find mailbox folder'
|
|
1468
|
+
};
|
|
1469
|
+
error.code = 'NotFound';
|
|
1470
|
+
error.statusCode = 404;
|
|
1471
|
+
throw error;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
path = folder.pathName;
|
|
1475
|
+
specialUse = folder.specialUse;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// Process email headers
|
|
1479
|
+
if (messageData.internetMessageHeaders) {
|
|
1480
|
+
let headers = {};
|
|
1481
|
+
|
|
1482
|
+
for (let header of messageData?.internetMessageHeaders || []) {
|
|
1483
|
+
let { name, value } = header;
|
|
1484
|
+
name = (name || '').toString().trim().toLowerCase();
|
|
1485
|
+
|
|
1486
|
+
value = (value || '').toString().trim();
|
|
1487
|
+
if (!(name in headers)) {
|
|
1488
|
+
headers[name] = [];
|
|
1489
|
+
}
|
|
1490
|
+
if (!Array.isArray(headers[name])) {
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
headers[name].push(value);
|
|
1494
|
+
|
|
1495
|
+
// Extract specific headers for easier access
|
|
1496
|
+
switch (name) {
|
|
1497
|
+
case 'in-reply-to': {
|
|
1498
|
+
messageData.inReplyTo = value;
|
|
1499
|
+
break;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
messageData.headers = headers;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
const formattedMessage = this.formatMessage(messageData, {
|
|
1508
|
+
extended: true,
|
|
1509
|
+
path,
|
|
1510
|
+
textType: options.textType,
|
|
1511
|
+
showPath: options.showPath
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
// Mark as seen if requested
|
|
1515
|
+
if (options.markAsSeen && (!formattedMessage.flags || !formattedMessage.flags.includes('\\Seen'))) {
|
|
1516
|
+
try {
|
|
1517
|
+
let response = await this.updateMessage(emailId, { flags: { add: ['\\Seen'] } });
|
|
1518
|
+
if (response?.flags?.result) {
|
|
1519
|
+
formattedMessage.flags = response?.flags?.result;
|
|
1520
|
+
}
|
|
1521
|
+
} catch (err) {
|
|
1522
|
+
this.logger.debug({ msg: 'Failed to mark message as Seen', message: emailId, err });
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// Generate web-safe HTML if requested
|
|
1527
|
+
if (options.preProcessHtml && formattedMessage.text && (formattedMessage.text.html || formattedMessage.text.plain)) {
|
|
1528
|
+
formattedMessage.text.html = mimeHtml({
|
|
1529
|
+
html: formattedMessage.text.html,
|
|
1530
|
+
text: formattedMessage.text.plain
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
formattedMessage.text.webSafe = true;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// Embed attached images as data URLs
|
|
1537
|
+
if (options.embedAttachedImages && formattedMessage.text?.html && formattedMessage.attachments) {
|
|
1538
|
+
let attachmentMap = new Map();
|
|
1539
|
+
|
|
1540
|
+
// Collect all referenced inline attachments
|
|
1541
|
+
for (let attachment of formattedMessage.attachments) {
|
|
1542
|
+
let contentId = attachment.contentId && attachment.contentId.replace(/^<|>$/g, '');
|
|
1543
|
+
if (contentId && !attachmentMap.has(contentId) && formattedMessage.text.html.indexOf(`cid:${contentId}`) >= 0) {
|
|
1544
|
+
attachmentMap.set(contentId, {
|
|
1545
|
+
attachment,
|
|
1546
|
+
content: await this.getAttachmentContent(attachment.id, {
|
|
1547
|
+
returnBase64: true
|
|
1548
|
+
})
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// Replace cid: references with data URLs
|
|
1554
|
+
formattedMessage.text.html = formattedMessage.text.html.replace(/\bcid:([^"'\s>]+)/g, (fullMatch, cidMatch) => {
|
|
1555
|
+
if (attachmentMap.has(cidMatch)) {
|
|
1556
|
+
let { content } = attachmentMap.get(cidMatch);
|
|
1557
|
+
if (content.content) {
|
|
1558
|
+
return `data:${content.contentType || 'application/octet-stream'};base64,${content.content}`;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
return fullMatch;
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
if (specialUse) {
|
|
1566
|
+
formattedMessage.messageSpecialUse = specialUse;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
return formattedMessage;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* Get message text content only
|
|
1574
|
+
* More efficient than getMessage when only text is needed
|
|
1575
|
+
*/
|
|
1576
|
+
async getText(textId, options) {
|
|
1577
|
+
options = options || {};
|
|
1578
|
+
|
|
1579
|
+
await this.prepare();
|
|
1580
|
+
|
|
1581
|
+
let messageData;
|
|
1582
|
+
|
|
1583
|
+
// Prepare request options with appropriate Prefer header for body content type
|
|
1584
|
+
let requestOptions = {};
|
|
1585
|
+
if (options.textType === 'plain') {
|
|
1586
|
+
// Request plain text format from MS Graph API
|
|
1587
|
+
requestOptions.headers = {
|
|
1588
|
+
Prefer: 'outlook.body-content-type="text"'
|
|
1589
|
+
};
|
|
1590
|
+
} else if (options.textType === 'html') {
|
|
1591
|
+
// Request HTML format from MS Graph API
|
|
1592
|
+
requestOptions.headers = {
|
|
1593
|
+
Prefer: 'outlook.body-content-type="html"'
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
try {
|
|
1598
|
+
messageData = await this.request(
|
|
1599
|
+
`/${this.oauth2UserPath}/messages/${textId}`,
|
|
1600
|
+
'get',
|
|
1601
|
+
{
|
|
1602
|
+
$select: 'body'
|
|
1603
|
+
},
|
|
1604
|
+
requestOptions
|
|
1605
|
+
);
|
|
1606
|
+
} catch (err) {
|
|
1607
|
+
switch (err.oauthRequest?.status) {
|
|
1608
|
+
case 404: {
|
|
1609
|
+
let error = new Error('Unknown message');
|
|
1610
|
+
error.info = {
|
|
1611
|
+
response: `Message does not exist`
|
|
1612
|
+
};
|
|
1613
|
+
error.code = 'NotFound';
|
|
1614
|
+
error.statusCode = 404;
|
|
1615
|
+
throw error;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
case 400: {
|
|
1619
|
+
let error = new Error('Invalid request');
|
|
1620
|
+
error.info = {
|
|
1621
|
+
response: err.oauthRequest?.response?.error?.message || `Invalid request`
|
|
1622
|
+
};
|
|
1623
|
+
error.code = err.oauthRequest?.response?.error?.code || 'InvalidRequest';
|
|
1624
|
+
error.statusCode = 400;
|
|
1625
|
+
throw error;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
default:
|
|
1629
|
+
this.logger.error({
|
|
1630
|
+
msg: 'Failed to fetch message data',
|
|
1631
|
+
emailId: textId,
|
|
1632
|
+
err
|
|
1633
|
+
});
|
|
1634
|
+
throw err;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
let response = {};
|
|
1639
|
+
|
|
1640
|
+
if (options.textType && options.textType !== '*') {
|
|
1641
|
+
response[options.textType] = '';
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
if (messageData?.body?.contentType) {
|
|
1645
|
+
let textContent = messageData.body.content || '';
|
|
1646
|
+
let textContentType;
|
|
1647
|
+
switch (messageData.body.contentType) {
|
|
1648
|
+
case 'text':
|
|
1649
|
+
textContentType = 'plain';
|
|
1650
|
+
break;
|
|
1651
|
+
|
|
1652
|
+
case 'html':
|
|
1653
|
+
default:
|
|
1654
|
+
textContentType = messageData.body.contentType;
|
|
1655
|
+
break;
|
|
1656
|
+
}
|
|
1657
|
+
if ([textContentType, '*'].includes(options.textType)) {
|
|
1658
|
+
response[textContentType] = textContent;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
response.hasMore = false;
|
|
1663
|
+
|
|
1664
|
+
return response;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
/**
|
|
1668
|
+
* Convert EmailEngine message format to Graph API message format
|
|
1669
|
+
* Handles all message properties including headers and attachments
|
|
1670
|
+
*/
|
|
1671
|
+
convertMessageToUploadObject(emailObject) {
|
|
1672
|
+
let messageUploadObj = {};
|
|
1673
|
+
|
|
1674
|
+
for (let key of Object.keys(emailObject)) {
|
|
1675
|
+
switch (key) {
|
|
1676
|
+
case 'from':
|
|
1677
|
+
messageUploadObj.from = {
|
|
1678
|
+
emailAddress: {
|
|
1679
|
+
address: emailObject.from.address
|
|
1680
|
+
}
|
|
1681
|
+
};
|
|
1682
|
+
if (emailObject.from.name) {
|
|
1683
|
+
messageUploadObj.from.emailAddress.name = emailObject.from.name;
|
|
1684
|
+
}
|
|
1685
|
+
break;
|
|
1686
|
+
|
|
1687
|
+
case 'to':
|
|
1688
|
+
case 'cc':
|
|
1689
|
+
case 'bcc': {
|
|
1690
|
+
let entryKey = `${key}Recipients`;
|
|
1691
|
+
messageUploadObj[entryKey] = [];
|
|
1692
|
+
for (let addressEntry of emailObject[key] || []) {
|
|
1693
|
+
let addressObj = {
|
|
1694
|
+
emailAddress: {
|
|
1695
|
+
address: addressEntry.address
|
|
1696
|
+
}
|
|
1697
|
+
};
|
|
1698
|
+
if (addressEntry.name) {
|
|
1699
|
+
addressObj.emailAddress.name = addressEntry.name;
|
|
1700
|
+
}
|
|
1701
|
+
messageUploadObj[entryKey].push(addressObj);
|
|
1702
|
+
}
|
|
1703
|
+
break;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
case 'subject':
|
|
1707
|
+
messageUploadObj[key] = emailObject[key];
|
|
1708
|
+
break;
|
|
1709
|
+
|
|
1710
|
+
case 'headers': {
|
|
1711
|
+
messageUploadObj.internetMessageHeaders = messageUploadObj.internetMessageHeaders || [];
|
|
1712
|
+
for (let header of Object.keys(emailObject.headers)) {
|
|
1713
|
+
messageUploadObj.internetMessageHeaders.push({
|
|
1714
|
+
name: header.toLowerCase(),
|
|
1715
|
+
value: emailObject.headers[header]
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
break;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
case 'headerLines': {
|
|
1722
|
+
messageUploadObj.internetMessageHeaders = messageUploadObj.internetMessageHeaders || [];
|
|
1723
|
+
for (let i = emailObject.headerLines.length - 1; i >= 0; i--) {
|
|
1724
|
+
let header = emailObject.headerLines[i];
|
|
1725
|
+
// Skip headers that Graph API handles automatically
|
|
1726
|
+
if (
|
|
1727
|
+
[
|
|
1728
|
+
'date',
|
|
1729
|
+
'content-transfer-encoding',
|
|
1730
|
+
'from',
|
|
1731
|
+
'to',
|
|
1732
|
+
'cc',
|
|
1733
|
+
'bcc',
|
|
1734
|
+
'subject',
|
|
1735
|
+
'mime-version',
|
|
1736
|
+
'content-type',
|
|
1737
|
+
'content-disposition',
|
|
1738
|
+
'message-id',
|
|
1739
|
+
'content-id'
|
|
1740
|
+
].includes(header.key) ||
|
|
1741
|
+
// MS Graph API only allows up to 5 custom headers
|
|
1742
|
+
messageUploadObj.internetMessageHeaders.length >= 5
|
|
1743
|
+
) {
|
|
1744
|
+
continue;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
let name = header.key;
|
|
1748
|
+
let value = header.value ? header.value.substring(header.value.indexOf(':') + 1).trim() : '';
|
|
1749
|
+
if (name && value) {
|
|
1750
|
+
messageUploadObj.internetMessageHeaders.unshift({
|
|
1751
|
+
name,
|
|
1752
|
+
value
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
break;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
case 'messageId':
|
|
1760
|
+
messageUploadObj.internetMessageId = emailObject.messageId;
|
|
1761
|
+
break;
|
|
1762
|
+
|
|
1763
|
+
case 'attachments': {
|
|
1764
|
+
messageUploadObj.attachments = [];
|
|
1765
|
+
let attachmentCounter = 0;
|
|
1766
|
+
for (let attachment of emailObject.attachments) {
|
|
1767
|
+
let attachmentEntry = {
|
|
1768
|
+
'@odata.type': '#microsoft.graph.fileAttachment'
|
|
1769
|
+
};
|
|
1770
|
+
if (attachment.filename) {
|
|
1771
|
+
attachmentEntry.name = attachment.filename;
|
|
1772
|
+
} else {
|
|
1773
|
+
// generate a filename based on contentType as name is a required value
|
|
1774
|
+
let ext = detectExtension(attachment.contentType);
|
|
1775
|
+
attachmentEntry.name = `attachment_${++attachmentCounter}.${ext}`;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
attachmentEntry.contentType = attachment.contentType || detectMimeType(attachment.filename) || 'application/octet-stream';
|
|
1779
|
+
attachmentEntry.contentBytes = attachment.content;
|
|
1780
|
+
if (attachment.cid) {
|
|
1781
|
+
// make sure that cid links to not use <content-id> format, otherwise this will be replaced
|
|
1782
|
+
attachmentEntry.contentId = attachment.cid.replace(/^[\s<]*|[\s>]*$/g, '');
|
|
1783
|
+
if (emailObject.html?.indexOf(attachmentEntry.contentId) >= 0) {
|
|
1784
|
+
attachmentEntry.isInline = true;
|
|
1785
|
+
emailObject.html.replace(new RegExp(`cid:<${attachment.cid}>`, 'g'), `cid:${attachment.cid}`);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
if (attachment.contentDisposition === 'inline') {
|
|
1789
|
+
attachmentEntry.isInline = true;
|
|
1790
|
+
}
|
|
1791
|
+
messageUploadObj.attachments.push(attachmentEntry);
|
|
1792
|
+
}
|
|
1793
|
+
break;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
// Set message body content
|
|
1799
|
+
if (emailObject.html) {
|
|
1800
|
+
messageUploadObj.body = {
|
|
1801
|
+
contentType: 'html',
|
|
1802
|
+
content: emailObject.html
|
|
1803
|
+
};
|
|
1804
|
+
} else if (emailObject.text) {
|
|
1805
|
+
messageUploadObj.body = {
|
|
1806
|
+
contentType: 'text',
|
|
1807
|
+
content: emailObject.text
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
if (messageUploadObj.internetMessageHeaders && !messageUploadObj.internetMessageHeaders.length) {
|
|
1812
|
+
delete messageUploadObj.internetMessageHeaders;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
return messageUploadObj;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/**
|
|
1819
|
+
* Upload a message to a folder (typically for drafts)
|
|
1820
|
+
* Supports custom flags and internal date via extended properties
|
|
1821
|
+
*/
|
|
1822
|
+
async uploadMessage(data) {
|
|
1823
|
+
await this.prepare();
|
|
1824
|
+
|
|
1825
|
+
let path = [].concat(data.path || []).join('/');
|
|
1826
|
+
|
|
1827
|
+
let targetFolder = await this.resolveFolder(path);
|
|
1828
|
+
if (!targetFolder) {
|
|
1829
|
+
let error = new Error('Upload failed');
|
|
1830
|
+
error.info = {
|
|
1831
|
+
response: 'Not able to find mailbox folder'
|
|
1832
|
+
};
|
|
1833
|
+
error.code = 'NotFound';
|
|
1834
|
+
error.statusCode = 404;
|
|
1835
|
+
throw error;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// Parse raw message into structured format
|
|
1839
|
+
let { emailObject, messageId, referencedMessage, documentStoreUsed } = await this.prepareRawMessage(data, {
|
|
1840
|
+
returnObject: true
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
let messageUploadObj = this.convertMessageToUploadObject(emailObject);
|
|
1844
|
+
|
|
1845
|
+
messageUploadObj.singleValueExtendedProperties = [];
|
|
1846
|
+
|
|
1847
|
+
// Set message flags using MAPI properties
|
|
1848
|
+
// https://learn.microsoft.com/en-us/office/client-developer/outlook/mapi/pidtagmessageflags-canonical-property
|
|
1849
|
+
// PR_MESSAGE_FLAGS
|
|
1850
|
+
if (data.flags) {
|
|
1851
|
+
let flagValue = 0;
|
|
1852
|
+
|
|
1853
|
+
for (let flag of data.flags || []) {
|
|
1854
|
+
switch (flag) {
|
|
1855
|
+
case '\\Seen': // mfRead, MSGFLAG_READ
|
|
1856
|
+
flagValue |= 0x0001;
|
|
1857
|
+
break;
|
|
1858
|
+
case '\\Draft': // mfUnsent, MSGFLAG_UNSENT
|
|
1859
|
+
flagValue |= 0x0008;
|
|
1860
|
+
break;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
messageUploadObj.singleValueExtendedProperties.push({ id: 'Integer 0x0E07', value: flagValue.toString(10) });
|
|
1865
|
+
} else {
|
|
1866
|
+
messageUploadObj.singleValueExtendedProperties.push({ id: 'Integer 0x0E07', value: '0' });
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// Set internal date using MAPI property
|
|
1870
|
+
// https://learn.microsoft.com/en-us/office/client-developer/outlook/mapi/pidtagmessagedeliverytime-canonical-property
|
|
1871
|
+
//PR_MESSAGE_DELIVERY_TIME
|
|
1872
|
+
if (data.internalDate) {
|
|
1873
|
+
messageUploadObj.singleValueExtendedProperties.push({ id: 'SystemTime 0x0E06', value: data.internalDate.toISOString() });
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
let messageData;
|
|
1877
|
+
try {
|
|
1878
|
+
messageData = await this.request(`/${this.oauth2UserPath}/mailFolders/${targetFolder.id}/messages`, 'post', messageUploadObj);
|
|
1879
|
+
} catch (err) {
|
|
1880
|
+
switch (err.oauthRequest?.status) {
|
|
1881
|
+
case 400: {
|
|
1882
|
+
let error = new Error('Invalid message format');
|
|
1883
|
+
error.info = {
|
|
1884
|
+
response: err.oauthRequest?.response?.error?.message || `Invalid message format`
|
|
1885
|
+
};
|
|
1886
|
+
error.code = err.oauthRequest?.response?.error?.code || 'InvalidMessage';
|
|
1887
|
+
error.statusCode = 500; // do not retry sending
|
|
1888
|
+
throw error;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
default:
|
|
1892
|
+
this.logger.error({
|
|
1893
|
+
msg: 'Failed to upload message',
|
|
1894
|
+
messageId,
|
|
1895
|
+
err
|
|
1896
|
+
});
|
|
1897
|
+
throw err;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
let response = {
|
|
1902
|
+
id: messageData?.id,
|
|
1903
|
+
path: targetFolder.pathName,
|
|
1904
|
+
messageId: messageData?.internetMessageId || messageId
|
|
1905
|
+
};
|
|
1906
|
+
|
|
1907
|
+
// Include reference info if this was a reply/forward
|
|
1908
|
+
if (data.reference && data.reference.message) {
|
|
1909
|
+
response.reference = {
|
|
1910
|
+
message: data.reference.message,
|
|
1911
|
+
documentStore: documentStoreUsed,
|
|
1912
|
+
success: referencedMessage ? true : false
|
|
1913
|
+
};
|
|
1914
|
+
|
|
1915
|
+
if (!referencedMessage) {
|
|
1916
|
+
response.reference.error = 'Referenced message was not found';
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
return response;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
/**
|
|
1924
|
+
* Send an email message
|
|
1925
|
+
* Can use either Graph API sendMail endpoint or fall back to SMTP
|
|
1926
|
+
*/
|
|
1927
|
+
async submitMessage(data) {
|
|
1928
|
+
await this.prepare();
|
|
1929
|
+
|
|
1930
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
1931
|
+
if (!accountData.smtp && !accountData.oauth2 && !data.gateway) {
|
|
1932
|
+
// can not make connection
|
|
1933
|
+
let err = new Error('SMTP configuration not found');
|
|
1934
|
+
err.code = 'SMTPUnavailable';
|
|
1935
|
+
err.statusCode = 404;
|
|
1936
|
+
throw err;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
let { raw, messageId, queueId, job: jobData, envelope } = data;
|
|
1940
|
+
|
|
1941
|
+
if (raw?.buffer) {
|
|
1942
|
+
// convert from a Uint8Array to a Buffer
|
|
1943
|
+
raw = Buffer.from(raw);
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// Check if we should use SMTP gateway instead
|
|
1947
|
+
let gatewayData;
|
|
1948
|
+
let gatewayObject;
|
|
1949
|
+
if (data.gateway) {
|
|
1950
|
+
gatewayObject = new Gateway({ gateway: data.gateway, redis: this.redis, secret: this.secret });
|
|
1951
|
+
try {
|
|
1952
|
+
gatewayData = await gatewayObject.loadGatewayData();
|
|
1953
|
+
} catch (err) {
|
|
1954
|
+
this.logger.info({ msg: 'Failed to load gateway data', messageId: data.messageId, gateway: data.gateway, err });
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
if (gatewayData) {
|
|
1959
|
+
// Send via SMTP (fall back to base class implementation)
|
|
1960
|
+
return await super.submitMessage(data);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// Get job entry for progress updates
|
|
1964
|
+
const submitJobEntry = await this.submitQueue.getJob(jobData.id);
|
|
1965
|
+
if (!submitJobEntry) {
|
|
1966
|
+
// already failed?
|
|
1967
|
+
this.logger.error({
|
|
1968
|
+
msg: 'Submit job was not found',
|
|
1969
|
+
job: jobData.id
|
|
1970
|
+
});
|
|
1971
|
+
return false;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// Check if structured format should be used (opt-in)
|
|
1975
|
+
// Default is raw MIME to preserve backward compatibility and calendar invites
|
|
1976
|
+
if (data.useStructuredFormat) {
|
|
1977
|
+
// Parse raw message into structured format to properly handle 'from' address
|
|
1978
|
+
// Microsoft Graph API ignores the From header in raw MIME messages and uses
|
|
1979
|
+
// the authenticated user's address instead. By parsing and sending as structured
|
|
1980
|
+
// JSON with explicit 'from' field, we enable proper "send on behalf of" behavior
|
|
1981
|
+
let emailObject;
|
|
1982
|
+
try {
|
|
1983
|
+
let parseResult = await this.prepareRawMessage(data, {
|
|
1984
|
+
returnObject: true
|
|
1985
|
+
});
|
|
1986
|
+
emailObject = parseResult.emailObject;
|
|
1987
|
+
} catch (err) {
|
|
1988
|
+
this.logger.error({
|
|
1989
|
+
msg: 'Failed to parse raw message',
|
|
1990
|
+
messageId,
|
|
1991
|
+
err
|
|
1992
|
+
});
|
|
1993
|
+
throw err;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
// Convert to Graph API message format
|
|
1997
|
+
let messagePayload = this.convertMessageToUploadObject(emailObject);
|
|
1998
|
+
|
|
1999
|
+
try {
|
|
2000
|
+
// Send via Graph API using structured message format
|
|
2001
|
+
// This ensures the 'from' field is properly respected for shared mailboxes
|
|
2002
|
+
// Note: sendMail returns 202 Accepted with empty body
|
|
2003
|
+
await this.request(
|
|
2004
|
+
`/${this.oauth2UserPath}/sendMail`,
|
|
2005
|
+
'post',
|
|
2006
|
+
{
|
|
2007
|
+
message: messagePayload
|
|
2008
|
+
},
|
|
2009
|
+
{
|
|
2010
|
+
returnText: true
|
|
2011
|
+
}
|
|
2012
|
+
);
|
|
2013
|
+
} catch (err) {
|
|
2014
|
+
switch (err.oauthRequest?.status) {
|
|
2015
|
+
case 400: {
|
|
2016
|
+
let error = new Error('Invalid message format');
|
|
2017
|
+
error.info = {
|
|
2018
|
+
response: err.oauthRequest?.response?.error?.message || `Invalid message format`
|
|
2019
|
+
};
|
|
2020
|
+
error.code = err.oauthRequest?.response?.error?.code || 'InvalidMessage';
|
|
2021
|
+
error.statusCode = 500; // do not retry sending
|
|
2022
|
+
throw error;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
default:
|
|
2026
|
+
this.logger.error({
|
|
2027
|
+
msg: 'Failed to submit message',
|
|
2028
|
+
messageId,
|
|
2029
|
+
err
|
|
2030
|
+
});
|
|
2031
|
+
throw err;
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
} else {
|
|
2035
|
+
// Use raw MIME format (default)
|
|
2036
|
+
// Preserves calendar invites and special MIME types, but ignores from field
|
|
2037
|
+
try {
|
|
2038
|
+
await this.request(`/${this.oauth2UserPath}/sendMail`, 'post', Buffer.from(raw.toString('base64')), {
|
|
2039
|
+
contentType: 'text/plain',
|
|
2040
|
+
returnText: true
|
|
2041
|
+
});
|
|
2042
|
+
} catch (err) {
|
|
2043
|
+
switch (err.oauthRequest?.status) {
|
|
2044
|
+
case 400: {
|
|
2045
|
+
let error = new Error('Invalid message format');
|
|
2046
|
+
error.info = {
|
|
2047
|
+
response: err.oauthRequest?.response?.error?.message || `Invalid message format`
|
|
2048
|
+
};
|
|
2049
|
+
error.code = err.oauthRequest?.response?.error?.code || 'InvalidMessage';
|
|
2050
|
+
error.statusCode = 500; // do not retry sending
|
|
2051
|
+
throw error;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
default:
|
|
2055
|
+
this.logger.error({
|
|
2056
|
+
msg: 'Failed to submit message',
|
|
2057
|
+
messageId,
|
|
2058
|
+
err
|
|
2059
|
+
});
|
|
2060
|
+
throw err;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// Update job progress
|
|
2066
|
+
try {
|
|
2067
|
+
await submitJobEntry.updateProgress({
|
|
2068
|
+
status: 'smtp-completed',
|
|
2069
|
+
messageId,
|
|
2070
|
+
originalMessageId: messageId
|
|
2071
|
+
});
|
|
2072
|
+
} catch (err) {
|
|
2073
|
+
// ignore
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// Send completion notification
|
|
2077
|
+
await this.notify(false, EMAIL_SENT_NOTIFY, {
|
|
2078
|
+
messageId,
|
|
2079
|
+
originalMessageId: messageId,
|
|
2080
|
+
queueId,
|
|
2081
|
+
envelope
|
|
2082
|
+
});
|
|
2083
|
+
|
|
2084
|
+
// Mark as successful in Redis for webhook feedback
|
|
2085
|
+
if (data.feedbackKey) {
|
|
2086
|
+
await this.redis
|
|
2087
|
+
.multi()
|
|
2088
|
+
.hset(data.feedbackKey, 'success', 'true')
|
|
2089
|
+
.expire(1 * 60 * 60);
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
return {
|
|
2093
|
+
messageId
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
/**
|
|
2098
|
+
* Create a new mailbox folder
|
|
2099
|
+
* Supports nested folders with path syntax
|
|
2100
|
+
*/
|
|
2101
|
+
async createMailbox(path) {
|
|
2102
|
+
await this.prepare();
|
|
2103
|
+
|
|
2104
|
+
path = [].concat(path || []).join('/');
|
|
2105
|
+
|
|
2106
|
+
let subPaths = path.split('/');
|
|
2107
|
+
|
|
2108
|
+
let displayName = subPaths.pop();
|
|
2109
|
+
let parentPath = subPaths.join('/');
|
|
2110
|
+
|
|
2111
|
+
// Resolve parent folder if specified
|
|
2112
|
+
let parentFolder;
|
|
2113
|
+
if (parentPath) {
|
|
2114
|
+
parentFolder = await this.resolveFolder(parentPath);
|
|
2115
|
+
if (!parentFolder) {
|
|
2116
|
+
let error = new Error('Create failed');
|
|
2117
|
+
error.info = {
|
|
2118
|
+
response: 'Not able to find parent folder'
|
|
2119
|
+
};
|
|
2120
|
+
error.code = 'NotFound';
|
|
2121
|
+
error.statusCode = 404;
|
|
2122
|
+
throw error;
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
// Build API URL based on whether this is a root or child folder
|
|
2127
|
+
let reqUrl;
|
|
2128
|
+
if (parentFolder) {
|
|
2129
|
+
// child folder
|
|
2130
|
+
reqUrl = `/${this.oauth2UserPath}/mailFolders/${parentFolder.id}/childFolders`;
|
|
2131
|
+
} else {
|
|
2132
|
+
// root folder
|
|
2133
|
+
reqUrl = `/${this.oauth2UserPath}/mailFolders`;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
let mailbox;
|
|
2137
|
+
try {
|
|
2138
|
+
mailbox = await this.request(reqUrl, 'post', {
|
|
2139
|
+
displayName,
|
|
2140
|
+
isHidden: false
|
|
2141
|
+
});
|
|
2142
|
+
if (!mailbox) {
|
|
2143
|
+
throw new Error('Failed to create mailbox');
|
|
2144
|
+
}
|
|
2145
|
+
} catch (err) {
|
|
2146
|
+
switch (err?.oauthRequest?.response?.error?.code) {
|
|
2147
|
+
case 'ErrorFolderExists':
|
|
2148
|
+
// already exists
|
|
2149
|
+
return {
|
|
2150
|
+
path,
|
|
2151
|
+
created: false
|
|
2152
|
+
};
|
|
2153
|
+
|
|
2154
|
+
default: {
|
|
2155
|
+
let error = new Error('Create failed');
|
|
2156
|
+
error.info = {
|
|
2157
|
+
response: err?.oauthRequest?.response?.error?.message
|
|
2158
|
+
};
|
|
2159
|
+
error.code = err?.oauthRequest?.response?.error?.code;
|
|
2160
|
+
error.statusCode = 400;
|
|
2161
|
+
throw error;
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// Refresh cache asynchronously
|
|
2167
|
+
setImmediate(() => {
|
|
2168
|
+
// refresh mailbox listing cache after changes
|
|
2169
|
+
this.listMailboxes().catch(err => {
|
|
2170
|
+
this.logger.error({ msg: 'Failed to list mailboxes', err });
|
|
2171
|
+
});
|
|
2172
|
+
});
|
|
2173
|
+
|
|
2174
|
+
return {
|
|
2175
|
+
mailboxId: mailbox.id,
|
|
2176
|
+
path: []
|
|
2177
|
+
.concat(parentFolder?.pathName || [])
|
|
2178
|
+
.concat(mailbox.displayName)
|
|
2179
|
+
.join('/'),
|
|
2180
|
+
created: true
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
/**
|
|
2185
|
+
* Modifies a mailbox folder (rename only, subscription is ignored for Outlook)
|
|
2186
|
+
* @param {string} path - Current path
|
|
2187
|
+
* @param {string} newPath - New path (optional)
|
|
2188
|
+
* @param {boolean} subscribed - Ignored for MS Graph API
|
|
2189
|
+
* @returns {Object} Modify result
|
|
2190
|
+
*/
|
|
2191
|
+
async modifyMailbox(path, newPath, subscribed) {
|
|
2192
|
+
// MS Graph API does not support subscription management, so we ignore the subscribed parameter
|
|
2193
|
+
// If no newPath provided, just return the current path without changes
|
|
2194
|
+
if (!newPath) {
|
|
2195
|
+
return {
|
|
2196
|
+
path: [].concat(path || []).join('/'),
|
|
2197
|
+
renamed: false
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
return await this.renameMailbox(path, newPath);
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
/**
|
|
2205
|
+
* Rename and/or move a mailbox folder
|
|
2206
|
+
* Handles both simple renames and moves to different parent folders
|
|
2207
|
+
*/
|
|
2208
|
+
async renameMailbox(path, newPath) {
|
|
2209
|
+
await this.prepare();
|
|
2210
|
+
|
|
2211
|
+
path = [].concat(path || []).join('/');
|
|
2212
|
+
newPath = [].concat(newPath || []).join('/');
|
|
2213
|
+
|
|
2214
|
+
let sourceFolder = await this.resolveFolder(path);
|
|
2215
|
+
if (!sourceFolder) {
|
|
2216
|
+
let error = new Error('Rename failed');
|
|
2217
|
+
error.info = {
|
|
2218
|
+
response: 'Not able to find mailbox folder'
|
|
2219
|
+
};
|
|
2220
|
+
error.code = 'NotFound';
|
|
2221
|
+
error.statusCode = 404;
|
|
2222
|
+
throw error;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
// Parse source and destination paths
|
|
2226
|
+
let sourceParts = sourceFolder.pathName.split('/');
|
|
2227
|
+
let sourceName = sourceParts.pop();
|
|
2228
|
+
let sourceParentPath = sourceParts.join('/');
|
|
2229
|
+
|
|
2230
|
+
let destinationFolder = await this.resolveFolder(newPath, { pathNameOnly: true });
|
|
2231
|
+
|
|
2232
|
+
let destinationParts = destinationFolder.pathName.split('/');
|
|
2233
|
+
let destinationName = destinationParts.pop();
|
|
2234
|
+
let destinationParentPath = destinationParts.join('/');
|
|
2235
|
+
|
|
2236
|
+
let destinationParentFolder;
|
|
2237
|
+
if (sourceParentPath !== destinationParentPath) {
|
|
2238
|
+
destinationParentFolder = await this.resolveFolder(destinationParentPath);
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
// Step 1. Rename if name changed
|
|
2242
|
+
if (sourceName !== destinationName) {
|
|
2243
|
+
let mailbox;
|
|
2244
|
+
try {
|
|
2245
|
+
mailbox = await this.request(`/${this.oauth2UserPath}/mailFolders/${sourceFolder.id}`, 'patch', {
|
|
2246
|
+
displayName: destinationName
|
|
2247
|
+
});
|
|
2248
|
+
if (!mailbox) {
|
|
2249
|
+
throw new Error('Failed to rename mailbox');
|
|
2250
|
+
}
|
|
2251
|
+
} catch (err) {
|
|
2252
|
+
this.logger.error({
|
|
2253
|
+
msg: 'Failed to rename folder',
|
|
2254
|
+
mailboxId: sourceFolder.id,
|
|
2255
|
+
path: sourceFolder.pathName,
|
|
2256
|
+
newPath: destinationFolder.pathName,
|
|
2257
|
+
err
|
|
2258
|
+
});
|
|
2259
|
+
throw err;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// Step 2. Move if parent changed
|
|
2264
|
+
if (destinationParentFolder) {
|
|
2265
|
+
let mailbox;
|
|
2266
|
+
try {
|
|
2267
|
+
mailbox = await this.request(`/${this.oauth2UserPath}/mailFolders/${sourceFolder.id}/move`, 'post', {
|
|
2268
|
+
destinationId: destinationParentFolder.id
|
|
2269
|
+
});
|
|
2270
|
+
if (!mailbox) {
|
|
2271
|
+
throw new Error('Failed to move mailbox');
|
|
2272
|
+
}
|
|
2273
|
+
} catch (err) {
|
|
2274
|
+
this.logger.error({
|
|
2275
|
+
msg: 'Failed to move folder',
|
|
2276
|
+
mailboxId: sourceFolder.id,
|
|
2277
|
+
path: sourceFolder.pathName,
|
|
2278
|
+
newPath: destinationFolder.pathName,
|
|
2279
|
+
err
|
|
2280
|
+
});
|
|
2281
|
+
throw err;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
// Refresh cache asynchronously
|
|
2286
|
+
setImmediate(() => {
|
|
2287
|
+
// refresh mailbox listing cache after changes
|
|
2288
|
+
this.listMailboxes().catch(err => {
|
|
2289
|
+
this.logger.error({ msg: 'Failed to list mailboxes', err });
|
|
2290
|
+
});
|
|
2291
|
+
});
|
|
2292
|
+
|
|
2293
|
+
return {
|
|
2294
|
+
mailboxId: sourceFolder.id,
|
|
2295
|
+
path: sourceFolder.pathName,
|
|
2296
|
+
newPath: destinationFolder.pathName,
|
|
2297
|
+
renamed: true
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
/**
|
|
2302
|
+
* Delete a mailbox folder
|
|
2303
|
+
*/
|
|
2304
|
+
async deleteMailbox(path) {
|
|
2305
|
+
await this.prepare();
|
|
2306
|
+
|
|
2307
|
+
path = [].concat(path || []).join('/');
|
|
2308
|
+
|
|
2309
|
+
let folder = await this.resolveFolder(path);
|
|
2310
|
+
if (!folder) {
|
|
2311
|
+
let error = new Error('Delete failed');
|
|
2312
|
+
error.info = {
|
|
2313
|
+
response: 'Not able to find mailbox folder'
|
|
2314
|
+
};
|
|
2315
|
+
error.code = 'NotFound';
|
|
2316
|
+
error.statusCode = 404;
|
|
2317
|
+
throw error;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
try {
|
|
2321
|
+
await this.request(`/${this.oauth2UserPath}/mailFolders/${folder.id}`, 'delete', Buffer.alloc(0), { returnText: true });
|
|
2322
|
+
} catch (err) {
|
|
2323
|
+
this.logger.error({
|
|
2324
|
+
msg: 'Failed to delete folder',
|
|
2325
|
+
mailboxId: folder.id,
|
|
2326
|
+
path: folder.pathName,
|
|
2327
|
+
err
|
|
2328
|
+
});
|
|
2329
|
+
throw err;
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
// Refresh cache asynchronously
|
|
2333
|
+
setImmediate(() => {
|
|
2334
|
+
// refresh mailbox listing cache after changes
|
|
2335
|
+
this.listMailboxes().catch(err => {
|
|
2336
|
+
this.logger.error({ msg: 'Failed to list mailboxes', err });
|
|
2337
|
+
});
|
|
2338
|
+
});
|
|
2339
|
+
|
|
2340
|
+
return {
|
|
2341
|
+
mailboxId: folder.id,
|
|
2342
|
+
path: folder.pathName,
|
|
2343
|
+
deleted: true
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
/**
|
|
2348
|
+
* Handle external notification (webhook) - triggers sync of pending changes
|
|
2349
|
+
*/
|
|
2350
|
+
async externalNotify() {
|
|
2351
|
+
this.triggerSync();
|
|
2352
|
+
|
|
2353
|
+
return true;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
// PRIVATE METHODS
|
|
2357
|
+
|
|
2358
|
+
/**
|
|
2359
|
+
* Get Redis key for account data
|
|
2360
|
+
*/
|
|
2361
|
+
getAccountKey() {
|
|
2362
|
+
return `${REDIS_PREFIX}iad:${this.account}`;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
/**
|
|
2366
|
+
* Get Redis key for account cache
|
|
2367
|
+
*/
|
|
2368
|
+
getAccountCacheKey() {
|
|
2369
|
+
return `${REDIS_PREFIX}iac:${this.account}`;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
/**
|
|
2373
|
+
* Get or create account object
|
|
2374
|
+
*/
|
|
2375
|
+
async getAccount() {
|
|
2376
|
+
if (this.accountObject) {
|
|
2377
|
+
return this.accountObject;
|
|
2378
|
+
}
|
|
2379
|
+
this.accountObject = new Account({ redis: this.redis, account: this.account, secret: await getSecret() });
|
|
2380
|
+
|
|
2381
|
+
return this.accountObject;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
/**
|
|
2385
|
+
* Set up delegated account access if configured
|
|
2386
|
+
* Allows accessing shared mailboxes or other user mailboxes
|
|
2387
|
+
*/
|
|
2388
|
+
async prepareDelegatedAccount() {
|
|
2389
|
+
if (this.delegatedAccountObject) {
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
2394
|
+
|
|
2395
|
+
if (accountData?.oauth2?.auth?.delegatedUser && accountData?.oauth2?.auth?.delegatedAccount) {
|
|
2396
|
+
await this.getDelegatedAccount(accountData);
|
|
2397
|
+
if (this.delegatedAccountObject) {
|
|
2398
|
+
// Update API path to target delegated user
|
|
2399
|
+
this.oauth2UserPath = `users/${encodeURIComponent(accountData?.oauth2?.auth?.delegatedUser)}`;
|
|
2400
|
+
}
|
|
2401
|
+
} else if (accountData?.oauth2?.auth?.delegatedUser) {
|
|
2402
|
+
// Created with delegated:true
|
|
2403
|
+
// Update API path to target delegated user
|
|
2404
|
+
this.oauth2UserPath = `users/${encodeURIComponent(accountData?.oauth2?.auth?.delegatedUser)}`;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
/**
|
|
2409
|
+
* Get OAuth2 access token from account or delegated account
|
|
2410
|
+
*/
|
|
2411
|
+
async getToken() {
|
|
2412
|
+
let tokenData;
|
|
2413
|
+
try {
|
|
2414
|
+
tokenData = await (this.delegatedAccountObject || this.accountObject).getActiveAccessTokenData();
|
|
2415
|
+
if (!['init', 'connecting', 'connected'].includes(this.state)) {
|
|
2416
|
+
// We're in an error state (authenticationError, disconnected, etc.)
|
|
2417
|
+
// But we just got a valid token, so we've recovered
|
|
2418
|
+
this.state = 'connected';
|
|
2419
|
+
await this.setStateVal();
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
// Track successful token refresh (only if token was actually refreshed, not cached)
|
|
2423
|
+
if (!tokenData.cached) {
|
|
2424
|
+
metricsMeta({ account: this.account }, this.logger, 'oauth2TokenRefresh', 'inc', { status: 'success', provider: 'outlook', statusCode: '200' });
|
|
2425
|
+
}
|
|
2426
|
+
} catch (E) {
|
|
2427
|
+
if (E.code === 'ETokenRefresh') {
|
|
2428
|
+
// Track failed token refresh
|
|
2429
|
+
const statusCode = String(E.statusCode || 0);
|
|
2430
|
+
metricsMeta({ account: this.account }, this.logger, 'oauth2TokenRefresh', 'inc', { status: 'failure', provider: 'outlook', statusCode });
|
|
2431
|
+
|
|
2432
|
+
// treat as authentication failure
|
|
2433
|
+
this.state = 'authenticationError';
|
|
2434
|
+
await this.setStateVal();
|
|
2435
|
+
|
|
2436
|
+
E.authenticationFailed = true;
|
|
2437
|
+
|
|
2438
|
+
if (!E.errorNotified) {
|
|
2439
|
+
E.errorNotified = true;
|
|
2440
|
+
await this.notify(false, AUTH_ERROR_NOTIFY, {
|
|
2441
|
+
response: E.oauthRequest?.response?.error?.message || E.response,
|
|
2442
|
+
serverResponseCode: 'TokenGenerationError'
|
|
2443
|
+
});
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
throw E;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
return tokenData.accessToken;
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
/**
|
|
2454
|
+
* Get or create OAuth2 client
|
|
2455
|
+
*/
|
|
2456
|
+
async getClient(force) {
|
|
2457
|
+
if (this.oAuth2Client && !force) {
|
|
2458
|
+
return this.oAuth2Client;
|
|
2459
|
+
}
|
|
2460
|
+
let accountData = await (this.delegatedAccountObject || this.accountObject).loadAccountData();
|
|
2461
|
+
this.oAuth2Client = await oauth2Apps.getClient(accountData.oauth2.provider, {
|
|
2462
|
+
logger: this.logger,
|
|
2463
|
+
logRaw: this.options.logRaw
|
|
2464
|
+
});
|
|
2465
|
+
return this.oAuth2Client;
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
/**
|
|
2469
|
+
* Ensure account and OAuth2 client are ready
|
|
2470
|
+
*/
|
|
2471
|
+
async prepare() {
|
|
2472
|
+
await this.getAccount();
|
|
2473
|
+
await this.prepareDelegatedAccount();
|
|
2474
|
+
await this.getClient();
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
/**
|
|
2478
|
+
* Set up timer to periodically renew webhook subscription
|
|
2479
|
+
*/
|
|
2480
|
+
setupRenewWatchTimer() {
|
|
2481
|
+
if (this.closed) {
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
clearTimeout(this.renewWatchTimer);
|
|
2485
|
+
this.renewWatchTimer = setTimeout(async () => {
|
|
2486
|
+
if (this.closed) {
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
try {
|
|
2491
|
+
// First try to renew existing subscription
|
|
2492
|
+
const renewalResult = await this.renewSubscription(false);
|
|
2493
|
+
|
|
2494
|
+
if (!renewalResult.success && (renewalResult.reason === 'expired' || renewalResult.reason === 'no_subscription')) {
|
|
2495
|
+
// If subscription expired or doesn't exist, ensure we have one
|
|
2496
|
+
await this.ensureSubscription();
|
|
2497
|
+
}
|
|
2498
|
+
} catch (err) {
|
|
2499
|
+
this.logger.error({ msg: 'Failed to renew MS Graph change subscription', account: this.account, err });
|
|
2500
|
+
} finally {
|
|
2501
|
+
// restart timer
|
|
2502
|
+
this.setupRenewWatchTimer();
|
|
2503
|
+
}
|
|
2504
|
+
}, RENEW_WATCH_TTL);
|
|
2505
|
+
this.renewWatchTimer.unref();
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
/**
|
|
2509
|
+
* Check if mailbox folder structure has changed using delta API
|
|
2510
|
+
* Returns true if changes detected, false otherwise
|
|
2511
|
+
*/
|
|
2512
|
+
async renewMailboxFolderCache() {
|
|
2513
|
+
// cache last known mailbox change
|
|
2514
|
+
let deltaReqUrl = (await this.redis.hget(this.getAccountKey(), 'outlookMailFoldersDeltaUrl')) || `/${this.oauth2UserPath}/mailFolders/delta`;
|
|
2515
|
+
|
|
2516
|
+
let hasChanges = false;
|
|
2517
|
+
|
|
2518
|
+
let deltaCheckDone = false;
|
|
2519
|
+
while (!deltaCheckDone) {
|
|
2520
|
+
let deltaRes;
|
|
2521
|
+
|
|
2522
|
+
try {
|
|
2523
|
+
deltaRes = await this.request(deltaReqUrl);
|
|
2524
|
+
} catch (err) {
|
|
2525
|
+
this.logger.error({ msg: 'Failed to check mailbox folder delta', err });
|
|
2526
|
+
// might be faulty entry, so clear it
|
|
2527
|
+
await this.redis.hdel(this.getAccountKey(), 'outlookMailFoldersDeltaUrl');
|
|
2528
|
+
|
|
2529
|
+
return true;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
if (deltaRes.value?.length) {
|
|
2533
|
+
hasChanges = true;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
if (deltaRes['@odata.nextLink']) {
|
|
2537
|
+
deltaReqUrl = deltaRes['@odata.nextLink'];
|
|
2538
|
+
await this.redis.hSetExists(this.getAccountKey(), 'outlookMailFoldersDeltaUrl', deltaReqUrl);
|
|
2539
|
+
} else {
|
|
2540
|
+
deltaCheckDone = true;
|
|
2541
|
+
if (deltaRes['@odata.deltaLink']) {
|
|
2542
|
+
deltaReqUrl = deltaRes['@odata.deltaLink'];
|
|
2543
|
+
await this.redis.hSetExists(this.getAccountKey(), 'outlookMailFoldersDeltaUrl', deltaReqUrl);
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
return hasChanges;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
/**
|
|
2552
|
+
* Get cached mailbox listing from Redis
|
|
2553
|
+
*/
|
|
2554
|
+
async getCachedMailboxListing() {
|
|
2555
|
+
let cachedListing;
|
|
2556
|
+
let cachedListingValue = await this.redis.hget(this.getAccountCacheKey(), 'outlookMailboxListing');
|
|
2557
|
+
if (cachedListingValue) {
|
|
2558
|
+
try {
|
|
2559
|
+
cachedListing = JSON.parse(cachedListingValue);
|
|
2560
|
+
} catch (err) {
|
|
2561
|
+
this.logger.error({ msg: 'Failed to parse cached mailbox listing', err });
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
return cachedListing;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
/**
|
|
2568
|
+
* Get complete mailbox folder listing from Graph API
|
|
2569
|
+
* Handles special use folders and nested folder structures
|
|
2570
|
+
*/
|
|
2571
|
+
async getMailboxListing() {
|
|
2572
|
+
// Map of special folder types
|
|
2573
|
+
let specialTags = new Map([
|
|
2574
|
+
['deleteditems', '\\Trash'],
|
|
2575
|
+
['drafts', '\\Drafts'],
|
|
2576
|
+
['inbox', '\\Inbox'],
|
|
2577
|
+
['junkemail', '\\Junk'],
|
|
2578
|
+
['sentitems', '\\Sent']
|
|
2579
|
+
]);
|
|
2580
|
+
|
|
2581
|
+
let specialUseKeys = Array.from(specialTags.keys());
|
|
2582
|
+
let specialUseTagIds = new Map();
|
|
2583
|
+
|
|
2584
|
+
// Resolve special use folders and cache their IDs
|
|
2585
|
+
for (let specialUseKey of specialUseKeys) {
|
|
2586
|
+
// Use caching. Assuming that the ID of the special-use folder does not change, even if the display name does
|
|
2587
|
+
let cachedValue;
|
|
2588
|
+
let cachedValueStr = await this.redis.hget(this.getAccountCacheKey(), `outlookMailbox:${specialUseKey}`);
|
|
2589
|
+
if (cachedValueStr) {
|
|
2590
|
+
try {
|
|
2591
|
+
cachedValue = JSON.parse(cachedValueStr);
|
|
2592
|
+
} catch (err) {
|
|
2593
|
+
await this.redis.hdel(this.getAccountCacheKey(), `outlookMailbox:${specialUseKey}`);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
if (cachedValue) {
|
|
2598
|
+
specialUseTagIds.set(cachedValue.id, specialTags.get(specialUseKey));
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
let reqUrl = `/${this.oauth2UserPath}/mailFolders/${specialUseKey}`;
|
|
2603
|
+
try {
|
|
2604
|
+
let mailbox = await this.request(reqUrl);
|
|
2605
|
+
if (mailbox) {
|
|
2606
|
+
await this.redis.hset(this.getAccountCacheKey(), `outlookMailbox:${specialUseKey}`, JSON.stringify(mailbox));
|
|
2607
|
+
specialUseTagIds.set(mailbox.id, specialTags.get(specialUseKey));
|
|
2608
|
+
}
|
|
2609
|
+
} catch (err) {
|
|
2610
|
+
this.logger.error({ msg: 'Failed to resolve mailbox for special use key', specialUseKey, err });
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
let mailboxListing = [];
|
|
2615
|
+
|
|
2616
|
+
/**
|
|
2617
|
+
* Recursively traverse folder hierarchy
|
|
2618
|
+
*/
|
|
2619
|
+
let traverse = async (pathNamePrefix, folderId) => {
|
|
2620
|
+
let list = [];
|
|
2621
|
+
let done = false;
|
|
2622
|
+
let reqUrl = `/${this.oauth2UserPath}/mailFolders${folderId ? `/${folderId}/childFolders` : ''}`;
|
|
2623
|
+
while (!done) {
|
|
2624
|
+
let mailboxRes = await this.request(reqUrl);
|
|
2625
|
+
if (mailboxRes.value) {
|
|
2626
|
+
list.push(
|
|
2627
|
+
...mailboxRes.value.map(entry => {
|
|
2628
|
+
if (!folderId) {
|
|
2629
|
+
entry.rootFolder = true;
|
|
2630
|
+
}
|
|
2631
|
+
let specialUse = specialUseTagIds.get(entry.id);
|
|
2632
|
+
if (specialUse) {
|
|
2633
|
+
entry.specialUse = specialUse;
|
|
2634
|
+
}
|
|
2635
|
+
if (pathNamePrefix) {
|
|
2636
|
+
entry.parentPath = pathNamePrefix;
|
|
2637
|
+
}
|
|
2638
|
+
entry.pathName = `${pathNamePrefix ? `${pathNamePrefix}/` : ''}${entry.displayName}`;
|
|
2639
|
+
return entry;
|
|
2640
|
+
})
|
|
2641
|
+
);
|
|
2642
|
+
}
|
|
2643
|
+
// Handle pagination
|
|
2644
|
+
if (!mailboxRes['@odata.nextLink']) {
|
|
2645
|
+
done = true;
|
|
2646
|
+
} else {
|
|
2647
|
+
reqUrl = mailboxRes['@odata.nextLink'];
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
mailboxListing.push(...list);
|
|
2652
|
+
|
|
2653
|
+
// Traverse child folders
|
|
2654
|
+
for (let entry of list) {
|
|
2655
|
+
// do not traverse subfolders for folders with a slash in the name (would create ambiguous paths)
|
|
2656
|
+
if (entry.childFolderCount && entry.displayName.indexOf('/') < 0) {
|
|
2657
|
+
await traverse(entry.pathName, entry.id);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
};
|
|
2661
|
+
|
|
2662
|
+
await traverse();
|
|
2663
|
+
|
|
2664
|
+
// keep only real folders and folders that do not contain slash in the name
|
|
2665
|
+
mailboxListing = mailboxListing.filter(
|
|
2666
|
+
entry => (!entry['@odata.type'] || /^#?microsoft\.graph\.mailFolder$/.test(entry['@odata.type'])) && entry.displayName.indexOf('/') < 0
|
|
2667
|
+
);
|
|
2668
|
+
|
|
2669
|
+
return mailboxListing;
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
/**
|
|
2673
|
+
* Resolve folder path to folder object
|
|
2674
|
+
* Supports special use tags, folder IDs, and path names
|
|
2675
|
+
*/
|
|
2676
|
+
async resolveFolder(path, options) {
|
|
2677
|
+
options = options || {};
|
|
2678
|
+
|
|
2679
|
+
path = [].concat(path || []).join('/');
|
|
2680
|
+
|
|
2681
|
+
let cachedListing = await this.getCachedMailboxListing();
|
|
2682
|
+
let mailboxListing = cachedListing || (await this.getMailboxListing());
|
|
2683
|
+
|
|
2684
|
+
if (options.byId) {
|
|
2685
|
+
return mailboxListing.find(entry => entry.id === path);
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
// Map IMAP special use tags to Outlook folder types
|
|
2689
|
+
let specialUseTags = new Map([
|
|
2690
|
+
['\\Trash', 'deleteditems'],
|
|
2691
|
+
['\\Drafts', 'drafts'],
|
|
2692
|
+
['\\Inbox', 'inbox'],
|
|
2693
|
+
['\\Junk', 'junkemail'],
|
|
2694
|
+
['\\Sent', 'sentitems']
|
|
2695
|
+
]);
|
|
2696
|
+
|
|
2697
|
+
// Handle case-insensitive INBOX
|
|
2698
|
+
if (/^inbox$/i.test(path)) {
|
|
2699
|
+
path = '\\Inbox';
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
// Resolve special use tag
|
|
2703
|
+
if (specialUseTags.has(path)) {
|
|
2704
|
+
// resolve special use tag folder
|
|
2705
|
+
let folderEntry = mailboxListing.find(entry => entry.specialUse === path);
|
|
2706
|
+
if (folderEntry) {
|
|
2707
|
+
return folderEntry;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
// Handle INBOX prefix in paths
|
|
2712
|
+
let pathParts = path.split('/');
|
|
2713
|
+
if (/^inbox$/i.test(pathParts[0])) {
|
|
2714
|
+
let inboxFolder = mailboxListing.find(entry => entry.specialUse === '\\Inbox');
|
|
2715
|
+
if (inboxFolder) {
|
|
2716
|
+
pathParts[0] = inboxFolder.pathName;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
path = pathParts.join('/');
|
|
2720
|
+
|
|
2721
|
+
if (options.pathNameOnly) {
|
|
2722
|
+
return { pathName: path };
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
let folderEntry = mailboxListing.find(entry => entry.pathName === path);
|
|
2726
|
+
|
|
2727
|
+
return folderEntry;
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
/**
|
|
2731
|
+
* Unified method to renew MS Graph webhook subscription with proper locking
|
|
2732
|
+
* Can be called from timer or lifecycle notification handler
|
|
2733
|
+
* @param {boolean} force - Force renewal even if not expired soon
|
|
2734
|
+
* @returns {Object} Result with success status and details
|
|
2735
|
+
*/
|
|
2736
|
+
async renewSubscription(force = false) {
|
|
2737
|
+
const lockKey = `${this.getAccountKey()}:subscription-renew-lock`;
|
|
2738
|
+
const lockTTL = 30; // 30 seconds TTL for the lock to prevent blocking on crash
|
|
2739
|
+
|
|
2740
|
+
// Try to acquire lock using Redis SET NX with expiry
|
|
2741
|
+
const lockAcquired = await this.redis.set(lockKey, Date.now(), 'EX', lockTTL, 'NX');
|
|
2742
|
+
|
|
2743
|
+
if (!lockAcquired) {
|
|
2744
|
+
this.logger.info({
|
|
2745
|
+
msg: 'Subscription renewal skipped',
|
|
2746
|
+
reason: 'lock_exists',
|
|
2747
|
+
account: this.account
|
|
2748
|
+
});
|
|
2749
|
+
return { success: false, reason: 'lock_exists' };
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
try {
|
|
2753
|
+
// Get current subscription
|
|
2754
|
+
let outlookSubscription = await this.redis.hget(this.getAccountKey(), 'outlookSubscription');
|
|
2755
|
+
if (outlookSubscription) {
|
|
2756
|
+
try {
|
|
2757
|
+
outlookSubscription = JSON.parse(outlookSubscription);
|
|
2758
|
+
} catch (err) {
|
|
2759
|
+
outlookSubscription = {};
|
|
2760
|
+
}
|
|
2761
|
+
} else {
|
|
2762
|
+
outlookSubscription = {};
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
// Check if subscription exists and is valid
|
|
2766
|
+
if (!outlookSubscription.id) {
|
|
2767
|
+
this.subscriptionState = 'unset';
|
|
2768
|
+
return { success: false, reason: 'no_subscription' };
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
let existingExpirationDateTime = new Date(outlookSubscription.expirationDateTime);
|
|
2772
|
+
if (existingExpirationDateTime.toString() === 'Invalid Date') {
|
|
2773
|
+
existingExpirationDateTime = null;
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
const now = Date.now();
|
|
2777
|
+
|
|
2778
|
+
// Check if already expired
|
|
2779
|
+
if (existingExpirationDateTime && existingExpirationDateTime.getTime() < now) {
|
|
2780
|
+
this.logger.warn({
|
|
2781
|
+
msg: 'Subscription already expired',
|
|
2782
|
+
subscriptionId: outlookSubscription.id,
|
|
2783
|
+
expirationDateTime: outlookSubscription.expirationDateTime,
|
|
2784
|
+
account: this.account
|
|
2785
|
+
});
|
|
2786
|
+
// Clear the expired subscription
|
|
2787
|
+
await this.redis.hdel(this.getAccountKey(), 'outlookSubscription');
|
|
2788
|
+
this.subscriptionState = 'expired';
|
|
2789
|
+
return { success: false, reason: 'expired' };
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
// Check if renewal is needed
|
|
2793
|
+
const shouldRenew = force || (existingExpirationDateTime && existingExpirationDateTime.getTime() < now + OUTLOOK_EXPIRATION_RENEW_TIME);
|
|
2794
|
+
|
|
2795
|
+
if (!shouldRenew) {
|
|
2796
|
+
this.logger.debug({
|
|
2797
|
+
msg: 'Subscription renewal not needed',
|
|
2798
|
+
subscriptionId: outlookSubscription.id,
|
|
2799
|
+
expirationDateTime: outlookSubscription.expirationDateTime,
|
|
2800
|
+
account: this.account
|
|
2801
|
+
});
|
|
2802
|
+
this.subscriptionState = 'valid';
|
|
2803
|
+
return { success: true, reason: 'not_needed' };
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
// Perform renewal
|
|
2807
|
+
const expirationDateTime = new Date(now + OUTLOOK_EXPIRATION_TIME);
|
|
2808
|
+
const subscriptionPayload = {
|
|
2809
|
+
expirationDateTime: expirationDateTime.toISOString()
|
|
2810
|
+
};
|
|
2811
|
+
|
|
2812
|
+
// Update state to renewing
|
|
2813
|
+
outlookSubscription.state = {
|
|
2814
|
+
state: 'renewing',
|
|
2815
|
+
time: Date.now()
|
|
2816
|
+
};
|
|
2817
|
+
await this.redis.hSetExists(this.getAccountKey(), 'outlookSubscription', JSON.stringify(outlookSubscription));
|
|
2818
|
+
this.subscriptionState = 'pending';
|
|
2819
|
+
|
|
2820
|
+
try {
|
|
2821
|
+
const subscriptionRes = await this.request(`/subscriptions/${outlookSubscription.id}`, 'PATCH', subscriptionPayload);
|
|
2822
|
+
|
|
2823
|
+
if (subscriptionRes?.expirationDateTime) {
|
|
2824
|
+
// Check if this was a retry before clearing state
|
|
2825
|
+
const wasRetry = outlookSubscription.state?.retryCount > 0;
|
|
2826
|
+
const previousRetryCount = outlookSubscription.state?.retryCount || 0;
|
|
2827
|
+
|
|
2828
|
+
outlookSubscription.expirationDateTime = subscriptionRes.expirationDateTime;
|
|
2829
|
+
outlookSubscription.state = {
|
|
2830
|
+
state: 'created',
|
|
2831
|
+
time: Date.now(),
|
|
2832
|
+
// Clear any previous error state and retry count
|
|
2833
|
+
retryCount: 0,
|
|
2834
|
+
error: null
|
|
2835
|
+
};
|
|
2836
|
+
|
|
2837
|
+
await this.redis.hSetExists(this.getAccountKey(), 'outlookSubscription', JSON.stringify(outlookSubscription));
|
|
2838
|
+
this.subscriptionState = 'valid';
|
|
2839
|
+
|
|
2840
|
+
// Log if this was a successful retry after errors
|
|
2841
|
+
this.logger.info({
|
|
2842
|
+
msg: wasRetry ? 'Subscription renewed successfully after retry' : 'Subscription renewed successfully',
|
|
2843
|
+
subscriptionId: outlookSubscription.id,
|
|
2844
|
+
newExpirationDateTime: subscriptionRes.expirationDateTime,
|
|
2845
|
+
account: this.account,
|
|
2846
|
+
wasRetry,
|
|
2847
|
+
previousRetryCount
|
|
2848
|
+
});
|
|
2849
|
+
|
|
2850
|
+
return { success: true, expirationDateTime: subscriptionRes.expirationDateTime };
|
|
2851
|
+
} else {
|
|
2852
|
+
throw new Error('Empty server response');
|
|
2853
|
+
}
|
|
2854
|
+
} catch (err) {
|
|
2855
|
+
this.logger.error({
|
|
2856
|
+
msg: 'Subscription renewal failed',
|
|
2857
|
+
subscriptionId: outlookSubscription.id,
|
|
2858
|
+
account: this.account,
|
|
2859
|
+
err
|
|
2860
|
+
});
|
|
2861
|
+
|
|
2862
|
+
outlookSubscription.state = {
|
|
2863
|
+
state: 'error',
|
|
2864
|
+
error: `Subscription renewal failed: ${err.oauthRequest?.response?.error?.message || err.message}`,
|
|
2865
|
+
time: Date.now(),
|
|
2866
|
+
retryCount: (outlookSubscription.state?.retryCount || 0) + 1
|
|
2867
|
+
};
|
|
2868
|
+
await this.redis.hSetExists(this.getAccountKey(), 'outlookSubscription', JSON.stringify(outlookSubscription));
|
|
2869
|
+
this.subscriptionState = 'failed';
|
|
2870
|
+
|
|
2871
|
+
// Schedule retry with exponential backoff (max 3 retries)
|
|
2872
|
+
const retryCount = outlookSubscription.state.retryCount;
|
|
2873
|
+
if (retryCount <= 3) {
|
|
2874
|
+
// Exponential backoff: 30s, 60s, 120s
|
|
2875
|
+
const retryDelay = Math.min(30 * Math.pow(2, retryCount - 1), 120) * 1000;
|
|
2876
|
+
|
|
2877
|
+
setTimeout(() => {
|
|
2878
|
+
// Retry will also acquire lock
|
|
2879
|
+
this.renewSubscription(force).catch(err => {
|
|
2880
|
+
this.logger.error({
|
|
2881
|
+
msg: 'Subscription renewal retry failed',
|
|
2882
|
+
account: this.account,
|
|
2883
|
+
retryAttempt: retryCount,
|
|
2884
|
+
err
|
|
2885
|
+
});
|
|
2886
|
+
});
|
|
2887
|
+
}, retryDelay);
|
|
2888
|
+
|
|
2889
|
+
this.logger.info({
|
|
2890
|
+
msg: 'Scheduling subscription renewal retry',
|
|
2891
|
+
account: this.account,
|
|
2892
|
+
retryAttempt: retryCount,
|
|
2893
|
+
retryDelayMs: retryDelay
|
|
2894
|
+
});
|
|
2895
|
+
} else {
|
|
2896
|
+
this.logger.error({
|
|
2897
|
+
msg: 'Max subscription renewal retries exceeded',
|
|
2898
|
+
account: this.account,
|
|
2899
|
+
retryCount
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
return { success: false, reason: 'renewal_failed', error: err.message };
|
|
2904
|
+
}
|
|
2905
|
+
} finally {
|
|
2906
|
+
// Always release the lock
|
|
2907
|
+
await this.redis.del(lockKey);
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
/**
|
|
2912
|
+
* Ensure webhook subscription exists and is valid
|
|
2913
|
+
* Creates new subscription or renews existing one
|
|
2914
|
+
*/
|
|
2915
|
+
async ensureSubscription() {
|
|
2916
|
+
let serviceUrl = (await settings.get('serviceUrl')) || null;
|
|
2917
|
+
let notificationBaseUrl = (await settings.get('notificationBaseUrl')) || serviceUrl || '';
|
|
2918
|
+
|
|
2919
|
+
if (!serviceUrl) {
|
|
2920
|
+
this.logger.fatal({ msg: 'Service URL not set' });
|
|
2921
|
+
return false;
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
let outlookSubscription = await this.redis.hget(this.getAccountKey(), 'outlookSubscription');
|
|
2925
|
+
if (outlookSubscription) {
|
|
2926
|
+
try {
|
|
2927
|
+
outlookSubscription = JSON.parse(outlookSubscription);
|
|
2928
|
+
} catch (err) {
|
|
2929
|
+
// ignore, I guess?
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
if (!outlookSubscription) {
|
|
2934
|
+
outlookSubscription = {};
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
// Check if subscription is being created/renewed
|
|
2938
|
+
if (['creating', 'renewing'].includes(outlookSubscription.state?.state) && outlookSubscription.state.time > Date.now() - 30 * 60 * 1000) {
|
|
2939
|
+
// allow previous operation to finish (30 minute timeout)
|
|
2940
|
+
this.logger.info({
|
|
2941
|
+
msg: 'Subscription renewal skipped',
|
|
2942
|
+
reason: 'pending',
|
|
2943
|
+
subscriptionId: outlookSubscription?.id,
|
|
2944
|
+
state: outlookSubscription.state?.state,
|
|
2945
|
+
created: outlookSubscription.state?.time,
|
|
2946
|
+
expected: Date.now() - 30 * 60 * 1000
|
|
2947
|
+
});
|
|
2948
|
+
this.subscriptionState = 'pending';
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
let now = Date.now();
|
|
2953
|
+
let expirationDateTime = new Date(now + OUTLOOK_EXPIRATION_TIME);
|
|
2954
|
+
|
|
2955
|
+
// Renew existing subscription if needed
|
|
2956
|
+
if (outlookSubscription.id) {
|
|
2957
|
+
let existingExpirationDateTime = new Date(outlookSubscription.expirationDateTime);
|
|
2958
|
+
if (existingExpirationDateTime.toString() === 'Invalid Date') {
|
|
2959
|
+
existingExpirationDateTime = null;
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
if (existingExpirationDateTime && existingExpirationDateTime.getTime() < now) {
|
|
2963
|
+
// already expired
|
|
2964
|
+
outlookSubscription = {};
|
|
2965
|
+
} else if (existingExpirationDateTime && existingExpirationDateTime.getTime() < now + OUTLOOK_EXPIRATION_RENEW_TIME) {
|
|
2966
|
+
// Use the unified renewal method
|
|
2967
|
+
const renewalResult = await this.renewSubscription(false);
|
|
2968
|
+
if (!renewalResult.success && renewalResult.reason === 'expired') {
|
|
2969
|
+
// Subscription expired, clear it to create a new one
|
|
2970
|
+
outlookSubscription = {};
|
|
2971
|
+
} else {
|
|
2972
|
+
return renewalResult.success;
|
|
2973
|
+
}
|
|
2974
|
+
} else {
|
|
2975
|
+
// Subscription is valid, do nothing
|
|
2976
|
+
this.logger.info({
|
|
2977
|
+
msg: 'Subscription renewal skipped',
|
|
2978
|
+
reason: 'valid',
|
|
2979
|
+
subscriptionId: outlookSubscription?.id,
|
|
2980
|
+
state: outlookSubscription.state?.state,
|
|
2981
|
+
created: outlookSubscription.state?.time,
|
|
2982
|
+
expirationDateTime: outlookSubscription.expirationDateTime
|
|
2983
|
+
});
|
|
2984
|
+
this.subscriptionState = 'valid';
|
|
2985
|
+
return;
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
// Create new subscription if needed
|
|
2990
|
+
if (!outlookSubscription.id) {
|
|
2991
|
+
const notificationUrl = prepareUrl('/oauth/msg/notification', notificationBaseUrl, {
|
|
2992
|
+
account: this.account
|
|
2993
|
+
});
|
|
2994
|
+
|
|
2995
|
+
const lifecycleNotificationUrl = prepareUrl('/oauth/msg/lifecycle', notificationBaseUrl, {
|
|
2996
|
+
account: this.account
|
|
2997
|
+
});
|
|
2998
|
+
|
|
2999
|
+
const subscriptionPayload = {
|
|
3000
|
+
changeType: 'created,updated,deleted',
|
|
3001
|
+
notificationUrl,
|
|
3002
|
+
lifecycleNotificationUrl,
|
|
3003
|
+
resource: `/${this.oauth2UserPath}/messages`,
|
|
3004
|
+
expirationDateTime: expirationDateTime.toISOString(),
|
|
3005
|
+
clientState: crypto.randomUUID()
|
|
3006
|
+
};
|
|
3007
|
+
|
|
3008
|
+
outlookSubscription.state = {
|
|
3009
|
+
state: 'creating',
|
|
3010
|
+
time: Date.now()
|
|
3011
|
+
};
|
|
3012
|
+
await this.redis.hSetExists(this.getAccountKey(), 'outlookSubscription', JSON.stringify(outlookSubscription));
|
|
3013
|
+
this.subscriptionState = 'pending';
|
|
3014
|
+
|
|
3015
|
+
let subscriptionRes;
|
|
3016
|
+
try {
|
|
3017
|
+
subscriptionRes = await this.request(`/subscriptions`, 'post', subscriptionPayload);
|
|
3018
|
+
if (subscriptionRes?.expirationDateTime) {
|
|
3019
|
+
outlookSubscription = {
|
|
3020
|
+
id: subscriptionRes.id,
|
|
3021
|
+
expirationDateTime: subscriptionRes.expirationDateTime,
|
|
3022
|
+
clientState: subscriptionRes.clientState,
|
|
3023
|
+
state: {
|
|
3024
|
+
state: 'created',
|
|
3025
|
+
time: Date.now()
|
|
3026
|
+
}
|
|
3027
|
+
};
|
|
3028
|
+
this.subscriptionState = 'valid';
|
|
3029
|
+
} else {
|
|
3030
|
+
throw new Error('Empty server response');
|
|
3031
|
+
}
|
|
3032
|
+
} catch (err) {
|
|
3033
|
+
outlookSubscription.state = {
|
|
3034
|
+
state: 'error',
|
|
3035
|
+
error: `Subscription failed: ${err.oauthRequest?.response?.error?.message || err.message}`,
|
|
3036
|
+
time: Date.now()
|
|
3037
|
+
};
|
|
3038
|
+
this.subscriptionState = 'failed';
|
|
3039
|
+
} finally {
|
|
3040
|
+
await this.redis.hSetExists(this.getAccountKey(), 'outlookSubscription', JSON.stringify(outlookSubscription));
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
/**
|
|
3046
|
+
* Format Graph API message data to EmailEngine format
|
|
3047
|
+
* Handles all message properties and attachments
|
|
3048
|
+
*/
|
|
3049
|
+
formatMessage(messageData, options) {
|
|
3050
|
+
let { extended, path, textType, showPath } = options || {};
|
|
3051
|
+
|
|
3052
|
+
let date = messageData.receivedDateTime ? new Date(messageData.receivedDateTime) : undefined;
|
|
3053
|
+
if (date?.toString() === 'Invalid Date') {
|
|
3054
|
+
date = undefined;
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
// Map Graph API flags to IMAP flags
|
|
3058
|
+
const flags = [];
|
|
3059
|
+
if (messageData.isRead) {
|
|
3060
|
+
flags.push('\\Seen');
|
|
3061
|
+
}
|
|
3062
|
+
if (messageData.isDraft) {
|
|
3063
|
+
flags.push('\\Draft');
|
|
3064
|
+
}
|
|
3065
|
+
if (messageData.flag?.flagStatus === 'flagged') {
|
|
3066
|
+
flags.push('\\Flagged');
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
let encodedTextSize = {};
|
|
3070
|
+
let textContents = {};
|
|
3071
|
+
|
|
3072
|
+
// set defaults for requested text type
|
|
3073
|
+
if (textType && textType !== '*') {
|
|
3074
|
+
textContents[options.textType] = '';
|
|
3075
|
+
encodedTextSize[options.textType] = 0;
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
// Process message body content
|
|
3079
|
+
if (messageData?.body?.contentType) {
|
|
3080
|
+
let textContent = messageData.body.content || '';
|
|
3081
|
+
let textContentType;
|
|
3082
|
+
switch (messageData.body.contentType) {
|
|
3083
|
+
case 'text':
|
|
3084
|
+
textContentType = 'plain';
|
|
3085
|
+
break;
|
|
3086
|
+
|
|
3087
|
+
case 'html':
|
|
3088
|
+
default:
|
|
3089
|
+
textContentType = messageData.body.contentType;
|
|
3090
|
+
break;
|
|
3091
|
+
}
|
|
3092
|
+
encodedTextSize[textContentType] = Buffer.byteLength(textContent);
|
|
3093
|
+
if ([textContentType, '*'].includes(textType)) {
|
|
3094
|
+
textContents[textContentType] = textContent;
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
let message = {
|
|
3099
|
+
id: messageData.id,
|
|
3100
|
+
|
|
3101
|
+
path: ((extended || showPath) && path) || undefined,
|
|
3102
|
+
|
|
3103
|
+
emailId: messageData.id,
|
|
3104
|
+
threadId: messageData.conversationId || undefined,
|
|
3105
|
+
|
|
3106
|
+
date: date ? date.toISOString() : undefined,
|
|
3107
|
+
|
|
3108
|
+
flags,
|
|
3109
|
+
labels: messageData.categories?.length ? messageData.categories : undefined,
|
|
3110
|
+
|
|
3111
|
+
// Convenience boolean flags
|
|
3112
|
+
unseen: !flags.includes('\\Seen') ? true : undefined,
|
|
3113
|
+
flagged: flags.includes('\\Flagged') ? true : undefined,
|
|
3114
|
+
draft: flags.includes('\\Draft') ? true : undefined,
|
|
3115
|
+
|
|
3116
|
+
subject: messageData.subject || undefined,
|
|
3117
|
+
from: messageData.from?.emailAddress || undefined,
|
|
3118
|
+
|
|
3119
|
+
replyTo: messageData.replyTo?.length ? messageData.replyTo.map(entry => entry.emailAddress).filter(entry => entry) : undefined,
|
|
3120
|
+
sender: (extended && messageData.sender?.emailAddress) || undefined,
|
|
3121
|
+
|
|
3122
|
+
to: messageData.toRecipients?.length ? messageData.toRecipients.map(entry => entry.emailAddress).filter(entry => entry) : undefined,
|
|
3123
|
+
cc: messageData.ccRecipients?.length ? messageData.ccRecipients.map(entry => entry.emailAddress).filter(entry => entry) : undefined,
|
|
3124
|
+
bcc: extended && messageData.bccRecipients?.length ? messageData.bccRecipients.map(entry => entry.emailAddress).filter(entry => entry) : undefined,
|
|
3125
|
+
|
|
3126
|
+
messageId: messageData.internetMessageId,
|
|
3127
|
+
|
|
3128
|
+
headers: (extended && messageData.headers) || undefined,
|
|
3129
|
+
|
|
3130
|
+
text: {
|
|
3131
|
+
id: messageData.id,
|
|
3132
|
+
encodedSize: encodedTextSize,
|
|
3133
|
+
plain: textContents?.plain?.toString(),
|
|
3134
|
+
html: textContents?.html?.toString(),
|
|
3135
|
+
hasMore: textContents?.plain || textContents?.html ? false : undefined
|
|
3136
|
+
},
|
|
3137
|
+
|
|
3138
|
+
preview: messageData.bodyPreview
|
|
3139
|
+
};
|
|
3140
|
+
|
|
3141
|
+
// Check for auto-reply based on headers
|
|
3142
|
+
if (message.headers && this.isAutoreply(message)) {
|
|
3143
|
+
message.isAutoReply = true;
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
// Process attachments
|
|
3147
|
+
if (messageData.attachments?.length) {
|
|
3148
|
+
message.attachments = messageData.attachments.map(entry => {
|
|
3149
|
+
const attachment = {
|
|
3150
|
+
// Encode both message ID and attachment ID for later retrieval
|
|
3151
|
+
id: msgpack.encode([messageData.id, entry.id]).toString('base64url'),
|
|
3152
|
+
contentType: entry.contentType,
|
|
3153
|
+
encodedSize: entry.size,
|
|
3154
|
+
inline: !!entry.isInline
|
|
3155
|
+
};
|
|
3156
|
+
|
|
3157
|
+
if (entry.name) {
|
|
3158
|
+
attachment.filename = entry.name;
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
if (entry.contentId) {
|
|
3162
|
+
// Normalize content ID format
|
|
3163
|
+
attachment.contentId = entry.contentId.replace(/^<*/, '<').replace(/>*$/, '>');
|
|
3164
|
+
// Check if attachment is embedded in HTML
|
|
3165
|
+
if (textContents?.html?.indexOf(`cid:${attachment.contentId.replace(/^[\s<]*|[\s>]*$/g, '')}`) >= 0) {
|
|
3166
|
+
attachment.embedded = true;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
return attachment;
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
return message;
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
/**
|
|
3178
|
+
* Get attachment content by attachment ID
|
|
3179
|
+
* Handles both regular attachments and message/rfc822 attachments
|
|
3180
|
+
*/
|
|
3181
|
+
async getAttachmentContent(attachmentId, options) {
|
|
3182
|
+
options = options || {};
|
|
3183
|
+
|
|
3184
|
+
// Decode compound attachment ID
|
|
3185
|
+
const [emailId, id] = msgpack.decode(Buffer.from(attachmentId, 'base64url'));
|
|
3186
|
+
|
|
3187
|
+
await this.prepare();
|
|
3188
|
+
|
|
3189
|
+
const attachmentErrorHandler = err => {
|
|
3190
|
+
switch (err.oauthRequest?.status) {
|
|
3191
|
+
case 404: {
|
|
3192
|
+
let error = new Error('Unknown attachment');
|
|
3193
|
+
error.info = {
|
|
3194
|
+
response: `Attachment does not exist`
|
|
3195
|
+
};
|
|
3196
|
+
error.code = 'NotFound';
|
|
3197
|
+
error.statusCode = 404;
|
|
3198
|
+
return error;
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
case 400: {
|
|
3202
|
+
let error = new Error('Invalid request');
|
|
3203
|
+
error.info = {
|
|
3204
|
+
response: err.oauthRequest?.response?.error?.message || `Invalid request`
|
|
3205
|
+
};
|
|
3206
|
+
error.code = err.oauthRequest?.response?.error?.code || 'InvalidRequest';
|
|
3207
|
+
error.statusCode = 400;
|
|
3208
|
+
return error;
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
default:
|
|
3212
|
+
this.logger.error({
|
|
3213
|
+
msg: 'Failed to fetch attachment',
|
|
3214
|
+
emailId,
|
|
3215
|
+
err
|
|
3216
|
+
});
|
|
3217
|
+
return err;
|
|
3218
|
+
}
|
|
3219
|
+
};
|
|
3220
|
+
|
|
3221
|
+
let attachmentResponse;
|
|
3222
|
+
|
|
3223
|
+
try {
|
|
3224
|
+
attachmentResponse = await this.request(`/${this.oauth2UserPath}/messages/${emailId}/attachments/${id}`);
|
|
3225
|
+
} catch (err) {
|
|
3226
|
+
throw attachmentErrorHandler(err);
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
let content = attachmentResponse?.contentBytes
|
|
3230
|
+
? options.returnBase64
|
|
3231
|
+
? attachmentResponse.contentBytes
|
|
3232
|
+
: Buffer.from(attachmentResponse.contentBytes, 'base64')
|
|
3233
|
+
: null;
|
|
3234
|
+
|
|
3235
|
+
// Some attachments (like message/rfc822) need special handling
|
|
3236
|
+
if (attachmentResponse?.contentType && !attachmentResponse?.contentBytes) {
|
|
3237
|
+
// fetch raw value instead, happens with message/rfc822 attachments
|
|
3238
|
+
try {
|
|
3239
|
+
let rawAttachmentContentStr = await this.request(
|
|
3240
|
+
`/${this.oauth2UserPath}/messages/${emailId}/attachments/${id}/$value`,
|
|
3241
|
+
'get',
|
|
3242
|
+
Buffer.alloc(0),
|
|
3243
|
+
{
|
|
3244
|
+
returnText: true
|
|
3245
|
+
}
|
|
3246
|
+
);
|
|
3247
|
+
if (rawAttachmentContentStr) {
|
|
3248
|
+
if (options.returnBase64) {
|
|
3249
|
+
content = Buffer.from(rawAttachmentContentStr).toString('base64');
|
|
3250
|
+
} else {
|
|
3251
|
+
content = Buffer.from(rawAttachmentContentStr);
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
} catch (err) {
|
|
3255
|
+
throw attachmentErrorHandler(err);
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
return options.contentOnly
|
|
3260
|
+
? content
|
|
3261
|
+
: {
|
|
3262
|
+
content,
|
|
3263
|
+
contentType: attachmentResponse?.contentType,
|
|
3264
|
+
filename: attachmentResponse?.name
|
|
3265
|
+
};
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
/**
|
|
3269
|
+
* Start processing pending webhook notifications
|
|
3270
|
+
* Prevents concurrent processing with a flag
|
|
3271
|
+
*/
|
|
3272
|
+
triggerSync() {
|
|
3273
|
+
if (this.processingHistory) {
|
|
3274
|
+
return;
|
|
3275
|
+
}
|
|
3276
|
+
this.processingHistory = true;
|
|
3277
|
+
this.processHistory()
|
|
3278
|
+
.catch(err => {
|
|
3279
|
+
this.logger.error({ msg: 'Failed to process account history', account: this.account, err });
|
|
3280
|
+
})
|
|
3281
|
+
.finally(() => {
|
|
3282
|
+
this.processingHistory = false;
|
|
3283
|
+
});
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
/**
|
|
3287
|
+
* Process queued webhook events
|
|
3288
|
+
* Handles created, updated, and deleted message events
|
|
3289
|
+
*/
|
|
3290
|
+
async processHistory() {
|
|
3291
|
+
let event;
|
|
3292
|
+
let newMessageOptions;
|
|
3293
|
+
|
|
3294
|
+
while ((event = await this.accountObject.pullQueueEvent()) !== null) {
|
|
3295
|
+
switch (event.type) {
|
|
3296
|
+
case 'updated':
|
|
3297
|
+
await this.processUpdatedMessage(event.message);
|
|
3298
|
+
break;
|
|
3299
|
+
|
|
3300
|
+
case 'deleted': {
|
|
3301
|
+
await this.processDeletedMessage(event.message);
|
|
3302
|
+
break;
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
case 'created': {
|
|
3306
|
+
if (!newMessageOptions) {
|
|
3307
|
+
// cache options for efficiency
|
|
3308
|
+
newMessageOptions = await this.getMessageFetchOptions();
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
newMessageOptions.showPath = true;
|
|
3312
|
+
|
|
3313
|
+
let messageData = await this.prepareNewMessage(event.message, newMessageOptions);
|
|
3314
|
+
if (messageData) {
|
|
3315
|
+
// When an email is sent, multiple "created" events are triggered: one for the draft and one for the sent mail folder.
|
|
3316
|
+
// However, since we process the event with a delay, we only observe the message stored in the Sent Mail folder.
|
|
3317
|
+
// This happens because the message was already moved there by the time we began processing the initial event from the drafts folder.
|
|
3318
|
+
|
|
3319
|
+
// Check rolling bucket lock to see if we recently processed the same new email event for the same folder
|
|
3320
|
+
let recentlySeen = await this.rollingBucketLock(`${messageData.id}:created`, messageData.path);
|
|
3321
|
+
if (recentlySeen) {
|
|
3322
|
+
this.logger.debug({ msg: 'Ignore recently seen new email event', type: 'history-event', emailId: event.message });
|
|
3323
|
+
break;
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
await this.processNew(messageData, newMessageOptions);
|
|
3327
|
+
}
|
|
3328
|
+
break;
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
default:
|
|
3332
|
+
this.logger.debug({ msg: 'Future feature', type: 'history-event', event });
|
|
3333
|
+
break;
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
/**
|
|
3339
|
+
* Process a deleted message event
|
|
3340
|
+
* Verifies if actually deleted or just moved
|
|
3341
|
+
*/
|
|
3342
|
+
async processDeletedMessage(emailId) {
|
|
3343
|
+
// Verify if the message was actually deleted.
|
|
3344
|
+
// If the email was moved, we receive both a 'deleted' and a 'created' event with the same ID.
|
|
3345
|
+
let messageData;
|
|
3346
|
+
try {
|
|
3347
|
+
messageData = await this.request(`/${this.oauth2UserPath}/messages/${emailId}`, 'get', {
|
|
3348
|
+
$select: ['id', 'parentFolderId'].join(',')
|
|
3349
|
+
});
|
|
3350
|
+
} catch (err) {
|
|
3351
|
+
this.logger.error({
|
|
3352
|
+
msg: 'Failed to fetch message data',
|
|
3353
|
+
emailId,
|
|
3354
|
+
err
|
|
3355
|
+
});
|
|
3356
|
+
}
|
|
3357
|
+
if (messageData) {
|
|
3358
|
+
// The message still exists, so there's no need to notify about the deletion.
|
|
3359
|
+
// The message was likely moved, but since we cannot determine the previous mailbox folder,
|
|
3360
|
+
// there is no point in notifying about it.
|
|
3361
|
+
this.logger.debug({ msg: 'Ignore deleted email event. Still exists.', type: 'history-event', emailId, messageData });
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
let messageUpdate = {
|
|
3366
|
+
id: emailId
|
|
3367
|
+
};
|
|
3368
|
+
|
|
3369
|
+
await this.notify(this, MESSAGE_DELETED_NOTIFY, messageUpdate);
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
/**
|
|
3373
|
+
* Process an updated message event
|
|
3374
|
+
* Tracks flag changes and deduplicates repeated updates
|
|
3375
|
+
*/
|
|
3376
|
+
async processUpdatedMessage(emailId) {
|
|
3377
|
+
let messageData;
|
|
3378
|
+
|
|
3379
|
+
try {
|
|
3380
|
+
messageData = await this.request(`/${this.oauth2UserPath}/messages/${emailId}`, 'get', {
|
|
3381
|
+
$select: ['id', 'isRead', 'isDraft', 'flag', 'conversationId', 'parentFolderId'].join(',')
|
|
3382
|
+
});
|
|
3383
|
+
} catch (err) {
|
|
3384
|
+
this.logger.error({
|
|
3385
|
+
msg: 'Failed to fetch message data',
|
|
3386
|
+
emailId,
|
|
3387
|
+
err
|
|
3388
|
+
});
|
|
3389
|
+
}
|
|
3390
|
+
if (!messageData) {
|
|
3391
|
+
// do nothing, message not found
|
|
3392
|
+
return;
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3395
|
+
let folder;
|
|
3396
|
+
try {
|
|
3397
|
+
folder = await this.resolveFolder(messageData.parentFolderId, { byId: true });
|
|
3398
|
+
} catch (err) {
|
|
3399
|
+
this.logger.error({
|
|
3400
|
+
msg: 'Failed to resolve folder for message',
|
|
3401
|
+
emailId,
|
|
3402
|
+
err
|
|
3403
|
+
});
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
let path = folder ? folder.pathName : this.path;
|
|
3407
|
+
let specialUse = folder ? folder.specialUse : this.listingEntry.specialUse;
|
|
3408
|
+
|
|
3409
|
+
// we do not know which flags were added or removed, so list the full value
|
|
3410
|
+
|
|
3411
|
+
const changes = { flags: { value: [] } };
|
|
3412
|
+
|
|
3413
|
+
if (messageData.isRead) {
|
|
3414
|
+
changes.flags.value.push('\\Seen');
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
if (messageData.isDraft) {
|
|
3418
|
+
changes.flags.value.push('\\Draft');
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3421
|
+
if (messageData.flag?.flagStatus === 'flagged') {
|
|
3422
|
+
changes.flags.value.push('\\Flagged');
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
let messageUpdate = {
|
|
3426
|
+
id: messageData.id,
|
|
3427
|
+
threadId: messageData.conversationId,
|
|
3428
|
+
changes
|
|
3429
|
+
};
|
|
3430
|
+
|
|
3431
|
+
// Check rolling bucket lock to see if we recently processed the update event for the same email
|
|
3432
|
+
let recentlySeen = await this.rollingBucketLock(`${messageData.id}:updated`, JSON.stringify(changes.flags.value));
|
|
3433
|
+
if (recentlySeen) {
|
|
3434
|
+
this.logger.debug({ msg: 'Ignore recently seen updated email event', type: 'history-event', emailId: messageData.id, flags: changes.flags.value });
|
|
3435
|
+
return;
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
await this.notify({ path, specialUse }, MESSAGE_UPDATED_NOTIFY, messageUpdate);
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
/**
|
|
3442
|
+
* Prepare a new message for processing
|
|
3443
|
+
* Fetches full message details and checks if it's actually new
|
|
3444
|
+
*/
|
|
3445
|
+
async prepareNewMessage(emailId, options) {
|
|
3446
|
+
this.logger.debug({ msg: 'New message', id: emailId, options });
|
|
3447
|
+
|
|
3448
|
+
if (options.fetchHeaders) {
|
|
3449
|
+
options.headers = options.fetchHeaders;
|
|
3450
|
+
} else {
|
|
3451
|
+
options.headers = 'headers' in options ? options.headers : false;
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
let messageData = await this.getMessage(emailId, options);
|
|
3455
|
+
|
|
3456
|
+
if (!messageData) {
|
|
3457
|
+
await this.notify(this, MESSAGE_MISSING_NOTIFY, {
|
|
3458
|
+
id: emailId
|
|
3459
|
+
});
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
// check if we have seen this message before or not (approximate estimation using HyperLogLog)
|
|
3464
|
+
messageData.seemsLikeNew = messageData.messageSpecialUse !== '\\Sent' && !!(await this.redis.pfadd(this.getSeenMessagesKey(), messageData.messageId));
|
|
3465
|
+
|
|
3466
|
+
return messageData;
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
/**
|
|
3470
|
+
* Search for message IDs matching criteria
|
|
3471
|
+
* Used for bulk operations
|
|
3472
|
+
*/
|
|
3473
|
+
async searchEmailIds(path, search) {
|
|
3474
|
+
let messages = [];
|
|
3475
|
+
let cursor;
|
|
3476
|
+
|
|
3477
|
+
let maxMessages = 1000;
|
|
3478
|
+
let notDone = true;
|
|
3479
|
+
|
|
3480
|
+
while (notDone) {
|
|
3481
|
+
let messageListResult = await this.listMessages(
|
|
3482
|
+
{
|
|
3483
|
+
path,
|
|
3484
|
+
pageSize: 250,
|
|
3485
|
+
search,
|
|
3486
|
+
cursor
|
|
3487
|
+
},
|
|
3488
|
+
{ metadataOnly: true }
|
|
3489
|
+
);
|
|
3490
|
+
|
|
3491
|
+
if (messageListResult?.messages) {
|
|
3492
|
+
messages = messages.concat(messageListResult?.messages);
|
|
3493
|
+
if (messages.length >= maxMessages) {
|
|
3494
|
+
messages = messages.slice(0, maxMessages);
|
|
3495
|
+
notDone = false;
|
|
3496
|
+
break;
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
if (!messageListResult.nextPageCursor) {
|
|
3501
|
+
notDone = false;
|
|
3502
|
+
break;
|
|
3503
|
+
}
|
|
3504
|
+
cursor = messageListResult.nextPageCursor;
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
return messages.map(message => message.id);
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
/**
|
|
3511
|
+
* Convert IMAP SEARCH query to Outlook native search syntax
|
|
3512
|
+
* Uses KQL (Keyword Query Language)
|
|
3513
|
+
*/
|
|
3514
|
+
prepareSearchQuery(search) {
|
|
3515
|
+
search = search || {};
|
|
3516
|
+
|
|
3517
|
+
const searchParts = [];
|
|
3518
|
+
|
|
3519
|
+
const enabledKeys = ['to', 'cc', 'bcc', 'larger', 'smaller', 'body', 'before', 'sentBefore', 'since', 'sentSince'];
|
|
3520
|
+
|
|
3521
|
+
// not supported search terms
|
|
3522
|
+
for (let key of Object.keys(search)) {
|
|
3523
|
+
if (!enabledKeys.includes(key)) {
|
|
3524
|
+
let error = new Error(`Unsupported search term "${key}" for Outlook Search`);
|
|
3525
|
+
error.code = 'UnsupportedSearchTerm';
|
|
3526
|
+
error.statusCode = 400;
|
|
3527
|
+
throw error;
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
/**
|
|
3532
|
+
* Escape and format search terms for KQL
|
|
3533
|
+
*/
|
|
3534
|
+
let escapeString = term => {
|
|
3535
|
+
if (typeof term === 'object' && term && Object.prototype.toString.apply(term) === '[object Date]') {
|
|
3536
|
+
// convert dates to "MM/DD/YYYY"
|
|
3537
|
+
let d = term.getDate();
|
|
3538
|
+
let m = term.getMonth() + 1;
|
|
3539
|
+
let y = term.getFullYear();
|
|
3540
|
+
term = `${m < 10 ? '0' : ''}${m}/${d < 10 ? '0' : ''}${d}/${y}`;
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
let str = term.replace(/[\s"']+/g, ' ').trim();
|
|
3544
|
+
if (str.indexOf(' ') >= 0) {
|
|
3545
|
+
str = `'${str}'`;
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
return str;
|
|
3549
|
+
};
|
|
3550
|
+
|
|
3551
|
+
// Build KQL query parts
|
|
3552
|
+
for (let key of ['from', 'to', 'cc', 'bcc', 'subject', 'body']) {
|
|
3553
|
+
if (search[key]) {
|
|
3554
|
+
searchParts.push(`${key}:${escapeString(search[key])}`);
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
if (search.before || search.sentBefore) {
|
|
3559
|
+
searchParts.push(`received<=${escapeString(search.before || search.sentBefore)}`);
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
if (search.since || search.sentSince) {
|
|
3563
|
+
searchParts.push(`sent>=${escapeString(search.since || search.sentSince)}`);
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
if (search.smaller) {
|
|
3567
|
+
searchParts.push(`size<${Number(search.smaller) || 0}`);
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
if (search.larger) {
|
|
3571
|
+
searchParts.push(`size>${Number(search.larger) || 0}`);
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
return searchParts.join(' ');
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
/**
|
|
3578
|
+
* Convert IMAP SEARCH query to OData filter syntax
|
|
3579
|
+
* More precise than KQL but limited in supported fields
|
|
3580
|
+
*/
|
|
3581
|
+
prepareFilterQuery(search) {
|
|
3582
|
+
search = search || {};
|
|
3583
|
+
|
|
3584
|
+
const filterParts = [];
|
|
3585
|
+
|
|
3586
|
+
// Fields not supported by OData filters
|
|
3587
|
+
const disabledKeys = [
|
|
3588
|
+
'seq',
|
|
3589
|
+
'uid',
|
|
3590
|
+
'paths',
|
|
3591
|
+
'answered',
|
|
3592
|
+
'deleted',
|
|
3593
|
+
'modseq',
|
|
3594
|
+
|
|
3595
|
+
//
|
|
3596
|
+
'to',
|
|
3597
|
+
'cc',
|
|
3598
|
+
'bcc',
|
|
3599
|
+
'larger',
|
|
3600
|
+
'smaller'
|
|
3601
|
+
];
|
|
3602
|
+
|
|
3603
|
+
// not supported search terms
|
|
3604
|
+
for (let disabledKey of disabledKeys) {
|
|
3605
|
+
if (disabledKey in search) {
|
|
3606
|
+
let error = new Error(`Unsupported search term "${disabledKey}"`);
|
|
3607
|
+
error.code = 'UnsupportedSearchTerm';
|
|
3608
|
+
error.statusCode = 400;
|
|
3609
|
+
throw error;
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
// Build OData filter expressions
|
|
3614
|
+
|
|
3615
|
+
// unseen
|
|
3616
|
+
if (typeof search.unseen === 'boolean') {
|
|
3617
|
+
filterParts.push(`isRead eq ${search.unseen ? 'false' : 'true'}`);
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
// seen
|
|
3621
|
+
if (typeof search.seen === 'boolean') {
|
|
3622
|
+
filterParts.push(`isRead eq ${search.seen ? 'true' : 'false'}`);
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
// draft
|
|
3626
|
+
if (typeof search.draft === 'boolean') {
|
|
3627
|
+
filterParts.push(`isDraft eq ${search.draft ? 'true' : 'false'}`);
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
// flagged
|
|
3631
|
+
if (typeof search.flagged === 'boolean') {
|
|
3632
|
+
filterParts.push(`flag/flagStatus eq '${search.flagged ? 'flagged' : 'notFlagged'}'`);
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
// from
|
|
3636
|
+
if (search.from) {
|
|
3637
|
+
filterParts.push(
|
|
3638
|
+
`(from/emailAddress/address eq ${this.formatSearchTerm(search.from)} or contains(from/emailAddress/name, ${this.formatSearchTerm(
|
|
3639
|
+
search.from
|
|
3640
|
+
)}))`
|
|
3641
|
+
);
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
if (search.subject) {
|
|
3645
|
+
filterParts.push(`contains(subject, ${this.formatSearchTerm(search.subject)})`);
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
if (search.body) {
|
|
3649
|
+
filterParts.push(`contains(body/content, ${this.formatSearchTerm(search.body)})`);
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
if (search.emailId) {
|
|
3653
|
+
filterParts.push(`id eq ${this.formatSearchTerm(search.emailId)}`);
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
if (search.threadId) {
|
|
3657
|
+
filterParts.push(`conversationId eq ${this.formatSearchTerm(search.threadId)}`);
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
if (search.before || search.sentBefore) {
|
|
3661
|
+
filterParts.push(`receivedDateTime lt ${this.formatSearchTerm(search.before || search.sentBefore, false)}`);
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
if (search.since || search.sentSince) {
|
|
3665
|
+
filterParts.push(`receivedDateTime gt ${this.formatSearchTerm(search.since || search.sentSince, false)}`);
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
// Limited header search support
|
|
3669
|
+
for (let headerKey of Object.keys(search.header || {})) {
|
|
3670
|
+
switch (headerKey.toLowerCase().trim()) {
|
|
3671
|
+
case 'message-id':
|
|
3672
|
+
filterParts.push(`internetMessageId eq ${this.formatSearchTerm(search.header[headerKey])}`);
|
|
3673
|
+
break;
|
|
3674
|
+
default: {
|
|
3675
|
+
let error = new Error(`Unsupported search header "${headerKey}"`);
|
|
3676
|
+
error.code = 'UnsupportedSearchTerm';
|
|
3677
|
+
error.statusCode = 400;
|
|
3678
|
+
throw error;
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
return filterParts.join(' and ').trim();
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
/**
|
|
3687
|
+
* Implement time-based deduplication using Redis buckets
|
|
3688
|
+
* Prevents processing duplicate events within a time window
|
|
3689
|
+
*/
|
|
3690
|
+
async rollingBucketLock(key, value = '1') {
|
|
3691
|
+
// 10 minute buckets
|
|
3692
|
+
let currentBucketTime = new Date(`${new Date().toISOString().substring(0, 15)}0:00.000Z`).getTime();
|
|
3693
|
+
|
|
3694
|
+
let buckets = 2;
|
|
3695
|
+
|
|
3696
|
+
let pipeline = this.redis.multi();
|
|
3697
|
+
// Check one bucket to the future, current bucket, and one to the past
|
|
3698
|
+
// Always check newer first. In this case, if there is an outdated value in an older bucket, then we use the newer value
|
|
3699
|
+
for (let i = -1; i < buckets; i++) {
|
|
3700
|
+
let bucketTime = new Date(currentBucketTime - i * 10 * 60 * 1000).toISOString();
|
|
3701
|
+
let bucketKey = `${REDIS_PREFIX}bck:${bucketTime}`;
|
|
3702
|
+
pipeline = pipeline.hget(bucketKey, key);
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
let pipelineRes = await pipeline.exec();
|
|
3706
|
+
for (let [, bucketRes] of pipelineRes) {
|
|
3707
|
+
if (bucketRes) {
|
|
3708
|
+
if (bucketRes === value) {
|
|
3709
|
+
return true;
|
|
3710
|
+
}
|
|
3711
|
+
// There is a value, but it does not match the known last value
|
|
3712
|
+
break;
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
// Add the key to current bucket
|
|
3717
|
+
let bucketTime = new Date(currentBucketTime).toISOString();
|
|
3718
|
+
let bucketKey = `${REDIS_PREFIX}bck:${bucketTime}`;
|
|
3719
|
+
await this.redis
|
|
3720
|
+
.multi()
|
|
3721
|
+
.hset(bucketKey, key, value)
|
|
3722
|
+
// make sure the bucket does not expire until the next one is already valid
|
|
3723
|
+
.expire(bucketKey, 12 * 60)
|
|
3724
|
+
.exec();
|
|
3725
|
+
|
|
3726
|
+
return false;
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
/**
|
|
3730
|
+
* Format search terms for OData filters
|
|
3731
|
+
* Handles proper escaping and date formatting
|
|
3732
|
+
*/
|
|
3733
|
+
formatSearchTerm(term, quot = "'") {
|
|
3734
|
+
if (typeof term === 'object' && term && Object.prototype.toString.apply(new Date()) === '[object Date]') {
|
|
3735
|
+
term = term.toISOString();
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3738
|
+
term = (term || '')
|
|
3739
|
+
.toString()
|
|
3740
|
+
.replace(/[\s"]+/g, ' ')
|
|
3741
|
+
.trim();
|
|
3742
|
+
|
|
3743
|
+
if (quot === "'") {
|
|
3744
|
+
term = term.replace(/'/g, "''");
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
return `${quot ? quot : ''}${term}${quot ? quot : ''}`;
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
/**
|
|
3751
|
+
* Decode pagination cursor string
|
|
3752
|
+
* Supports both page numbers and Graph API skip tokens
|
|
3753
|
+
*/
|
|
3754
|
+
decodeCursorStr(cursorStr) {
|
|
3755
|
+
let type = 'ms';
|
|
3756
|
+
|
|
3757
|
+
if (cursorStr) {
|
|
3758
|
+
let splitPos = cursorStr.indexOf('_');
|
|
3759
|
+
if (splitPos >= 0) {
|
|
3760
|
+
let cursorType = cursorStr.substring(0, splitPos);
|
|
3761
|
+
cursorStr = cursorStr.substring(splitPos + 1);
|
|
3762
|
+
if (cursorType && type !== cursorType) {
|
|
3763
|
+
let error = new Error('Invalid cursor');
|
|
3764
|
+
error.code = 'InvalidCursorType';
|
|
3765
|
+
throw error;
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
try {
|
|
3770
|
+
let { page: cursorPage, skipToken } = JSON.parse(Buffer.from(cursorStr, 'base64url'));
|
|
3771
|
+
if (typeof cursorPage === 'number' && cursorPage >= 0) {
|
|
3772
|
+
return { cursorPage, skipToken };
|
|
3773
|
+
}
|
|
3774
|
+
} catch (err) {
|
|
3775
|
+
this.logger.error({ msg: 'Cursor parsing error', cursorStr, err });
|
|
3776
|
+
|
|
3777
|
+
let error = new Error('Invalid paging cursor');
|
|
3778
|
+
error.code = 'InvalidCursorValue';
|
|
3779
|
+
error.statusCode = 400;
|
|
3780
|
+
throw error;
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3784
|
+
return null;
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
/**
|
|
3788
|
+
* Encode pagination state into cursor string
|
|
3789
|
+
* Includes both page number and Graph API skip token
|
|
3790
|
+
*/
|
|
3791
|
+
encodeCursorString(cursorPage, skipToken) {
|
|
3792
|
+
if ((typeof cursorPage !== 'number' && !skipToken) || cursorPage < 0) {
|
|
3793
|
+
return null;
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
cursorPage = cursorPage || 0;
|
|
3797
|
+
|
|
3798
|
+
let type = 'ms';
|
|
3799
|
+
let encodedToken = `${type}_${Buffer.from(JSON.stringify({ page: cursorPage, skipToken })).toString('base64url')}`;
|
|
3800
|
+
|
|
3801
|
+
return encodedToken;
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
module.exports = { OutlookClient };
|