emailengine-app 1.14.8 → 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 +72 -344
- 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 +1576 -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 -1769
- package/lib/lua/z-push.lua +0 -14
- package/lib/mailbox.js +0 -1546
- 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,2628 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { parentPort } = require('worker_threads');
|
|
4
|
+
const { ImapFlow } = require('imapflow');
|
|
5
|
+
const { Mailbox } = require('./imap/mailbox');
|
|
6
|
+
const logger = require('../logger');
|
|
7
|
+
const packageData = require('../../package.json');
|
|
8
|
+
const { backOff } = require('exponential-backoff');
|
|
9
|
+
const msgpack = require('msgpack5')();
|
|
10
|
+
|
|
11
|
+
const { oauth2ProviderData } = require('../oauth2-apps');
|
|
12
|
+
const { BaseClient } = require('./base-client');
|
|
13
|
+
const { oauth2Apps } = require('../oauth2-apps');
|
|
14
|
+
|
|
15
|
+
const { Subconnection } = require('./imap/subconnection');
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
getLocalAddress,
|
|
19
|
+
normalizePath,
|
|
20
|
+
resolveCredentials,
|
|
21
|
+
emitChangeEvent,
|
|
22
|
+
getByteSize,
|
|
23
|
+
getBoolean,
|
|
24
|
+
readEnvValue,
|
|
25
|
+
validUidValidity,
|
|
26
|
+
getDuration,
|
|
27
|
+
LRUCache
|
|
28
|
+
} = require('../tools');
|
|
29
|
+
|
|
30
|
+
// Time to wait between mailbox resync operations (15 minutes)
|
|
31
|
+
const RESYNC_DELAY = 15 * 60;
|
|
32
|
+
// TTL for ensuring main mailbox selection after operations (5 seconds)
|
|
33
|
+
const ENSURE_MAIN_TTL = 5 * 1000;
|
|
34
|
+
|
|
35
|
+
const { AUTH_ERROR_NOTIFY, AUTH_SUCCESS_NOTIFY, CONNECT_ERROR_NOTIFY, DEFAULT_DOWNLOAD_CHUNK_SIZE, MAX_BACKOFF_DELAY, TLS_DEFAULTS } = require('../consts');
|
|
36
|
+
|
|
37
|
+
// Configuration for download operations - chunk size for streaming attachments/messages
|
|
38
|
+
const DOWNLOAD_CHUNK_SIZE = getByteSize(readEnvValue('EENGINE_CHUNK_SIZE')) || DEFAULT_DOWNLOAD_CHUNK_SIZE;
|
|
39
|
+
// Flag to disable IMAP compression (COMPRESS extension) if needed for debugging or compatibility
|
|
40
|
+
const DISABLE_IMAP_COMPRESSION = getBoolean(readEnvValue('EENGINE_DISABLE_COMPRESSION'));
|
|
41
|
+
// Custom socket timeout for IMAP connections
|
|
42
|
+
const IMAP_SOCKET_TIMEOUT = getDuration(readEnvValue('EENGINE_IMAP_SOCKET_TIMEOUT'));
|
|
43
|
+
|
|
44
|
+
// Gmail API configuration
|
|
45
|
+
const GMAIL_API_BASE = 'https://gmail.googleapis.com';
|
|
46
|
+
|
|
47
|
+
logger.trace({ msg: 'Worker configuration', DOWNLOAD_CHUNK_SIZE, DISABLE_IMAP_COMPRESSION, IMAP_SOCKET_TIMEOUT });
|
|
48
|
+
|
|
49
|
+
const settings = require('../settings');
|
|
50
|
+
const { redis } = require('../db');
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Sends metrics data to the parent process for monitoring and analytics
|
|
54
|
+
* @param {Object} meta - Metadata object
|
|
55
|
+
* @param {Object} logger - Logger instance
|
|
56
|
+
* @param {string} key - Metric key
|
|
57
|
+
* @param {string} method - Metric method (e.g., 'inc', 'dec')
|
|
58
|
+
* @param {...any} args - Additional arguments for the metric
|
|
59
|
+
*/
|
|
60
|
+
async function metricsMeta(meta, logger, key, method, ...args) {
|
|
61
|
+
try {
|
|
62
|
+
parentPort.postMessage({
|
|
63
|
+
cmd: 'metrics',
|
|
64
|
+
key,
|
|
65
|
+
method,
|
|
66
|
+
args,
|
|
67
|
+
meta: meta || {}
|
|
68
|
+
});
|
|
69
|
+
} catch (err) {
|
|
70
|
+
logger.error({ msg: 'Failed to post metrics to parent', err });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Main IMAP client class that handles all IMAP operations for an email account
|
|
76
|
+
* Extends BaseClient for common email client functionality
|
|
77
|
+
*/
|
|
78
|
+
class IMAPClient extends BaseClient {
|
|
79
|
+
constructor(account, options) {
|
|
80
|
+
options = options || {};
|
|
81
|
+
super(account, options);
|
|
82
|
+
|
|
83
|
+
// Connection state flags
|
|
84
|
+
this.isClosing = false;
|
|
85
|
+
this.isClosed = false;
|
|
86
|
+
|
|
87
|
+
// Base IMAP configuration that will be merged with account-specific settings
|
|
88
|
+
this.imapConfig = {
|
|
89
|
+
// Set emitLogs to true if you want to get all the log entries as objects from the IMAP module
|
|
90
|
+
logger: this.mainLogger.child({
|
|
91
|
+
sub: 'imap-connection',
|
|
92
|
+
channel: 'primary'
|
|
93
|
+
}),
|
|
94
|
+
clientInfo: {
|
|
95
|
+
name: packageData.name,
|
|
96
|
+
version: packageData.version,
|
|
97
|
+
vendor: (packageData.author && packageData.author.name) || packageData.author,
|
|
98
|
+
'support-url': (packageData.bugs && packageData.bugs.url) || packageData.bugs
|
|
99
|
+
},
|
|
100
|
+
logRaw: this.options.logRaw
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Map of normalized mailbox paths to Mailbox instances
|
|
104
|
+
this.mailboxes = new Map();
|
|
105
|
+
|
|
106
|
+
// Timer for handling untagged EXPUNGE responses
|
|
107
|
+
this.untaggedExpungeTimer = false;
|
|
108
|
+
|
|
109
|
+
// Timer for periodic mailbox listing refresh
|
|
110
|
+
this.refreshListingTimer = false;
|
|
111
|
+
// Timer for periodic mailbox resynchronization
|
|
112
|
+
this.resyncTimer = false;
|
|
113
|
+
|
|
114
|
+
// Timer to ensure we return to the main mailbox after operations
|
|
115
|
+
this.completedTimer = false;
|
|
116
|
+
|
|
117
|
+
// LRU caches for efficient UID packing/unpacking operations
|
|
118
|
+
this.pathCache = new LRUCache();
|
|
119
|
+
this.idCache = new LRUCache();
|
|
120
|
+
|
|
121
|
+
// Default folder delimiter (usually '/' or '.')
|
|
122
|
+
this.defaultDelimiter = '/';
|
|
123
|
+
|
|
124
|
+
// Array of subconnection instances for monitoring multiple folders simultaneously
|
|
125
|
+
this.subconnections = [];
|
|
126
|
+
|
|
127
|
+
// Flag indicating if the connection is paused
|
|
128
|
+
this.paused = false;
|
|
129
|
+
|
|
130
|
+
// Primary IMAP connection instance
|
|
131
|
+
this.imapClient = null;
|
|
132
|
+
// Secondary connection for commands that shouldn't interrupt IDLE
|
|
133
|
+
this.commandClient = null;
|
|
134
|
+
|
|
135
|
+
// Flag indicating if mailbox synchronization is in progress
|
|
136
|
+
this.syncing = false;
|
|
137
|
+
|
|
138
|
+
// Counter for generating unique connection IDs
|
|
139
|
+
this.connectionCount = 0;
|
|
140
|
+
// Set of all active connections for tracking
|
|
141
|
+
this.connections = new Set();
|
|
142
|
+
|
|
143
|
+
// Indexing strategy for this account ('full' or other modes)
|
|
144
|
+
this.imapIndexer = null;
|
|
145
|
+
|
|
146
|
+
// Current connection state
|
|
147
|
+
this.state = 'connecting';
|
|
148
|
+
|
|
149
|
+
// Reconnection tracking for capped exponential backoff
|
|
150
|
+
this.reconnectRetries = 0;
|
|
151
|
+
this.reconnectMaxDelay = 30000; // 30 seconds max delay
|
|
152
|
+
|
|
153
|
+
// Error reconnection tracking
|
|
154
|
+
this.errorReconnectDelay = 2000;
|
|
155
|
+
this.errorReconnectMaxDelay = 30000;
|
|
156
|
+
this.reconnectTimer = null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Called when a task is completed - ensures we return to the main mailbox
|
|
161
|
+
* This prevents the connection from staying in a rarely-used mailbox
|
|
162
|
+
*/
|
|
163
|
+
onTaskCompleted() {
|
|
164
|
+
// check if we need to re-select main mailbox
|
|
165
|
+
this.completedTimer = setTimeout(() => {
|
|
166
|
+
clearTimeout(this.completedTimer);
|
|
167
|
+
this.ensureMainMailbox().catch(err => this.logger.error({ msg: 'Failed to select main mailbox', err }));
|
|
168
|
+
}, ENSURE_MAIN_TTL);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Gets an IMAP connection for executing commands
|
|
173
|
+
* @param {Object} connectionOptions - Connection options
|
|
174
|
+
* @param {boolean} connectionOptions.allowSecondary - Whether to allow using secondary connections
|
|
175
|
+
* @param {boolean} connectionOptions.noPool - Force creation of new connection
|
|
176
|
+
* @param {Object} connectionOptions.connectionClient - Existing connection to reuse
|
|
177
|
+
* @param {string} reason - Reason for requesting the connection (for logging)
|
|
178
|
+
* @returns {Object} IMAP connection instance
|
|
179
|
+
*/
|
|
180
|
+
async getImapConnection(connectionOptions, reason) {
|
|
181
|
+
connectionOptions = connectionOptions || {};
|
|
182
|
+
|
|
183
|
+
let { allowSecondary, noPool, connectionClient: existingConnectionClient } = connectionOptions || {};
|
|
184
|
+
|
|
185
|
+
// If an existing connection was provided and it's usable, return it
|
|
186
|
+
if (existingConnectionClient && existingConnectionClient.usable) {
|
|
187
|
+
return existingConnectionClient;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Determine if we're in a syncing state where secondary connections might be needed
|
|
191
|
+
let syncing = this.syncing || ['init', 'connecting', 'syncing'].includes(this.state);
|
|
192
|
+
if (!noPool && (!syncing || !allowSecondary)) {
|
|
193
|
+
// Return the primary connection for most operations
|
|
194
|
+
return this.imapClient;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// TODO: if noPool is true, then always create a new connection
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
// Try to get or create a command connection for operations that shouldn't interrupt IDLE
|
|
201
|
+
const connectionClient = await this.getCommandConnection(reason);
|
|
202
|
+
if (connectionClient && connectionClient.usable) {
|
|
203
|
+
connectionOptions.connectionClient = connectionClient;
|
|
204
|
+
return connectionClient;
|
|
205
|
+
} else {
|
|
206
|
+
// fall back to default connection
|
|
207
|
+
return this.imapClient;
|
|
208
|
+
}
|
|
209
|
+
} catch (err) {
|
|
210
|
+
this.logger.error({ msg: 'Failed to acquire command connection', reason, err });
|
|
211
|
+
return this.imapClient;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Gets or creates a command connection (secondary connection for non-IDLE operations)
|
|
217
|
+
* This allows running IMAP commands without interrupting the primary connection's IDLE state
|
|
218
|
+
* @param {string} reason - Reason for requesting the connection
|
|
219
|
+
* @returns {Object} Command connection instance
|
|
220
|
+
*/
|
|
221
|
+
async getCommandConnection(reason) {
|
|
222
|
+
// Return existing command connection if available
|
|
223
|
+
if (this.commandClient && this.commandClient.usable) {
|
|
224
|
+
// use existing command channel
|
|
225
|
+
return this.commandClient;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Acquire a lock to prevent multiple simultaneous connection attempts
|
|
229
|
+
let lock = this.accountObject.getLock();
|
|
230
|
+
|
|
231
|
+
let connectLock;
|
|
232
|
+
let lockKey = ['commandClient', this.account].join(':');
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
this.logger.debug({ msg: 'Acquiring connection lock', lockKey });
|
|
236
|
+
connectLock = await lock.waitAcquireLock(lockKey, 5 * 60 * 1000, 1 * 60 * 1000);
|
|
237
|
+
if (!connectLock.success) {
|
|
238
|
+
this.logger.error({ msg: 'Failed to get lock', lockKey });
|
|
239
|
+
throw new Error('Failed to get connection lock');
|
|
240
|
+
}
|
|
241
|
+
this.logger.debug({ msg: 'Acquired connection lock', lockKey, index: connectLock.index });
|
|
242
|
+
} catch (err) {
|
|
243
|
+
this.logger.error({ msg: 'Failed to get lock', lockKey, err });
|
|
244
|
+
throw err;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
// create a new connection for the command channel
|
|
249
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
250
|
+
|
|
251
|
+
// Check if IMAP is configured and enabled
|
|
252
|
+
if ((!accountData.imap && !accountData.oauth2) || (accountData.imap && accountData.imap.disabled)) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Check again if command client was created while waiting for lock
|
|
257
|
+
if (this.commandClient && this.commandClient.usable) {
|
|
258
|
+
// use existing command channel created during the lock
|
|
259
|
+
return this.commandClient;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Generate unique connection ID for tracking
|
|
263
|
+
const commandCid = `${this.cid}:c:${this.connectionCount++}`;
|
|
264
|
+
|
|
265
|
+
let imapConfig = await this.getImapConfig(accountData);
|
|
266
|
+
|
|
267
|
+
// Create new IMAP connection with specific settings for command operations
|
|
268
|
+
let commandClient = new ImapFlow(
|
|
269
|
+
Object.assign({}, imapConfig, {
|
|
270
|
+
disableAutoIdle: true, // Don't automatically IDLE on this connection
|
|
271
|
+
id: commandCid,
|
|
272
|
+
socketTimeout: 60 * 1000, // 60 second timeout for command operations
|
|
273
|
+
logger: this.logger.child({
|
|
274
|
+
cid: commandCid,
|
|
275
|
+
channel: 'command'
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
);
|
|
279
|
+
this.connections.add(commandClient);
|
|
280
|
+
await this.redis.hSetExists(this.getAccountKey(), 'connections', this.connections.size.toString());
|
|
281
|
+
|
|
282
|
+
commandClient.log.debug({ msg: 'Created command client', reason });
|
|
283
|
+
|
|
284
|
+
this.commandClient = commandClient;
|
|
285
|
+
|
|
286
|
+
// Mark this as a secondary connection for identification
|
|
287
|
+
commandClient.secondaryConnection = true;
|
|
288
|
+
|
|
289
|
+
// Set up error handling for the command connection
|
|
290
|
+
const onErr = err => {
|
|
291
|
+
commandClient?.log.error({ msg: 'IMAP connection error', cid: commandCid, channel: 'command', account: this.account, err });
|
|
292
|
+
commandClient.close();
|
|
293
|
+
this.commandClient = null;
|
|
294
|
+
};
|
|
295
|
+
commandClient.on('error', onErr);
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
await commandClient.connect();
|
|
299
|
+
commandClient.log.info({ msg: 'Command channel connected', cid: commandCid, channel: 'command', account: this.account });
|
|
300
|
+
} catch (err) {
|
|
301
|
+
if (this.connections.delete(commandClient)) {
|
|
302
|
+
await this.redis.hSetExists(this.getAccountKey(), 'connections', this.connections.size.toString());
|
|
303
|
+
}
|
|
304
|
+
commandClient.log.error({ msg: 'Failed to connect command client', cid: commandCid, channel: 'command', account: this.account, err });
|
|
305
|
+
throw err;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Clean up when connection closes
|
|
309
|
+
commandClient.on('close', async () => {
|
|
310
|
+
try {
|
|
311
|
+
if (this.connections.delete(commandClient)) {
|
|
312
|
+
await this.redis.hSetExists(this.getAccountKey(), 'connections', this.connections.size.toString());
|
|
313
|
+
}
|
|
314
|
+
commandClient.log.info({ msg: 'Connection closed', cid: commandCid, channel: 'command', account: this.account });
|
|
315
|
+
} catch (err) {
|
|
316
|
+
this.logger.error({ msg: 'Error in command client close handler', cid: commandCid, account: this.account, err });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
this.commandClient = null;
|
|
320
|
+
commandClient.removeAllListeners();
|
|
321
|
+
commandClient = null;
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
return commandClient;
|
|
325
|
+
} finally {
|
|
326
|
+
// Always release the lock
|
|
327
|
+
this.logger.debug({ msg: 'Releasing connection lock', lockKey, index: connectLock.index });
|
|
328
|
+
await lock.releaseLock(connectLock);
|
|
329
|
+
this.logger.debug({ msg: 'Released connection lock', lockKey, index: connectLock.index });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Ensures the main mailbox is selected on the primary connection
|
|
335
|
+
* This is called after operations to return to the monitored mailbox
|
|
336
|
+
*/
|
|
337
|
+
async ensureMainMailbox() {
|
|
338
|
+
let mainPath = this.main ? this.main.path : 'INBOX';
|
|
339
|
+
if (this.mailbox && normalizePath(this.mailbox.path) === normalizePath(mainPath)) {
|
|
340
|
+
// already selected
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// start waiting for changes
|
|
345
|
+
await this.select(mainPath);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Packs a mailbox path and UID into a compact base64url string for use as message IDs
|
|
350
|
+
* This creates a unique identifier that can be unpacked later to retrieve the message
|
|
351
|
+
* @param {Object|string} mailbox - Mailbox object or path
|
|
352
|
+
* @param {number} uid - Message UID
|
|
353
|
+
* @returns {string|false} Base64url encoded ID or false on error
|
|
354
|
+
*/
|
|
355
|
+
async packUid(mailbox, uid) {
|
|
356
|
+
if (isNaN(uid) || !mailbox) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (typeof uid !== 'number') {
|
|
361
|
+
uid = Number(uid);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Convert mailbox path to mailbox object if needed
|
|
365
|
+
if (typeof mailbox === 'string') {
|
|
366
|
+
if (this.mailboxes.has(normalizePath(mailbox))) {
|
|
367
|
+
mailbox = this.mailboxes.get(normalizePath(mailbox));
|
|
368
|
+
} else {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Get stored mailbox status including UID validity
|
|
374
|
+
const storedStatus = await mailbox.getStoredStatus();
|
|
375
|
+
if (!validUidValidity(storedStatus.uidValidity) || !storedStatus.path) {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Pack UID validity and mailbox path into a buffer
|
|
380
|
+
let uidValBuf = Buffer.alloc(8);
|
|
381
|
+
uidValBuf.writeBigUInt64BE(storedStatus.uidValidity, 0);
|
|
382
|
+
let mailboxBuf = Buffer.concat([uidValBuf, Buffer.from(storedStatus.path)]);
|
|
383
|
+
|
|
384
|
+
// Get or create a numeric mailbox ID for efficient storage
|
|
385
|
+
let mailboxId;
|
|
386
|
+
if (this.pathCache.has(mailboxBuf.toString('hex'))) {
|
|
387
|
+
mailboxId = this.pathCache.get(mailboxBuf.toString('hex'));
|
|
388
|
+
} else {
|
|
389
|
+
mailboxId = await this.redis.zGetMailboxId(this.getAccountKey(), this.getMailboxHashKey(), mailboxBuf);
|
|
390
|
+
if (isNaN(mailboxId) || typeof mailboxId !== 'number') {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Cache for future use
|
|
395
|
+
this.pathCache.set(mailboxBuf.toString('hex'), mailboxId);
|
|
396
|
+
this.idCache.set(mailboxId, mailboxBuf);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Pack mailbox ID and UID into final buffer
|
|
400
|
+
let uidBuf = Buffer.alloc(4 + 4);
|
|
401
|
+
uidBuf.writeUInt32BE(mailboxId, 0);
|
|
402
|
+
uidBuf.writeUInt32BE(uid, 4);
|
|
403
|
+
|
|
404
|
+
let res = uidBuf.toString('base64url');
|
|
405
|
+
|
|
406
|
+
return res;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Unpacks a base64url encoded ID back into mailbox path and UID
|
|
411
|
+
* @param {string|Buffer} id - Packed message ID
|
|
412
|
+
* @returns {Object|false} Object with path, uidValidity, and uid or false on error
|
|
413
|
+
*/
|
|
414
|
+
async unpackUid(id) {
|
|
415
|
+
const packed = Buffer.isBuffer(id) ? id : Buffer.from(id, 'base64url');
|
|
416
|
+
|
|
417
|
+
let mailboxId = packed.readUInt32BE(0);
|
|
418
|
+
let uid = packed.readUInt32BE(4);
|
|
419
|
+
|
|
420
|
+
// Look up mailbox path from ID
|
|
421
|
+
let mailboxBuf;
|
|
422
|
+
if (this.idCache.has(mailboxId)) {
|
|
423
|
+
mailboxBuf = this.idCache.get(mailboxId);
|
|
424
|
+
} else {
|
|
425
|
+
mailboxBuf = await this.redis.zGetMailboxPathBuffer(this.getMailboxHashKey(), mailboxId);
|
|
426
|
+
if (!mailboxBuf) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Cache for future use
|
|
431
|
+
this.pathCache.set(mailboxBuf.toString('hex'), mailboxId);
|
|
432
|
+
this.idCache.set(mailboxId, mailboxBuf);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!mailboxBuf) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Extract path and UID validity from buffer
|
|
440
|
+
let path = mailboxBuf.subarray(8).toString();
|
|
441
|
+
return {
|
|
442
|
+
path,
|
|
443
|
+
uidValidity: mailboxBuf.readBigUInt64BE(0).toString(),
|
|
444
|
+
uid
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Unpacks a text content ID to get message location and text part paths
|
|
450
|
+
* @param {string} textId - Base64url encoded text ID
|
|
451
|
+
* @returns {Object} Object with message info and text parts array
|
|
452
|
+
*/
|
|
453
|
+
async getMessageTextPaths(textId) {
|
|
454
|
+
let buf = Buffer.from(textId, 'base64url');
|
|
455
|
+
let id = buf.subarray(0, 8);
|
|
456
|
+
let textParts = msgpack.decode(buf.subarray(8));
|
|
457
|
+
|
|
458
|
+
let message = await this.unpackUid(id);
|
|
459
|
+
if (!message) {
|
|
460
|
+
return { message: false };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return { message, textParts };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Clears all cached data for a specific mailbox
|
|
468
|
+
* Used when a mailbox is deleted or needs to be reset
|
|
469
|
+
* @param {Object} entry - Mailbox entry with path
|
|
470
|
+
*/
|
|
471
|
+
async clearMailboxEntry(entry) {
|
|
472
|
+
if (!entry || !entry.path) {
|
|
473
|
+
return; // ?
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
let mailbox;
|
|
477
|
+
if (!this.mailboxes.has(normalizePath(entry.path))) {
|
|
478
|
+
mailbox = new Mailbox(this, entry);
|
|
479
|
+
} else {
|
|
480
|
+
mailbox = this.mailboxes.get(normalizePath(entry.path));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
await mailbox.clear();
|
|
484
|
+
mailbox = false;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Retrieves the current mailbox listing from the IMAP server
|
|
489
|
+
* Compares with stored listing to detect new/deleted/renamed folders
|
|
490
|
+
* @param {Object} options - Listing options
|
|
491
|
+
* @param {Object} connectionOptions - Connection options
|
|
492
|
+
* @returns {Array} Array of mailbox objects
|
|
493
|
+
*/
|
|
494
|
+
async getCurrentListing(options, connectionOptions) {
|
|
495
|
+
options = options || {};
|
|
496
|
+
|
|
497
|
+
this.checkIMAPConnection(connectionOptions);
|
|
498
|
+
|
|
499
|
+
const connectionClient = await this.getImapConnection(connectionOptions, 'getCurrentListing');
|
|
500
|
+
if (!connectionClient) {
|
|
501
|
+
this.imapClient.close();
|
|
502
|
+
let error = new Error('Failed to get connection');
|
|
503
|
+
error.code = 'ConnectionError';
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
508
|
+
|
|
509
|
+
// Build hints for special-use folders from account configuration
|
|
510
|
+
let specialUseHints = {};
|
|
511
|
+
for (let type of ['sent', 'drafts', 'junk', 'trash', 'archive']) {
|
|
512
|
+
if (accountData.imap && accountData.imap[`${type}MailPath`]) {
|
|
513
|
+
specialUseHints[type] = accountData.imap[`${type}MailPath`];
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
options = Object.assign({}, options, {
|
|
518
|
+
specialUseHints
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Get mailbox listing from server
|
|
522
|
+
let listing = await connectionClient.list(options);
|
|
523
|
+
if (!listing.length) {
|
|
524
|
+
// server bug, the list can never be empty
|
|
525
|
+
this.imapClient.close();
|
|
526
|
+
let error = new Error('Server bug: empty mailbox listing');
|
|
527
|
+
error.code = 'ServerBug';
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Extract delimiter from INBOX (most reliable source)
|
|
532
|
+
let inboxData = (listing || []).find(entry => /^INBOX$/i.test(entry.path));
|
|
533
|
+
if (inboxData && inboxData.delimiter) {
|
|
534
|
+
this.defaultDelimiter = inboxData.delimiter;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ignore non-selectable folders
|
|
538
|
+
listing = listing
|
|
539
|
+
.filter(mailbox => !mailbox.flags.has('\\Noselect'))
|
|
540
|
+
.map(mailbox => {
|
|
541
|
+
mailbox.noInferiors = mailbox.flags.has('\\Noinferiors');
|
|
542
|
+
return mailbox;
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
let hasChanges = false;
|
|
546
|
+
|
|
547
|
+
// compare listing for new / deleted / renamed folders
|
|
548
|
+
let storedListing = await this.redis.hgetallBuffer(this.getMailboxListKey());
|
|
549
|
+
|
|
550
|
+
storedListing = Object.keys(storedListing || {})
|
|
551
|
+
.map(path => {
|
|
552
|
+
try {
|
|
553
|
+
return msgpack.decode(storedListing[path]);
|
|
554
|
+
} catch (err) {
|
|
555
|
+
// should not happen
|
|
556
|
+
}
|
|
557
|
+
return false;
|
|
558
|
+
})
|
|
559
|
+
.filter(entry => entry);
|
|
560
|
+
|
|
561
|
+
// compare listings to detect changes
|
|
562
|
+
for (let mailbox of listing) {
|
|
563
|
+
let existingMailbox = storedListing.find(entry => normalizePath(entry.path) === normalizePath(mailbox.path));
|
|
564
|
+
if (!existingMailbox) {
|
|
565
|
+
// found new!
|
|
566
|
+
mailbox.isNew = true;
|
|
567
|
+
hasChanges = true;
|
|
568
|
+
} else if (
|
|
569
|
+
existingMailbox.delimiter !== mailbox.delimiter ||
|
|
570
|
+
existingMailbox.specialUseSource !== mailbox.specialUseSource ||
|
|
571
|
+
existingMailbox.noInferiors !== mailbox.noInferiors
|
|
572
|
+
) {
|
|
573
|
+
hasChanges = true;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Check for deleted mailboxes
|
|
578
|
+
for (let entry of storedListing) {
|
|
579
|
+
if (!listing.some(mailbox => normalizePath(entry.path) === normalizePath(mailbox.path))) {
|
|
580
|
+
// found deleted!
|
|
581
|
+
await this.clearMailboxEntry(entry);
|
|
582
|
+
hasChanges = true;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// on changes store updated listing
|
|
587
|
+
if (hasChanges) {
|
|
588
|
+
// store
|
|
589
|
+
const listingObject = {};
|
|
590
|
+
listing.forEach(entry => {
|
|
591
|
+
let mailbox = {};
|
|
592
|
+
Object.keys(entry).forEach(key => {
|
|
593
|
+
if (['path', 'specialUse', 'name', 'listed', 'subscribed', 'delimiter', 'specialUseSource', 'noInferiors'].includes(key)) {
|
|
594
|
+
mailbox[key] = entry[key];
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
listingObject[normalizePath(entry.path)] = msgpack.encode(mailbox);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// Atomic update of the mailbox listing
|
|
601
|
+
await this.redis.multi().del(this.getMailboxListKey()).hmset(this.getMailboxListKey(), listingObject).exec();
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return listing;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Refreshes the folder list and creates Mailbox instances for new folders
|
|
609
|
+
* @returns {Set} Set of newly created mailbox instances that need syncing
|
|
610
|
+
*/
|
|
611
|
+
async refreshFolderList() {
|
|
612
|
+
if (this.refreshingList) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
this.refreshingList = true;
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
619
|
+
|
|
620
|
+
// Get configured paths to monitor (default to all)
|
|
621
|
+
const accountPaths = [].concat(accountData.path || '*');
|
|
622
|
+
if (!accountPaths.length) {
|
|
623
|
+
accountPaths.push('*');
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
let listing = await this.getCurrentListing();
|
|
627
|
+
|
|
628
|
+
let syncNeeded = new Set();
|
|
629
|
+
for (let entry of listing) {
|
|
630
|
+
if (
|
|
631
|
+
// previously unseen
|
|
632
|
+
!this.mailboxes.has(normalizePath(entry.path))
|
|
633
|
+
) {
|
|
634
|
+
// Apply filtering rules based on account configuration
|
|
635
|
+
if (!accountPaths.includes('*')) {
|
|
636
|
+
if (!accountPaths.includes(entry.path) && !accountPaths.includes(entry.specialUse)) {
|
|
637
|
+
// ignore changes
|
|
638
|
+
entry.syncDisabled = true;
|
|
639
|
+
}
|
|
640
|
+
} else if (this.isGmail && !['\\All', '\\Junk', '\\Trash'].includes(entry.specialUse)) {
|
|
641
|
+
// For Gmail, only monitor All Mail, Spam, and Trash folders
|
|
642
|
+
// Other folders are just labels and covered by All Mail
|
|
643
|
+
entry.syncDisabled = true;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
let mailbox = new Mailbox(this, entry);
|
|
647
|
+
this.mailboxes.set(normalizePath(entry.path), mailbox);
|
|
648
|
+
syncNeeded.add(mailbox);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// sync new mailboxes
|
|
653
|
+
for (let mailbox of syncNeeded) {
|
|
654
|
+
await mailbox.sync(true);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return syncNeeded;
|
|
658
|
+
} finally {
|
|
659
|
+
this.refreshingList = false;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Establishes initial IMAP connection and performs setup
|
|
665
|
+
* Sets up event handlers and determines account type (Gmail, Outlook, etc.)
|
|
666
|
+
* @returns {Object} Connection response
|
|
667
|
+
*/
|
|
668
|
+
async connect() {
|
|
669
|
+
if (this.isClosing || this.isClosed) {
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
this.state = 'connecting';
|
|
674
|
+
await this.setStateVal();
|
|
675
|
+
|
|
676
|
+
let imapClient = this.imapClient;
|
|
677
|
+
|
|
678
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
679
|
+
|
|
680
|
+
// throws if connection fails
|
|
681
|
+
let response = await imapClient.connect();
|
|
682
|
+
|
|
683
|
+
this.state = 'syncing';
|
|
684
|
+
await this.setStateVal();
|
|
685
|
+
await emitChangeEvent(this.logger, this.account, 'state', this.state);
|
|
686
|
+
|
|
687
|
+
let listing = await this.getCurrentListing();
|
|
688
|
+
|
|
689
|
+
// Detect email provider type based on capabilities and folder structure
|
|
690
|
+
// User might have disabled All Mail folder access and in that case we should treat it as a regular mailbox
|
|
691
|
+
this.isGmail = imapClient.capabilities.has('X-GM-EXT-1') && listing.some(entry => entry.specialUse === '\\All');
|
|
692
|
+
this.isOutlook = /\boffice365\.com$/i.test(imapClient.host); // || /The Microsoft Exchange IMAP4 service is ready/.test(imapClient.greeting);
|
|
693
|
+
this.isLarkSuite = /\blarksuite\.com$/i.test(imapClient.host);
|
|
694
|
+
|
|
695
|
+
const accountPaths = [].concat(accountData.path || '*');
|
|
696
|
+
if (!accountPaths.length) {
|
|
697
|
+
accountPaths.push('*');
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// store synced folder entries
|
|
701
|
+
const mainList = [];
|
|
702
|
+
|
|
703
|
+
// Process mailbox listing and determine which folders to monitor
|
|
704
|
+
for (let entry of listing) {
|
|
705
|
+
if (!accountPaths.includes('*')) {
|
|
706
|
+
// Limited path monitoring - only specific folders
|
|
707
|
+
if (!accountPaths.includes(entry.path) && !accountPaths.includes(entry.specialUse)) {
|
|
708
|
+
entry.syncDisabled = true;
|
|
709
|
+
} else {
|
|
710
|
+
// insert to stored list with the sorting index
|
|
711
|
+
let index = accountPaths.indexOf(entry.path) >= 0 ? accountPaths.indexOf(entry.path) : accountPaths.indexOf(entry.specialUse);
|
|
712
|
+
mainList.push({ index, entry });
|
|
713
|
+
}
|
|
714
|
+
} else {
|
|
715
|
+
// Monitor all folders - determine main folder for IDLE
|
|
716
|
+
if ((this.isGmail && entry.specialUse === '\\All') || (!this.isGmail && entry.specialUse === '\\Inbox')) {
|
|
717
|
+
// In case of gmail prefer All mail folder as the folder to actively track, otherwise INBOX
|
|
718
|
+
// idle in this folder
|
|
719
|
+
this.main = entry;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (this.isGmail && !['\\All', '\\Junk', '\\Trash'].includes(entry.specialUse)) {
|
|
723
|
+
// do not look for changes from this folder
|
|
724
|
+
entry.syncDisabled = true;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
let mailbox = new Mailbox(this, entry);
|
|
729
|
+
this.mailboxes.set(normalizePath(entry.path), mailbox);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (mainList.length) {
|
|
733
|
+
// set the highest synced entry as the main folder
|
|
734
|
+
this.main = mainList.sort((a, b) => a.index - b.index)[0].entry;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Set up event handlers for IMAP notifications
|
|
738
|
+
|
|
739
|
+
// Process untagged EXISTS responses (new messages)
|
|
740
|
+
imapClient.on('exists', async event => {
|
|
741
|
+
if (!event || !event.path || !this.mailboxes.has(normalizePath(event.path))) {
|
|
742
|
+
return; //?
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
let mailbox = this.mailboxes.get(normalizePath(event.path));
|
|
746
|
+
try {
|
|
747
|
+
await mailbox.onExists(event);
|
|
748
|
+
} catch (err) {
|
|
749
|
+
imapClient.log.error({ msg: 'Exists error', err });
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// Handle mailbox open events
|
|
754
|
+
imapClient.on('mailboxOpen', async event => {
|
|
755
|
+
if (!event || !event.path || !this.mailboxes.has(normalizePath(event.path))) {
|
|
756
|
+
return; //?
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
let mailbox = this.mailboxes.get(normalizePath(event.path));
|
|
760
|
+
try {
|
|
761
|
+
await mailbox.onOpen(event);
|
|
762
|
+
} catch (err) {
|
|
763
|
+
imapClient.log.error({ msg: 'Open error', err });
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// Handle mailbox close events
|
|
768
|
+
imapClient.on('mailboxClose', async event => {
|
|
769
|
+
if (!event || !event.path || !this.mailboxes.has(normalizePath(event.path))) {
|
|
770
|
+
return; //?
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
let mailbox = this.mailboxes.get(normalizePath(event.path));
|
|
774
|
+
try {
|
|
775
|
+
await mailbox.onClose(event);
|
|
776
|
+
} catch (err) {
|
|
777
|
+
imapClient.log.error({ msg: 'Close error', err });
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
// Handle flag changes
|
|
782
|
+
imapClient.on('flags', async event => {
|
|
783
|
+
if (!event || !event.path || !this.mailboxes.has(normalizePath(event.path))) {
|
|
784
|
+
return; //?
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
let mailbox = this.mailboxes.get(normalizePath(event.path));
|
|
788
|
+
try {
|
|
789
|
+
await mailbox.onFlags(event);
|
|
790
|
+
} catch (err) {
|
|
791
|
+
imapClient.log.error({ msg: 'Flags error', err });
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
return response;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Handles reconnection with exponential backoff
|
|
800
|
+
* @param {boolean} force - Force reconnection even if paused/closed
|
|
801
|
+
* @returns {boolean} Success status
|
|
802
|
+
*/
|
|
803
|
+
async reconnect(force) {
|
|
804
|
+
if (this._connecting) {
|
|
805
|
+
// backoff reconnect already in progress
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
808
|
+
if (this.paused || this.isClosing || (this.isClosed && !force)) {
|
|
809
|
+
this.logger.debug({
|
|
810
|
+
msg: 'Skipped establishing connection',
|
|
811
|
+
paused: this.paused,
|
|
812
|
+
hasClient: !!this.imapClient,
|
|
813
|
+
usable: this.imapClient?.usable,
|
|
814
|
+
closing: this.isClosing,
|
|
815
|
+
closed: this.isClosed,
|
|
816
|
+
force
|
|
817
|
+
});
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
820
|
+
this.logger.debug({ msg: 'Establishing connection', force });
|
|
821
|
+
|
|
822
|
+
if (force) {
|
|
823
|
+
// Close all subconnections and command client when forcing reconnection
|
|
824
|
+
this.closeSubconnections();
|
|
825
|
+
if (this.commandClient) {
|
|
826
|
+
this.commandClient.close();
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
this._connecting = true;
|
|
831
|
+
this.isClosed = false;
|
|
832
|
+
|
|
833
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
834
|
+
// Get indexing strategy for this account
|
|
835
|
+
this.imapIndexer = typeof accountData.imapIndexer === 'string' && accountData.imapIndexer ? accountData.imapIndexer : 'full';
|
|
836
|
+
|
|
837
|
+
try {
|
|
838
|
+
this.logger.debug({ msg: 'Initiating connection to IMAP' });
|
|
839
|
+
// Use exponential backoff for connection attempts
|
|
840
|
+
await backOff(() => this.start(), {
|
|
841
|
+
maxDelay: MAX_BACKOFF_DELAY,
|
|
842
|
+
numOfAttempts: Infinity,
|
|
843
|
+
retry: () => !this.isClosing && !this.isClosed,
|
|
844
|
+
startingDelay: 2000
|
|
845
|
+
});
|
|
846
|
+
this.logger.debug({
|
|
847
|
+
msg: 'Connection created',
|
|
848
|
+
hasClient: !!this.imapClient,
|
|
849
|
+
usable: this.imapClient && this.imapClient.usable,
|
|
850
|
+
connected: this.isConnected()
|
|
851
|
+
});
|
|
852
|
+
} finally {
|
|
853
|
+
this._connecting = false;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (this.paused) {
|
|
857
|
+
this.logger.debug({ msg: 'Skipped connection setup', reason: 'paused' });
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (this.state === 'unset') {
|
|
862
|
+
this.logger.debug({ msg: 'Skipped connection setup', reason: 'unset' });
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
try {
|
|
867
|
+
await this.checkIMAPConnection();
|
|
868
|
+
this.logger.debug({ msg: 'Starting mailbox sync' });
|
|
869
|
+
await this.syncMailboxes();
|
|
870
|
+
this.logger.debug({ msg: 'Mailboxes synced', usable: this.imapClient?.usable });
|
|
871
|
+
|
|
872
|
+
if (this.imapClient?.usable) {
|
|
873
|
+
// was able to finish syncing, clear the failure flag
|
|
874
|
+
try {
|
|
875
|
+
await this.redis.hdel(this.getAccountKey(), 'syncError');
|
|
876
|
+
} catch (err) {
|
|
877
|
+
// ignore
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
} catch (err) {
|
|
881
|
+
// ended in an unconncted state
|
|
882
|
+
this.logger.error({ msg: 'Failed to set up connection, will retry', err });
|
|
883
|
+
|
|
884
|
+
// Calculate delay with capped exponential backoff
|
|
885
|
+
const retryDelay = Math.min(this.reconnectMaxDelay, 1000 * Math.pow(1.5, Math.min(this.reconnectRetries, 10)));
|
|
886
|
+
this.reconnectRetries++;
|
|
887
|
+
|
|
888
|
+
this.logger.info({
|
|
889
|
+
msg: 'Scheduling reconnection attempt',
|
|
890
|
+
attempt: this.reconnectRetries,
|
|
891
|
+
delay: retryDelay
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
return setTimeout(() => {
|
|
895
|
+
this.reconnect()
|
|
896
|
+
.then(() => {
|
|
897
|
+
// Reset counter on successful reconnect
|
|
898
|
+
this.reconnectRetries = 0;
|
|
899
|
+
})
|
|
900
|
+
.catch(err => {
|
|
901
|
+
this.logger.error({
|
|
902
|
+
msg: 'Connection retry failed',
|
|
903
|
+
err,
|
|
904
|
+
attempt: this.reconnectRetries
|
|
905
|
+
});
|
|
906
|
+
});
|
|
907
|
+
}, retryDelay);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
this.logger.debug({
|
|
911
|
+
msg: 'Connection established',
|
|
912
|
+
hasClient: !!this.imapClient,
|
|
913
|
+
usable: this.imapClient && this.imapClient.usable,
|
|
914
|
+
connected: this.isConnected()
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Synchronizes all mailboxes and schedules periodic resync
|
|
920
|
+
* Handles the main synchronization loop for the account
|
|
921
|
+
*/
|
|
922
|
+
async syncMailboxes() {
|
|
923
|
+
// Clear any pending timers
|
|
924
|
+
clearTimeout(this.untaggedExpungeTimer);
|
|
925
|
+
clearTimeout(this.resyncTimer);
|
|
926
|
+
clearTimeout(this.completedTimer);
|
|
927
|
+
|
|
928
|
+
if (!this.imapClient || !this.imapClient.usable) {
|
|
929
|
+
this.logger.debug({ msg: 'Skipped syncing', reason: 'no imap client' });
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Refresh folder list and sync new folders
|
|
934
|
+
let synced = await this.refreshFolderList();
|
|
935
|
+
|
|
936
|
+
// Sync existing folders that weren't just synced
|
|
937
|
+
for (let mailbox of this.mailboxes.values()) {
|
|
938
|
+
if (!synced || !synced.has(mailbox)) {
|
|
939
|
+
await mailbox.sync();
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (!this.imapClient || !this.imapClient.usable) {
|
|
944
|
+
this.logger.debug({ msg: 'Syncing completed, skipping state change', reason: 'no imap client' });
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Update state to connected
|
|
949
|
+
this.state = 'connected';
|
|
950
|
+
await this.setStateVal();
|
|
951
|
+
|
|
952
|
+
// Store IMAP server capabilities for reference
|
|
953
|
+
const capabilities = (this.imapClient.rawCapabilities || []).map(entry => entry && entry.value).filter(entry => entry);
|
|
954
|
+
const authCapabilities = [];
|
|
955
|
+
let lastUsedAuthCapability = null;
|
|
956
|
+
if (this.imapClient.authCapabilities) {
|
|
957
|
+
for (let [authCapa, usedAuth] of this.imapClient.authCapabilities) {
|
|
958
|
+
authCapabilities.push(authCapa);
|
|
959
|
+
if (usedAuth) {
|
|
960
|
+
lastUsedAuthCapability = authCapa;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const serverInfo = Object.assign({}, this.imapClient.serverInfo || {}, {
|
|
966
|
+
capabilities,
|
|
967
|
+
authCapabilities,
|
|
968
|
+
lastUsedAuthCapability
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
await this.redis.hSetExists(this.getAccountKey(), 'imapServerInfo', JSON.stringify(serverInfo));
|
|
972
|
+
// Clear error state on successful connection
|
|
973
|
+
await this.redis.hdel(this.getAccountKey(), 'lastErrorState', 'lastError:errorCount', 'lastError:first');
|
|
974
|
+
await emitChangeEvent(this.logger, this.account, 'state', this.state);
|
|
975
|
+
|
|
976
|
+
// Select main mailbox for IDLE monitoring
|
|
977
|
+
let mainPath = this.main ? this.main.path : 'INBOX';
|
|
978
|
+
if (this.mailbox && normalizePath(this.mailbox.path) === normalizePath(mainPath)) {
|
|
979
|
+
// already selected
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
this.logger.debug({ msg: 'Syncing completed, selecting main path', path: mainPath });
|
|
984
|
+
// start waiting for changes
|
|
985
|
+
await this.select(mainPath);
|
|
986
|
+
|
|
987
|
+
// Schedule next sync cycle
|
|
988
|
+
let retryCount = 0;
|
|
989
|
+
let setSyncTimer = () => {
|
|
990
|
+
clearTimeout(this.resyncTimer);
|
|
991
|
+
this.resyncTimer = setTimeout(() => {
|
|
992
|
+
this.syncMailboxes()
|
|
993
|
+
.then(() => {
|
|
994
|
+
// Reset retry count on successful sync
|
|
995
|
+
retryCount = 0;
|
|
996
|
+
})
|
|
997
|
+
.catch(err => {
|
|
998
|
+
this.logger.error({ msg: 'Mailbox Sync Error', err, retryCount });
|
|
999
|
+
retryCount++;
|
|
1000
|
+
// Exponential backoff with max delay of 30 seconds
|
|
1001
|
+
const retryDelay = Math.min(30000, 2000 * Math.pow(2, retryCount));
|
|
1002
|
+
this.logger.warn({ msg: 'Scheduling sync retry with backoff', retryDelay, retryCount });
|
|
1003
|
+
setTimeout(setSyncTimer, retryDelay);
|
|
1004
|
+
});
|
|
1005
|
+
}, this.resyncDelay);
|
|
1006
|
+
};
|
|
1007
|
+
setSyncTimer();
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Selects a mailbox on the IMAP connection
|
|
1012
|
+
* @param {string} path - Mailbox path to select
|
|
1013
|
+
*/
|
|
1014
|
+
async select(path) {
|
|
1015
|
+
if (!this.mailboxes.has(normalizePath(path))) {
|
|
1016
|
+
// nothing to do here, mailbox not found
|
|
1017
|
+
this.logger.debug({ msg: 'Can not select unlisted path', path });
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
let mailbox = this.mailboxes.get(normalizePath(path));
|
|
1022
|
+
await mailbox.select();
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Builds IMAP configuration from account data
|
|
1027
|
+
* Handles both OAuth2 and regular authentication
|
|
1028
|
+
* @param {Object} accountData - Account configuration data
|
|
1029
|
+
* @param {Object} ctx - Context object (defaults to this)
|
|
1030
|
+
* @returns {Object} Complete IMAP configuration
|
|
1031
|
+
*/
|
|
1032
|
+
async getImapConfig(accountData, ctx) {
|
|
1033
|
+
if (!accountData) {
|
|
1034
|
+
accountData = await this.accountObject.loadAccountData();
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// the same method is also called by subconnections, so do not mark the primary connection as failing if something happens
|
|
1038
|
+
ctx = ctx || this;
|
|
1039
|
+
let imapConnectionConfig;
|
|
1040
|
+
|
|
1041
|
+
if (accountData.oauth2 && accountData.oauth2.auth) {
|
|
1042
|
+
// OAuth2 authentication configuration
|
|
1043
|
+
const { oauth2User, accessToken, oauth2App } = await this.loadOAuth2AccountCredentials(accountData, ctx, 'imap');
|
|
1044
|
+
const providerData = oauth2ProviderData(oauth2App.provider, oauth2App.cloud);
|
|
1045
|
+
|
|
1046
|
+
imapConnectionConfig = Object.assign(
|
|
1047
|
+
{
|
|
1048
|
+
auth: {
|
|
1049
|
+
user: oauth2User,
|
|
1050
|
+
accessToken
|
|
1051
|
+
},
|
|
1052
|
+
resyncDelay: RESYNC_DELAY
|
|
1053
|
+
},
|
|
1054
|
+
providerData.imap || {}
|
|
1055
|
+
);
|
|
1056
|
+
} else {
|
|
1057
|
+
// Regular password authentication
|
|
1058
|
+
// deep copy of imap settings
|
|
1059
|
+
imapConnectionConfig = JSON.parse(JSON.stringify(accountData.imap));
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// If authentication server is set then it overrides authentication data
|
|
1063
|
+
if (imapConnectionConfig.useAuthServer) {
|
|
1064
|
+
try {
|
|
1065
|
+
imapConnectionConfig.auth = await resolveCredentials(this.account, 'imap');
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
err.authenticationFailed = true;
|
|
1068
|
+
await ctx.notify(false, AUTH_ERROR_NOTIFY, {
|
|
1069
|
+
response: err.message,
|
|
1070
|
+
serverResponseCode: 'HTTPRequestError'
|
|
1071
|
+
});
|
|
1072
|
+
ctx.logger.error({
|
|
1073
|
+
account: this.account,
|
|
1074
|
+
err
|
|
1075
|
+
});
|
|
1076
|
+
ctx.state = AUTH_ERROR_NOTIFY;
|
|
1077
|
+
throw err;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Configure TLS settings
|
|
1082
|
+
if (!imapConnectionConfig.tls) {
|
|
1083
|
+
imapConnectionConfig.tls = {};
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Set local address for outgoing connections (for IP rotation)
|
|
1087
|
+
const localAddress = await getLocalAddress(redis, 'imap', this.account);
|
|
1088
|
+
imapConnectionConfig.tls.localAddress = localAddress.localAddress;
|
|
1089
|
+
this.logger.info({
|
|
1090
|
+
msg: 'Selected local address',
|
|
1091
|
+
account: this.account,
|
|
1092
|
+
proto: 'IMAP',
|
|
1093
|
+
address: localAddress.localAddress,
|
|
1094
|
+
name: localAddress.name,
|
|
1095
|
+
selector: localAddress.addressSelector
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
// Apply default TLS settings
|
|
1099
|
+
for (let key of Object.keys(TLS_DEFAULTS)) {
|
|
1100
|
+
if (!(key in imapConnectionConfig.tls)) {
|
|
1101
|
+
imapConnectionConfig.tls[key] = TLS_DEFAULTS[key];
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// reload log config
|
|
1106
|
+
await this.accountLogger.reload();
|
|
1107
|
+
|
|
1108
|
+
// Build final IMAP configuration
|
|
1109
|
+
let imapConfig = Object.assign(
|
|
1110
|
+
{
|
|
1111
|
+
resyncDelay: RESYNC_DELAY,
|
|
1112
|
+
id: this.cid,
|
|
1113
|
+
emitLogs: this.accountLogger.enabled
|
|
1114
|
+
},
|
|
1115
|
+
imapConnectionConfig,
|
|
1116
|
+
this.imapConfig,
|
|
1117
|
+
{
|
|
1118
|
+
// Allow customization of client identification
|
|
1119
|
+
clientInfo: {
|
|
1120
|
+
name: (await settings.get('imapClientName')) || this.imapConfig.clientInfo.name,
|
|
1121
|
+
version: (await settings.get('imapClientVersion')) || this.imapConfig.clientInfo.version,
|
|
1122
|
+
vendor: (await settings.get('imapClientVendor')) || this.imapConfig.clientInfo.vendor,
|
|
1123
|
+
'support-url': (await settings.get('imapClientSupportUrl')) || this.imapConfig.clientInfo['support-url']
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
);
|
|
1127
|
+
|
|
1128
|
+
this.resyncDelay = imapConfig.resyncDelay * 1000;
|
|
1129
|
+
|
|
1130
|
+
// set up proxy if needed
|
|
1131
|
+
if (accountData.proxy) {
|
|
1132
|
+
imapConfig.proxy = accountData.proxy;
|
|
1133
|
+
} else {
|
|
1134
|
+
let proxyUrl = await settings.get('proxyUrl');
|
|
1135
|
+
let proxyEnabled = await settings.get('proxyEnabled');
|
|
1136
|
+
if (proxyEnabled && proxyUrl && !imapConfig.proxy) {
|
|
1137
|
+
imapConfig.proxy = proxyUrl;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Provider-specific workarounds
|
|
1142
|
+
if (/(\.rambler\.ru|\.163\.com)$/i.test(imapConfig.host)) {
|
|
1143
|
+
// Special case for Rambler and 163. Break IDLE at least once a minute
|
|
1144
|
+
imapConfig.maxIdleTime = 55 * 1000;
|
|
1145
|
+
} else if (/\.yahoo\.com$/i.test(imapConfig.host)) {
|
|
1146
|
+
// Special case for Yahoo. Break IDLE at least once every three minutes
|
|
1147
|
+
imapConfig.maxIdleTime = 3 * 60 * 1000;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/*
|
|
1151
|
+
else if (/(\.naver\.com)$/i.test(imapConfig.host)) {
|
|
1152
|
+
// NOOP does nothing in Naver, must run SELECT for changes in the folder to apply
|
|
1153
|
+
imapConfig.maxIdleTime = 55 * 1000;
|
|
1154
|
+
imapConfig.missingIdleCommand = 'SELECT';
|
|
1155
|
+
}
|
|
1156
|
+
*/
|
|
1157
|
+
|
|
1158
|
+
// Apply global settings
|
|
1159
|
+
if (DISABLE_IMAP_COMPRESSION) {
|
|
1160
|
+
imapConfig.disableCompression = true;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
if (IMAP_SOCKET_TIMEOUT) {
|
|
1164
|
+
imapConfig.socketTimeout = IMAP_SOCKET_TIMEOUT;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Handle certificate validation settings
|
|
1168
|
+
const ignoreMailCertErrors = await settings.get('ignoreMailCertErrors');
|
|
1169
|
+
if (ignoreMailCertErrors && imapConfig?.tls?.rejectUnauthorized !== false) {
|
|
1170
|
+
imapConfig.tls = imapConfig.tls || {};
|
|
1171
|
+
imapConfig.tls.rejectUnauthorized = false;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
return imapConfig;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* Initializes a new IMAP connection and sets up event handlers
|
|
1179
|
+
* This is the main connection setup method
|
|
1180
|
+
*/
|
|
1181
|
+
async start() {
|
|
1182
|
+
if (this.paused) {
|
|
1183
|
+
this.logger.debug({ msg: 'Skipped start', reason: 'paused' });
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
let initialState = this.state;
|
|
1188
|
+
|
|
1189
|
+
// Clean up existing connection if any
|
|
1190
|
+
if (this.imapClient) {
|
|
1191
|
+
this.logger.debug({ msg: 'Clearing previous connection' });
|
|
1192
|
+
let prevImapClient = this.imapClient;
|
|
1193
|
+
prevImapClient.disabled = true;
|
|
1194
|
+
try {
|
|
1195
|
+
prevImapClient.removeAllListeners();
|
|
1196
|
+
|
|
1197
|
+
const prevImapErrorHandler = err => {
|
|
1198
|
+
this.logger.error({ msg: 'IMAP connection error', type: 'imapClient', previous: true, account: this.account, err });
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
prevImapClient.once('error', prevImapErrorHandler);
|
|
1202
|
+
prevImapClient.close();
|
|
1203
|
+
prevImapClient.removeListener('error', prevImapErrorHandler);
|
|
1204
|
+
|
|
1205
|
+
if (this.commandClient) {
|
|
1206
|
+
this.logger.debug({ msg: 'Clearing previous command connection' });
|
|
1207
|
+
this.commandClient.close();
|
|
1208
|
+
}
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
this.logger.error({ msg: 'IMAP close error', err });
|
|
1211
|
+
} finally {
|
|
1212
|
+
if (prevImapClient === this.imapClient) {
|
|
1213
|
+
this.imapClient = null;
|
|
1214
|
+
}
|
|
1215
|
+
prevImapClient = null;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
try {
|
|
1220
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
1221
|
+
|
|
1222
|
+
// Load notification settings
|
|
1223
|
+
this.notifyFrom = accountData.notifyFrom;
|
|
1224
|
+
this.syncFrom = accountData.syncFrom;
|
|
1225
|
+
|
|
1226
|
+
if ((!accountData.imap && !accountData.oauth2) || (accountData.imap && accountData.imap.disabled)) {
|
|
1227
|
+
// can not make a connection
|
|
1228
|
+
this.state = 'unset';
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
let imapConfig = await this.getImapConfig(accountData);
|
|
1233
|
+
|
|
1234
|
+
// Generate unique connection ID
|
|
1235
|
+
imapConfig.id = `${imapConfig.id}:m:${this.connectionCount++}`;
|
|
1236
|
+
|
|
1237
|
+
// Create new IMAP connection instance
|
|
1238
|
+
let imapClient = new ImapFlow(
|
|
1239
|
+
Object.assign({}, imapConfig, {
|
|
1240
|
+
expungeHandler: async payload => await this.expungeHandler(payload)
|
|
1241
|
+
})
|
|
1242
|
+
);
|
|
1243
|
+
this.connections.add(imapClient);
|
|
1244
|
+
await this.redis.hSetExists(this.getAccountKey(), 'connections', this.connections.size.toString());
|
|
1245
|
+
|
|
1246
|
+
imapClient.log.debug({ msg: 'Created primary client' });
|
|
1247
|
+
|
|
1248
|
+
this.imapClient = imapClient;
|
|
1249
|
+
|
|
1250
|
+
// Mark as primary connection
|
|
1251
|
+
imapClient.primaryConnection = true;
|
|
1252
|
+
|
|
1253
|
+
// Forward IMAP logs to account logger
|
|
1254
|
+
// if emitLogs option is true then separate log event is fired for every log entry
|
|
1255
|
+
imapClient.on('log', entry => {
|
|
1256
|
+
if (!entry) {
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (typeof entry === 'string') {
|
|
1261
|
+
// should not happen
|
|
1262
|
+
entry = { msg: entry };
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
this.accountLogger.log(entry);
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
// Handle connection errors
|
|
1269
|
+
imapClient.on('error', err => {
|
|
1270
|
+
imapClient?.log.error({ msg: 'IMAP connection error', type: 'imapClient', account: this.account, err });
|
|
1271
|
+
if (imapClient !== this.imapClient || this._connecting) {
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
imapClient.close(); // ensure the client is closed on errors
|
|
1275
|
+
|
|
1276
|
+
// Debounced reconnection with exponential backoff
|
|
1277
|
+
if (!this.reconnectTimer) {
|
|
1278
|
+
// Calculate delay with cap
|
|
1279
|
+
this.errorReconnectDelay = Math.min(this.errorReconnectMaxDelay, this.errorReconnectDelay * 1.5);
|
|
1280
|
+
|
|
1281
|
+
this.logger.info({
|
|
1282
|
+
msg: 'Scheduling error-triggered reconnection',
|
|
1283
|
+
delay: this.errorReconnectDelay
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
this.reconnectTimer = setTimeout(() => {
|
|
1287
|
+
this.reconnectTimer = null;
|
|
1288
|
+
this.reconnect()
|
|
1289
|
+
.then(() => {
|
|
1290
|
+
// Reset delay on success
|
|
1291
|
+
this.errorReconnectDelay = 2000;
|
|
1292
|
+
})
|
|
1293
|
+
.catch(err => {
|
|
1294
|
+
this.logger.error({
|
|
1295
|
+
msg: 'IMAP reconnection error',
|
|
1296
|
+
account: this.account,
|
|
1297
|
+
err,
|
|
1298
|
+
nextDelay: this.errorReconnectDelay
|
|
1299
|
+
});
|
|
1300
|
+
});
|
|
1301
|
+
}, this.errorReconnectDelay);
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
// Track IMAP command/response metrics
|
|
1306
|
+
imapClient.on('response', data => {
|
|
1307
|
+
metricsMeta({}, this.logger, 'imapResponses', 'inc', data);
|
|
1308
|
+
|
|
1309
|
+
// update byte counters as well
|
|
1310
|
+
let imapStats = imapClient.stats(true);
|
|
1311
|
+
|
|
1312
|
+
metricsMeta({}, this.logger, 'imapBytesSent', 'inc', imapStats.sent);
|
|
1313
|
+
metricsMeta({}, this.logger, 'imapBytesReceived', 'inc', imapStats.received);
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
// Handle connection close events
|
|
1317
|
+
imapClient.on('close', async () => {
|
|
1318
|
+
const wasDeleted = this.connections.delete(imapClient);
|
|
1319
|
+
|
|
1320
|
+
if (wasDeleted) {
|
|
1321
|
+
try {
|
|
1322
|
+
await this.redis.hSetExists(this.getAccountKey(), 'connections', this.connections.size.toString());
|
|
1323
|
+
} catch (err) {
|
|
1324
|
+
this.logger.error({ msg: 'Failed to update connection count in Redis', err });
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
imapClient?.log.info({ msg: 'Connection closed', type: 'imapClient', account: this.account, disabled: imapClient.disabled });
|
|
1328
|
+
|
|
1329
|
+
// Update state on unexpected disconnect
|
|
1330
|
+
if (['init', 'connecting', 'syncing', 'connected'].includes(this.state)) {
|
|
1331
|
+
this.state = 'disconnected';
|
|
1332
|
+
try {
|
|
1333
|
+
await this.setStateVal();
|
|
1334
|
+
await emitChangeEvent(this.logger, this.account, 'state', this.state);
|
|
1335
|
+
} catch (err) {
|
|
1336
|
+
this.logger.error({ msg: 'Failed to update state on connection close', account: this.account, err });
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
try {
|
|
1341
|
+
// Handle cleanup for all mailboxes
|
|
1342
|
+
for (let [, mailbox] of this.mailboxes) {
|
|
1343
|
+
if (mailbox.syncing) {
|
|
1344
|
+
try {
|
|
1345
|
+
// set failure flag
|
|
1346
|
+
await this.redis.hSetNew(
|
|
1347
|
+
this.getAccountKey(),
|
|
1348
|
+
'syncError',
|
|
1349
|
+
JSON.stringify({
|
|
1350
|
+
path: mailbox.path,
|
|
1351
|
+
time: new Date().toISOString(),
|
|
1352
|
+
error: {
|
|
1353
|
+
error: 'Connection closed unexpectedly'
|
|
1354
|
+
}
|
|
1355
|
+
})
|
|
1356
|
+
);
|
|
1357
|
+
} catch (err) {
|
|
1358
|
+
// ignore
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
if (mailbox.selected) {
|
|
1363
|
+
// should be at most one though
|
|
1364
|
+
await mailbox.onClose();
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
} catch (err) {
|
|
1368
|
+
imapClient.log.error({ msg: 'Connection close error', err });
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
try {
|
|
1372
|
+
// Attempt reconnection if this was an unexpected close
|
|
1373
|
+
if (!imapClient.disabled && imapClient === this.imapClient && !this._connecting) {
|
|
1374
|
+
imapClient.log.debug({ msg: 'Requesting reconnection due to unexpected close', type: 'imapClient', account: this.account });
|
|
1375
|
+
await this.reconnect();
|
|
1376
|
+
}
|
|
1377
|
+
} catch (err) {
|
|
1378
|
+
imapClient.log.error({ msg: 'Reconnection error', err });
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
imapClient = null;
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
try {
|
|
1385
|
+
// Establish connection and perform initial sync
|
|
1386
|
+
await this.connect();
|
|
1387
|
+
|
|
1388
|
+
// Check if this is the first successful connection
|
|
1389
|
+
let prevConnectedCount = await this.redis.hget(this.getAccountKey(), `state:count:connected`);
|
|
1390
|
+
let isFirstSuccessfulConnection = prevConnectedCount === '0'; // string zero means the account has been initialized but not yet connected
|
|
1391
|
+
|
|
1392
|
+
let isiInitial = !!isFirstSuccessfulConnection;
|
|
1393
|
+
|
|
1394
|
+
if (!isFirstSuccessfulConnection) {
|
|
1395
|
+
// check if the connection was previously in an errored state
|
|
1396
|
+
let prevLastErrorState = await this.redis.hget(this.getAccountKey(), 'lastErrorState');
|
|
1397
|
+
if (prevLastErrorState) {
|
|
1398
|
+
try {
|
|
1399
|
+
prevLastErrorState = JSON.parse(prevLastErrorState);
|
|
1400
|
+
} catch (err) {
|
|
1401
|
+
// ignore
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
if (prevLastErrorState && typeof prevLastErrorState === 'object' && Object.keys(prevLastErrorState).length) {
|
|
1406
|
+
// was previously errored
|
|
1407
|
+
isFirstSuccessfulConnection = true;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// Send appropriate notifications based on connection history
|
|
1412
|
+
if (isFirstSuccessfulConnection) {
|
|
1413
|
+
this.logger.info({ msg: 'Successful login without a previous active session', account: this.account, isiInitial, prevActive: false });
|
|
1414
|
+
await this.notify(false, AUTH_SUCCESS_NOTIFY, {
|
|
1415
|
+
user: imapConfig.auth.user
|
|
1416
|
+
});
|
|
1417
|
+
} else {
|
|
1418
|
+
this.logger.info({ msg: 'Successful login with a previous active session', account: this.account, isiInitial, prevActive: true });
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// Set up subconnections for monitoring multiple folders
|
|
1422
|
+
this.setupSubConnections()
|
|
1423
|
+
.then(result => {
|
|
1424
|
+
this.logger.info({ msg: 'Set up subconnections', account: this.account, result });
|
|
1425
|
+
})
|
|
1426
|
+
.catch(err => {
|
|
1427
|
+
this.logger.error({ msg: 'Failed to set up subconnections', account: this.account, err });
|
|
1428
|
+
});
|
|
1429
|
+
} catch (err) {
|
|
1430
|
+
// Handle various authentication and connection errors
|
|
1431
|
+
if (err.oauthError && err.oauthError.status === 'invalid_request') {
|
|
1432
|
+
// access token is invalid, clear it
|
|
1433
|
+
try {
|
|
1434
|
+
await this.accountObject.invalidateAccessToken();
|
|
1435
|
+
} catch (err) {
|
|
1436
|
+
this.logger.error({ msg: 'Failed to invalidate OAuth2 access token', account: this.account, err });
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
if (err.authenticationFailed) {
|
|
1441
|
+
this.logger.error({ msg: 'Failed to authenticate', account: this.account, err });
|
|
1442
|
+
await this.notify(false, AUTH_ERROR_NOTIFY, {
|
|
1443
|
+
response: err.response,
|
|
1444
|
+
serverResponseCode: err.serverResponseCode
|
|
1445
|
+
});
|
|
1446
|
+
this.state = 'authenticationError';
|
|
1447
|
+
} else {
|
|
1448
|
+
this.logger.error({ msg: 'Failed to connect', account: this.account, err });
|
|
1449
|
+
await this.notify(false, CONNECT_ERROR_NOTIFY, {
|
|
1450
|
+
response: err.response || err.message,
|
|
1451
|
+
serverResponseCode: err.serverResponseCode || err.code
|
|
1452
|
+
});
|
|
1453
|
+
this.state = 'connectError';
|
|
1454
|
+
}
|
|
1455
|
+
throw err;
|
|
1456
|
+
}
|
|
1457
|
+
} finally {
|
|
1458
|
+
// Update state if it changed during connection
|
|
1459
|
+
if (this.state !== initialState) {
|
|
1460
|
+
// update state
|
|
1461
|
+
try {
|
|
1462
|
+
let existingState = await this.redis.hget(this.getAccountKey(), 'state');
|
|
1463
|
+
if (existingState !== this.state) {
|
|
1464
|
+
await this.setStateVal();
|
|
1465
|
+
await emitChangeEvent(this.logger, this.account, 'state', this.state);
|
|
1466
|
+
}
|
|
1467
|
+
} catch (err) {
|
|
1468
|
+
// ignore
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* Initializes the IMAP client and starts the connection
|
|
1476
|
+
*/
|
|
1477
|
+
async init() {
|
|
1478
|
+
await this.reconnect();
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* Deletes the account and cleans up all resources
|
|
1483
|
+
* This permanently removes all cached data
|
|
1484
|
+
*/
|
|
1485
|
+
async delete() {
|
|
1486
|
+
if (this.isClosed || this.isClosing) {
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
this.isClosing = true;
|
|
1490
|
+
|
|
1491
|
+
if (this.imapClient) {
|
|
1492
|
+
this.imapClient.disabled = true;
|
|
1493
|
+
this.imapClient.close();
|
|
1494
|
+
if (this.commandClient) {
|
|
1495
|
+
this.commandClient.close();
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// Clear all timers
|
|
1500
|
+
clearTimeout(this.refreshListingTimer);
|
|
1501
|
+
clearTimeout(this.untaggedExpungeTimer);
|
|
1502
|
+
clearTimeout(this.resyncTimer);
|
|
1503
|
+
clearTimeout(this.completedTimer);
|
|
1504
|
+
|
|
1505
|
+
try {
|
|
1506
|
+
// Clean up all mailboxes
|
|
1507
|
+
for (let [, mailbox] of this.mailboxes) {
|
|
1508
|
+
if (mailbox.selected) {
|
|
1509
|
+
await mailbox.onClose();
|
|
1510
|
+
}
|
|
1511
|
+
await mailbox.clear({ skipNotify: true });
|
|
1512
|
+
mailbox = false;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// Remove mailbox listing from Redis
|
|
1516
|
+
await this.redis.del(this.getMailboxListKey());
|
|
1517
|
+
} finally {
|
|
1518
|
+
this.isClosing = false;
|
|
1519
|
+
this.isClosed = true;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
this.logger.info({ msg: 'Closed account', account: this.account });
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/**
|
|
1526
|
+
* Closes the IMAP connection without deleting account data
|
|
1527
|
+
* Can be reopened later
|
|
1528
|
+
*/
|
|
1529
|
+
close() {
|
|
1530
|
+
if (this.isClosed || this.isClosing) {
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
this.isClosing = true;
|
|
1534
|
+
|
|
1535
|
+
if (this.imapClient) {
|
|
1536
|
+
this.imapClient.close();
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
if (this.commandClient) {
|
|
1540
|
+
this.commandClient.close();
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
clearTimeout(this.refreshListingTimer);
|
|
1544
|
+
clearTimeout(this.untaggedExpungeTimer);
|
|
1545
|
+
clearTimeout(this.resyncTimer);
|
|
1546
|
+
clearTimeout(this.completedTimer);
|
|
1547
|
+
|
|
1548
|
+
this.isClosing = false;
|
|
1549
|
+
this.isClosed = true;
|
|
1550
|
+
|
|
1551
|
+
this.closeSubconnections();
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* Checks if the IMAP connection is active and usable
|
|
1556
|
+
* @returns {boolean} Connection status
|
|
1557
|
+
*/
|
|
1558
|
+
isConnected() {
|
|
1559
|
+
return this.imapClient && this.imapClient.usable && !this.isClosing && !this.isClosed;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
/**
|
|
1563
|
+
* Gets the current connection state
|
|
1564
|
+
* @returns {string} Current state
|
|
1565
|
+
*/
|
|
1566
|
+
async currentState() {
|
|
1567
|
+
if (this.state === 'connected' && !this.isConnected()) {
|
|
1568
|
+
this.state = 'disconnected';
|
|
1569
|
+
}
|
|
1570
|
+
return this.state;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
/**
|
|
1574
|
+
* Validates that IMAP connection is available
|
|
1575
|
+
* @param {Object} connectionOptions - Connection options
|
|
1576
|
+
* @throws {Error} If connection is not available
|
|
1577
|
+
*/
|
|
1578
|
+
checkIMAPConnection(connectionOptions) {
|
|
1579
|
+
connectionOptions = connectionOptions || {};
|
|
1580
|
+
|
|
1581
|
+
if (
|
|
1582
|
+
!this.isConnected() &&
|
|
1583
|
+
!connectionOptions.noPool &&
|
|
1584
|
+
!connectionOptions.allowSecondary &&
|
|
1585
|
+
(!connectionOptions.connectionClient || !connectionOptions.connectionClient.usable)
|
|
1586
|
+
) {
|
|
1587
|
+
let err = new Error('IMAP connection is currently not available for requested account');
|
|
1588
|
+
err.code = 'IMAPUnavailable';
|
|
1589
|
+
err.statusCode = 503;
|
|
1590
|
+
throw err;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// Mailbox level user methods
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* Fetch message text from IMAP. Resulting value is a unicode string.
|
|
1598
|
+
*
|
|
1599
|
+
* @param {string} textId ID of the text content
|
|
1600
|
+
* @param {object} [options] Options object
|
|
1601
|
+
* @param {number} [options.maxLength] If set then limits output stream to specified chars (NB! not bytes but unicode characters). Limit applies to each text type separately, so 1000 would mean you'd get a 1000 char string for plaintext and 1000 char string for html.
|
|
1602
|
+
* @param {string} [options.contentType] If set then limits output for selected type only
|
|
1603
|
+
* @returns {Object} Text object, where key is text type (either 'plain' or 'html') and value is a unicode string
|
|
1604
|
+
*/
|
|
1605
|
+
async getText(textId, options, connectionOptions) {
|
|
1606
|
+
options = options || {};
|
|
1607
|
+
connectionOptions = connectionOptions || { allowSecondary: true };
|
|
1608
|
+
|
|
1609
|
+
this.checkIMAPConnection(connectionOptions);
|
|
1610
|
+
|
|
1611
|
+
let { message, textParts } = await this.getMessageTextPaths(textId);
|
|
1612
|
+
if (!message || !textParts || !textParts.length) {
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
if (!this.mailboxes.has(normalizePath(message.path))) {
|
|
1617
|
+
return false; //?
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
let mailbox = this.mailboxes.get(normalizePath(message.path));
|
|
1621
|
+
|
|
1622
|
+
let textType = (options.textType || '').toLowerCase().trim();
|
|
1623
|
+
|
|
1624
|
+
// Process text parts based on requested type
|
|
1625
|
+
if (Array.isArray(textParts)) {
|
|
1626
|
+
let re = /^\d+(\.\d+)*$/;
|
|
1627
|
+
switch (textType) {
|
|
1628
|
+
case 'plain':
|
|
1629
|
+
textParts = Array.isArray(textParts[0]) ? textParts[0].filter(entry => re.test(entry)) : false;
|
|
1630
|
+
break;
|
|
1631
|
+
case 'html':
|
|
1632
|
+
textParts = Array.isArray(textParts[1]) ? textParts[1].filter(entry => re.test(entry)) : false;
|
|
1633
|
+
break;
|
|
1634
|
+
default:
|
|
1635
|
+
textParts = textParts.flatMap(part => part).filter(entry => re.test(entry));
|
|
1636
|
+
break;
|
|
1637
|
+
}
|
|
1638
|
+
} else {
|
|
1639
|
+
textParts = [];
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
let result = await mailbox.getText(message, textParts, options, connectionOptions);
|
|
1643
|
+
|
|
1644
|
+
// Filter result based on requested type
|
|
1645
|
+
if (textType && textType !== '*') {
|
|
1646
|
+
result = {
|
|
1647
|
+
[textType]: result[textType] || '',
|
|
1648
|
+
hasMore: result.hasMore
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
return result;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/**
|
|
1656
|
+
* Retrieves a complete message by ID
|
|
1657
|
+
* @param {string} id - Message ID
|
|
1658
|
+
* @param {Object} options - Retrieval options
|
|
1659
|
+
* @param {Object} connectionOptions - Connection options
|
|
1660
|
+
* @returns {Object|false} Message object or false if not found
|
|
1661
|
+
*/
|
|
1662
|
+
async getMessage(id, options, connectionOptions) {
|
|
1663
|
+
options = options || {};
|
|
1664
|
+
connectionOptions = connectionOptions || { allowSecondary: true };
|
|
1665
|
+
|
|
1666
|
+
this.checkIMAPConnection(connectionOptions);
|
|
1667
|
+
|
|
1668
|
+
let buf = Buffer.from(id, 'base64url');
|
|
1669
|
+
let message = await this.unpackUid(buf.subarray(0, 8));
|
|
1670
|
+
if (!message) {
|
|
1671
|
+
return false;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
if (!this.mailboxes.has(normalizePath(message.path))) {
|
|
1675
|
+
return false; //?
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
let mailbox = this.mailboxes.get(normalizePath(message.path));
|
|
1679
|
+
|
|
1680
|
+
return await mailbox.getMessage(message, options, connectionOptions);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
/**
|
|
1684
|
+
* Updates message flags/labels
|
|
1685
|
+
* @param {string} id - Message ID
|
|
1686
|
+
* @param {Object} updates - Update operations (e.g., flags to add/remove)
|
|
1687
|
+
* @param {Object} connectionOptions - Connection options
|
|
1688
|
+
* @returns {Object|false} Update result or false if failed
|
|
1689
|
+
*/
|
|
1690
|
+
async updateMessage(id, updates, connectionOptions) {
|
|
1691
|
+
updates = updates || {};
|
|
1692
|
+
connectionOptions = connectionOptions || { allowSecondary: true };
|
|
1693
|
+
|
|
1694
|
+
this.checkIMAPConnection(connectionOptions);
|
|
1695
|
+
|
|
1696
|
+
let buf = Buffer.from(id, 'base64url');
|
|
1697
|
+
let message = await this.unpackUid(buf.subarray(0, 8));
|
|
1698
|
+
if (!message) {
|
|
1699
|
+
return false;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
if (!this.mailboxes.has(normalizePath(message.path))) {
|
|
1703
|
+
return false; //?
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
let mailbox = this.mailboxes.get(normalizePath(message.path));
|
|
1707
|
+
|
|
1708
|
+
return await mailbox.updateMessage(message, updates, connectionOptions);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/**
|
|
1712
|
+
* Updates multiple messages matching search criteria
|
|
1713
|
+
* @param {string} path - Mailbox path
|
|
1714
|
+
* @param {Object} search - Search criteria
|
|
1715
|
+
* @param {Object} updates - Update operations
|
|
1716
|
+
* @param {Object} connectionOptions - Connection options
|
|
1717
|
+
* @returns {Object|false} Update result or false if failed
|
|
1718
|
+
*/
|
|
1719
|
+
async updateMessages(path, search, updates, connectionOptions) {
|
|
1720
|
+
updates = updates || {};
|
|
1721
|
+
connectionOptions = connectionOptions || { allowSecondary: true };
|
|
1722
|
+
|
|
1723
|
+
this.checkIMAPConnection(connectionOptions);
|
|
1724
|
+
|
|
1725
|
+
if (!this.mailboxes.has(normalizePath(path))) {
|
|
1726
|
+
return false; //?
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
let mailbox = this.mailboxes.get(normalizePath(path));
|
|
1730
|
+
|
|
1731
|
+
return await mailbox.updateMessages(search, updates, connectionOptions);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
/**
|
|
1735
|
+
* Lists all mailboxes/folders
|
|
1736
|
+
* @param {Object} options - Listing options
|
|
1737
|
+
* @param {Object} connectionOptions - Connection options
|
|
1738
|
+
* @returns {Array} Array of mailbox objects
|
|
1739
|
+
*/
|
|
1740
|
+
async listMailboxes(options, connectionOptions) {
|
|
1741
|
+
connectionOptions = connectionOptions || { allowSecondary: true };
|
|
1742
|
+
|
|
1743
|
+
this.checkIMAPConnection(connectionOptions);
|
|
1744
|
+
|
|
1745
|
+
return await this.getCurrentListing(options, connectionOptions);
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* Moves a message to another mailbox
|
|
1750
|
+
* @param {string} id - Message ID
|
|
1751
|
+
* @param {Object} target - Target mailbox info
|
|
1752
|
+
* @param {Object} options - Move options
|
|
1753
|
+
* @param {Object} connectionOptions - Connection options
|
|
1754
|
+
* @returns {Object|false} Move result or false if failed
|
|
1755
|
+
*/
|
|
1756
|
+
async moveMessage(id, target, options, connectionOptions) {
|
|
1757
|
+
target = target || {};
|
|
1758
|
+
connectionOptions = connectionOptions || { allowSecondary: true };
|
|
1759
|
+
|
|
1760
|
+
this.checkIMAPConnection(connectionOptions);
|
|
1761
|
+
|
|
1762
|
+
let buf = Buffer.from(id, 'base64url');
|
|
1763
|
+
let message = await this.unpackUid(buf.subarray(0, 8));
|
|
1764
|
+
|
|
1765
|
+
if (!message) {
|
|
1766
|
+
return false;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
if (!this.mailboxes.has(normalizePath(message.path))) {
|
|
1770
|
+
return false; //?
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
let mailbox = this.mailboxes.get(normalizePath(message.path));
|
|
1774
|
+
return await mailbox.moveMessage(message, target, options, connectionOptions);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
/**
|
|
1778
|
+
* Moves multiple messages to another mailbox
|
|
1779
|
+
* @param {string} source - Source mailbox path
|
|
1780
|
+
* @param {Object} search - Search criteria
|
|
1781
|
+
* @param {Object} target - Target mailbox info
|
|
1782
|
+
* @param {Object} connectionOptions - Connection options
|
|
1783
|
+
* @returns {Object|false} Move result or false if failed
|
|
1784
|
+
*/
|
|
1785
|
+
async moveMessages(source, search, target, connectionOptions) {
|
|
1786
|
+
target = target || {};
|
|
1787
|
+
connectionOptions = connectionOptions || { allowSecondary: true };
|
|
1788
|
+
|
|
1789
|
+
this.checkIMAPConnection(connectionOptions);
|
|
1790
|
+
|
|
1791
|
+
if (!this.mailboxes.has(normalizePath(source))) {
|
|
1792
|
+
return false; //?
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
let mailbox = this.mailboxes.get(normalizePath(source));
|
|
1796
|
+
|
|
1797
|
+
let res = await mailbox.moveMessages(search, target, connectionOptions);
|
|
1798
|
+
|
|
1799
|
+
// force sync target mailbox to show moved messages
|
|
1800
|
+
let targetMailbox = this.mailboxes.get(normalizePath(target.path));
|
|
1801
|
+
if (targetMailbox) {
|
|
1802
|
+
targetMailbox.sync().catch(err => this.logger.error({ msg: 'Mailbox sync error', path: target.path, err }));
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
return res;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
/**
|
|
1809
|
+
* Deletes a message
|
|
1810
|
+
* @param {string} id - Message ID
|
|
1811
|
+
* @param {boolean} force - Force permanent deletion
|
|
1812
|
+
* @param {Object} connectionOptions - Connection options
|
|
1813
|
+
* @returns {Object|false} Delete result or false if failed
|
|
1814
|
+
*/
|
|
1815
|
+
async deleteMessage(id, force, connectionOptions) {
|
|
1816
|
+
connectionOptions = connectionOptions || { allowSecondary: true };
|
|
1817
|
+
|
|
1818
|
+
this.checkIMAPConnection(connectionOptions);
|
|
1819
|
+
|
|
1820
|
+
let buf = Buffer.from(id, 'base64url');
|
|
1821
|
+
let message = await this.unpackUid(buf.subarray(0, 8));
|
|
1822
|
+
if (!message) {
|
|
1823
|
+
return false;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
if (!this.mailboxes.has(normalizePath(message.path))) {
|
|
1827
|
+
return false; //?
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
let mailbox = this.mailboxes.get(normalizePath(message.path));
|
|
1831
|
+
|
|
1832
|
+
return await mailbox.deleteMessage(message, force, connectionOptions);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* Deletes multiple messages matching search criteria
|
|
1837
|
+
* @param {string} path - Mailbox path
|
|
1838
|
+
* @param {Object} search - Search criteria
|
|
1839
|
+
* @param {boolean} force - Force permanent deletion
|
|
1840
|
+
* @param {Object} connectionOptions - Connection options
|
|
1841
|
+
* @returns {Object|false} Delete result or false if failed
|
|
1842
|
+
*/
|
|
1843
|
+
async deleteMessages(path, search, force, connectionOptions) {
|
|
1844
|
+
connectionOptions = connectionOptions || { allowSecondary: true };
|
|
1845
|
+
|
|
1846
|
+
this.checkIMAPConnection(connectionOptions);
|
|
1847
|
+
|
|
1848
|
+
if (!this.mailboxes.has(normalizePath(path))) {
|
|
1849
|
+
return false; //?
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
let mailbox = this.mailboxes.get(normalizePath(path));
|
|
1853
|
+
let res = await mailbox.deleteMessages(search, force, connectionOptions);
|
|
1854
|
+
|
|
1855
|
+
// force sync target mailbox if messages were moved to trash
|
|
1856
|
+
if (res && res.moved && res.moved.destination) {
|
|
1857
|
+
let targetMailbox = this.mailboxes.get(normalizePath(res.moved.destination));
|
|
1858
|
+
if (targetMailbox) {
|
|
1859
|
+
targetMailbox.sync().catch(err => this.logger.error({ msg: 'Mailbox sync error', path: res && res.moved && res.moved.destination, err }));
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
return res;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
/**
|
|
1867
|
+
* Downloads an attachment from IMAP as a binary stream
|
|
1868
|
+
*
|
|
1869
|
+
* @param {string} attachmentId ID of the attachment
|
|
1870
|
+
* @param {object} [options] Options object
|
|
1871
|
+
* @param {number} [options.maxLength] If set then limits output stream to specified bytes
|
|
1872
|
+
* @returns {Boolean|Stream} Attachment stream or `false` if not found
|
|
1873
|
+
*/
|
|
1874
|
+
async getAttachment(attachmentId, options, connectionOptions) {
|
|
1875
|
+
options = Object.assign(
|
|
1876
|
+
{
|
|
1877
|
+
chunkSize: DOWNLOAD_CHUNK_SIZE
|
|
1878
|
+
},
|
|
1879
|
+
options || {}
|
|
1880
|
+
);
|
|
1881
|
+
connectionOptions = connectionOptions || { allowSecondary: true };
|
|
1882
|
+
|
|
1883
|
+
this.checkIMAPConnection(connectionOptions);
|
|
1884
|
+
|
|
1885
|
+
// Unpack attachment ID to get message location and part number
|
|
1886
|
+
let buf = Buffer.from(attachmentId, 'base64url');
|
|
1887
|
+
let id = buf.subarray(0, 8);
|
|
1888
|
+
let part = buf.subarray(8).toString();
|
|
1889
|
+
|
|
1890
|
+
let message = await this.unpackUid(id);
|
|
1891
|
+
if (!message) {
|
|
1892
|
+
return false;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
if (!this.mailboxes.has(normalizePath(message.path))) {
|
|
1896
|
+
return false; //?
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
let mailbox = this.mailboxes.get(normalizePath(message.path));
|
|
1900
|
+
|
|
1901
|
+
return mailbox.getAttachment(message, part, options, connectionOptions);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
/**
|
|
1905
|
+
* Downloads attachment content as a buffer
|
|
1906
|
+
* @param {string} attachmentId - Attachment ID
|
|
1907
|
+
* @param {Object} options - Download options
|
|
1908
|
+
* @param {Object} connectionOptions - Connection options
|
|
1909
|
+
* @returns {Buffer|false} Attachment content or false if not found
|
|
1910
|
+
*/
|
|
1911
|
+
async getAttachmentContent(attachmentId, options, connectionOptions) {
|
|
1912
|
+
let stream = await this.getAttachment(attachmentId, options, connectionOptions);
|
|
1913
|
+
if (!stream) {
|
|
1914
|
+
return false;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
return new Promise((resolve, reject) => {
|
|
1918
|
+
let chunks = [];
|
|
1919
|
+
let chunklen = 0;
|
|
1920
|
+
stream.on('error', reject);
|
|
1921
|
+
stream.on('readable', () => {
|
|
1922
|
+
let chunk;
|
|
1923
|
+
while ((chunk = stream.read()) !== null) {
|
|
1924
|
+
chunks.push(chunk);
|
|
1925
|
+
chunklen += chunk.length;
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
stream.on('end', () => resolve(Buffer.concat(chunks, chunklen)));
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
/**
|
|
1933
|
+
* Downloads raw message from IMAP as a binary stream
|
|
1934
|
+
*
|
|
1935
|
+
* @param {string} id ID of the message
|
|
1936
|
+
* @param {object} [options] Options object
|
|
1937
|
+
* @param {number} [options.maxLength] If set then limits output stream to specified bytes
|
|
1938
|
+
* @returns {Boolean|Stream} Attachment stream or `false` if not found
|
|
1939
|
+
*/
|
|
1940
|
+
async getRawMessage(id, options, connectionOptions) {
|
|
1941
|
+
options = Object.assign(
|
|
1942
|
+
{
|
|
1943
|
+
chunkSize: DOWNLOAD_CHUNK_SIZE
|
|
1944
|
+
},
|
|
1945
|
+
options || {}
|
|
1946
|
+
);
|
|
1947
|
+
connectionOptions = connectionOptions || { allowSecondary: true };
|
|
1948
|
+
|
|
1949
|
+
this.checkIMAPConnection(connectionOptions);
|
|
1950
|
+
|
|
1951
|
+
let buf = Buffer.from(id, 'base64url');
|
|
1952
|
+
let message = await this.unpackUid(buf.subarray(0, 8));
|
|
1953
|
+
if (!message) {
|
|
1954
|
+
return false;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
if (!this.mailboxes.has(normalizePath(message.path))) {
|
|
1958
|
+
return false; //?
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
let mailbox = this.mailboxes.get(normalizePath(message.path));
|
|
1962
|
+
|
|
1963
|
+
// Use false as part to get the entire message
|
|
1964
|
+
return mailbox.getAttachment(message, false, options, connectionOptions);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
/**
|
|
1968
|
+
* Lists messages in a mailbox with pagination and search
|
|
1969
|
+
* @param {Object} options - Listing options including path, search criteria, pagination
|
|
1970
|
+
* @param {Object} connectionOptions - Connection options
|
|
1971
|
+
* @returns {Object|false} Message listing or false if failed
|
|
1972
|
+
*/
|
|
1973
|
+
async listMessages(options, connectionOptions) {
|
|
1974
|
+
options = options || {};
|
|
1975
|
+
connectionOptions = connectionOptions || { allowSecondary: true };
|
|
1976
|
+
|
|
1977
|
+
this.checkIMAPConnection(connectionOptions);
|
|
1978
|
+
|
|
1979
|
+
let path = normalizePath(options.path);
|
|
1980
|
+
|
|
1981
|
+
// Handle special-use folder aliases
|
|
1982
|
+
if (['\\Junk', '\\Sent', '\\Trash', '\\Inbox', '\\Drafts', '\\All'].includes(path)) {
|
|
1983
|
+
let resolvedPath = await this.getSpecialUseMailbox(path);
|
|
1984
|
+
if (resolvedPath) {
|
|
1985
|
+
path = resolvedPath.path;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
if (!this.mailboxes.has(path)) {
|
|
1990
|
+
return false; //?
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
let mailbox = this.mailboxes.get(path);
|
|
1994
|
+
|
|
1995
|
+
let listing = await mailbox.listMessages(options, connectionOptions);
|
|
1996
|
+
return listing;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
/**
|
|
2000
|
+
* Deletes a mailbox/folder
|
|
2001
|
+
* @param {string} path - Mailbox path to delete
|
|
2002
|
+
* @param {Object} connectionOptions - Connection options
|
|
2003
|
+
* @returns {Object} Delete result
|
|
2004
|
+
*/
|
|
2005
|
+
async deleteMailbox(path, connectionOptions) {
|
|
2006
|
+
connectionOptions = connectionOptions || { allowSecondary: true };
|
|
2007
|
+
|
|
2008
|
+
this.checkIMAPConnection(connectionOptions);
|
|
2009
|
+
|
|
2010
|
+
const connectionClient = await this.getImapConnection(connectionOptions, 'deleteMailbox');
|
|
2011
|
+
|
|
2012
|
+
let result = {
|
|
2013
|
+
path,
|
|
2014
|
+
deleted: false // set to true if mailbox is actually deleted
|
|
2015
|
+
};
|
|
2016
|
+
|
|
2017
|
+
try {
|
|
2018
|
+
// Acquire lock to prevent concurrent operations on the mailbox
|
|
2019
|
+
let lock = await connectionClient.getMailboxLock(path, { description: `Delete mailbox ${path}` });
|
|
2020
|
+
|
|
2021
|
+
try {
|
|
2022
|
+
await connectionClient.mailboxClose();
|
|
2023
|
+
try {
|
|
2024
|
+
await connectionClient.mailboxDelete(path);
|
|
2025
|
+
result.deleted = true;
|
|
2026
|
+
} catch (err) {
|
|
2027
|
+
// kind of ignore
|
|
2028
|
+
}
|
|
2029
|
+
} finally {
|
|
2030
|
+
lock.release();
|
|
2031
|
+
}
|
|
2032
|
+
} catch (err) {
|
|
2033
|
+
this.logger.debug({ msg: 'Mailbox select error', path, err });
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// Clean up local cache
|
|
2037
|
+
if (this.mailboxes.has(normalizePath(path))) {
|
|
2038
|
+
let mailbox = this.mailboxes.get(normalizePath(path));
|
|
2039
|
+
await mailbox.clear();
|
|
2040
|
+
mailbox = false;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
return result;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
/**
|
|
2047
|
+
* Refreshes mailbox listing after account configuration changes
|
|
2048
|
+
* @param {Object} accountData - Account data with path configuration
|
|
2049
|
+
*/
|
|
2050
|
+
runPostListing(accountData) {
|
|
2051
|
+
const accountPaths = [].concat(accountData.path || '*');
|
|
2052
|
+
if (!accountPaths.length) {
|
|
2053
|
+
accountPaths.push('*');
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
this.getCurrentListing()
|
|
2057
|
+
.then(listing => {
|
|
2058
|
+
let syncNeeded = new Set();
|
|
2059
|
+
for (let entry of listing) {
|
|
2060
|
+
if (
|
|
2061
|
+
// previously unseen
|
|
2062
|
+
!this.mailboxes.has(normalizePath(entry.path))
|
|
2063
|
+
) {
|
|
2064
|
+
// Apply filtering based on account configuration
|
|
2065
|
+
if (!accountPaths.includes('*')) {
|
|
2066
|
+
if (!accountPaths.includes(entry.path) && !accountPaths.includes(entry.specialUse)) {
|
|
2067
|
+
// ignore changes
|
|
2068
|
+
entry.syncDisabled = true;
|
|
2069
|
+
}
|
|
2070
|
+
} else if (this.isGmail && !['\\All', '\\Junk', '\\Trash'].includes(entry.specialUse)) {
|
|
2071
|
+
// do not look for changes from this folder
|
|
2072
|
+
entry.syncDisabled = true;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
let mailbox = new Mailbox(this, entry);
|
|
2076
|
+
this.mailboxes.set(normalizePath(entry.path), mailbox);
|
|
2077
|
+
syncNeeded.add(mailbox);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
let runSyncs = async () => {
|
|
2082
|
+
// sync new mailboxes
|
|
2083
|
+
for (let mailbox of syncNeeded) {
|
|
2084
|
+
await mailbox.sync(true);
|
|
2085
|
+
}
|
|
2086
|
+
};
|
|
2087
|
+
|
|
2088
|
+
return runSyncs();
|
|
2089
|
+
})
|
|
2090
|
+
.catch(err => {
|
|
2091
|
+
this.logger.error({ msg: 'List refresh error', err });
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
/**
|
|
2096
|
+
* Gets IMAP quota information
|
|
2097
|
+
* @param {Object} connectionOptions - Connection options
|
|
2098
|
+
* @returns {Object|false} Quota info or false if not supported
|
|
2099
|
+
*/
|
|
2100
|
+
async getQuota(connectionOptions) {
|
|
2101
|
+
connectionOptions = connectionOptions || { allowSecondary: true };
|
|
2102
|
+
|
|
2103
|
+
this.checkIMAPConnection(connectionOptions);
|
|
2104
|
+
|
|
2105
|
+
const connectionClient = await this.getImapConnection(connectionOptions, 'getQuota');
|
|
2106
|
+
|
|
2107
|
+
try {
|
|
2108
|
+
let result = await connectionClient.getQuota();
|
|
2109
|
+
return (result && result.storage) || false;
|
|
2110
|
+
} catch (err) {
|
|
2111
|
+
if (err.serverResponseCode) {
|
|
2112
|
+
let error = new Error('Quota request failed');
|
|
2113
|
+
error.info = {
|
|
2114
|
+
response: err.response && typeof err.response === 'string' && err.response.replace(/^[^\s]*\s*/, '')
|
|
2115
|
+
};
|
|
2116
|
+
error.code = err.serverResponseCode;
|
|
2117
|
+
error.statusCode = 400;
|
|
2118
|
+
throw error;
|
|
2119
|
+
} else if (err.responseStatus === 'NO') {
|
|
2120
|
+
return false;
|
|
2121
|
+
} else {
|
|
2122
|
+
throw err;
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
/**
|
|
2128
|
+
* Creates a new mailbox/folder
|
|
2129
|
+
* @param {string} path - Mailbox path to create
|
|
2130
|
+
* @param {Object} connectionOptions - Connection options
|
|
2131
|
+
* @returns {Object} Create result
|
|
2132
|
+
*/
|
|
2133
|
+
async createMailbox(path, connectionOptions) {
|
|
2134
|
+
connectionOptions = connectionOptions || { allowSecondary: true };
|
|
2135
|
+
|
|
2136
|
+
this.checkIMAPConnection(connectionOptions);
|
|
2137
|
+
|
|
2138
|
+
const connectionClient = await this.getImapConnection(connectionOptions, 'createMailbox');
|
|
2139
|
+
|
|
2140
|
+
try {
|
|
2141
|
+
let result = await connectionClient.mailboxCreate(path);
|
|
2142
|
+
if (result) {
|
|
2143
|
+
result.created = !!result.created;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// Refresh listing to include new mailbox
|
|
2147
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
2148
|
+
setImmediate(() => this.runPostListing(accountData));
|
|
2149
|
+
|
|
2150
|
+
return result;
|
|
2151
|
+
} catch (err) {
|
|
2152
|
+
if (err.serverResponseCode) {
|
|
2153
|
+
let error = new Error('Create failed');
|
|
2154
|
+
error.info = {
|
|
2155
|
+
response: err.response && typeof err.response === 'string' && err.response.replace(/^[^\s]*\s*/, '')
|
|
2156
|
+
};
|
|
2157
|
+
error.code = err.serverResponseCode;
|
|
2158
|
+
error.statusCode = 400;
|
|
2159
|
+
throw error;
|
|
2160
|
+
} else if (err.responseStatus === 'NO') {
|
|
2161
|
+
return {
|
|
2162
|
+
path,
|
|
2163
|
+
created: false
|
|
2164
|
+
};
|
|
2165
|
+
} else {
|
|
2166
|
+
throw err;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
/**
|
|
2172
|
+
* Modifies a mailbox (rename and/or change subscription status)
|
|
2173
|
+
* @param {string} path - Current mailbox path
|
|
2174
|
+
* @param {string|Array<string>} newPath - New mailbox path (optional)
|
|
2175
|
+
* @param {boolean} subscribed - Subscription status (optional)
|
|
2176
|
+
* @param {Object} connectionOptions - Connection options
|
|
2177
|
+
* @returns {Object} Modify result
|
|
2178
|
+
*/
|
|
2179
|
+
async modifyMailbox(path, newPath, subscribed, connectionOptions) {
|
|
2180
|
+
connectionOptions = connectionOptions || { allowSecondary: true };
|
|
2181
|
+
|
|
2182
|
+
this.checkIMAPConnection(connectionOptions);
|
|
2183
|
+
|
|
2184
|
+
const connectionClient = await this.getImapConnection(connectionOptions, 'modifyMailbox');
|
|
2185
|
+
|
|
2186
|
+
let result = {};
|
|
2187
|
+
|
|
2188
|
+
// Handle rename if newPath is provided and different from path
|
|
2189
|
+
if (newPath) {
|
|
2190
|
+
let normalizedNewPath = [].concat(newPath || []).join('/');
|
|
2191
|
+
let normalizedPath = [].concat(path || []).join('/');
|
|
2192
|
+
|
|
2193
|
+
if (normalizedNewPath !== normalizedPath) {
|
|
2194
|
+
try {
|
|
2195
|
+
let renameResult = await connectionClient.mailboxRename(path, newPath);
|
|
2196
|
+
if (renameResult) {
|
|
2197
|
+
result.path = renameResult.path;
|
|
2198
|
+
result.newPath = renameResult.newPath;
|
|
2199
|
+
result.renamed = !!renameResult.newPath;
|
|
2200
|
+
path = renameResult.newPath;
|
|
2201
|
+
|
|
2202
|
+
// Subscribe to renamed mailbox if not explicitly managing subscription
|
|
2203
|
+
if (subscribed === undefined) {
|
|
2204
|
+
try {
|
|
2205
|
+
await connectionClient.mailboxSubscribe(renameResult.newPath);
|
|
2206
|
+
} catch (err) {
|
|
2207
|
+
this.logger.debug({ msg: 'Failed to subscribe mailbox', path: renameResult.newPath, err });
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
} catch (err) {
|
|
2212
|
+
if (err.serverResponseCode && err.serverResponseCode !== 'ALREADYEXISTS') {
|
|
2213
|
+
let error = new Error('Rename failed');
|
|
2214
|
+
error.info = {
|
|
2215
|
+
response: err.response
|
|
2216
|
+
};
|
|
2217
|
+
error.code = err.serverResponseCode;
|
|
2218
|
+
error.statusCode = 400;
|
|
2219
|
+
throw error;
|
|
2220
|
+
} else if (err.responseStatus === 'NO') {
|
|
2221
|
+
let error = new Error('Can not rename mailbox');
|
|
2222
|
+
error.info = {
|
|
2223
|
+
response: err.response && typeof err.response === 'string' && err.response.replace(/^[^\s]*\s*/, '')
|
|
2224
|
+
};
|
|
2225
|
+
error.code = err.serverResponseCode;
|
|
2226
|
+
error.statusCode = 400;
|
|
2227
|
+
throw error;
|
|
2228
|
+
} else {
|
|
2229
|
+
throw err;
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
} else {
|
|
2233
|
+
result.path = normalizedPath;
|
|
2234
|
+
result.renamed = false;
|
|
2235
|
+
}
|
|
2236
|
+
} else {
|
|
2237
|
+
result.path = [].concat(path || []).join('/');
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
// Handle subscription change if explicitly specified
|
|
2241
|
+
if (subscribed !== undefined) {
|
|
2242
|
+
try {
|
|
2243
|
+
if (subscribed) {
|
|
2244
|
+
await connectionClient.mailboxSubscribe(path);
|
|
2245
|
+
} else {
|
|
2246
|
+
await connectionClient.mailboxUnsubscribe(path);
|
|
2247
|
+
}
|
|
2248
|
+
result.subscribed = subscribed;
|
|
2249
|
+
} catch (err) {
|
|
2250
|
+
this.logger.debug({ msg: 'Failed to change subscription status', path, subscribed, err });
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
// Refresh listing to update mailbox paths
|
|
2255
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
2256
|
+
setImmediate(() => this.runPostListing(accountData));
|
|
2257
|
+
|
|
2258
|
+
return result;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
/**
|
|
2262
|
+
* Gets a mailbox by its special-use flag
|
|
2263
|
+
* @param {string} specialUse - Special-use flag (e.g., '\\Sent', '\\Drafts')
|
|
2264
|
+
* @returns {Object|undefined} Mailbox info if found
|
|
2265
|
+
*/
|
|
2266
|
+
async getSpecialUseMailbox(specialUse) {
|
|
2267
|
+
let storedListing = await this.redis.hgetallBuffer(this.getMailboxListKey());
|
|
2268
|
+
return Object.keys(storedListing || {})
|
|
2269
|
+
.map(path => {
|
|
2270
|
+
try {
|
|
2271
|
+
return msgpack.decode(storedListing[path]);
|
|
2272
|
+
} catch (err) {
|
|
2273
|
+
// should not happen
|
|
2274
|
+
}
|
|
2275
|
+
return false;
|
|
2276
|
+
})
|
|
2277
|
+
.filter(entry => entry)
|
|
2278
|
+
.find(entry => entry.specialUse === specialUse);
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
/**
|
|
2282
|
+
* Uploads a message to a mailbox
|
|
2283
|
+
* @param {Object} data - Message data including path, flags, content
|
|
2284
|
+
* @param {Object} connectionOptions - Connection options
|
|
2285
|
+
* @returns {Object} Upload result with message ID
|
|
2286
|
+
*/
|
|
2287
|
+
async uploadMessage(data, connectionOptions) {
|
|
2288
|
+
connectionOptions = connectionOptions || { allowSecondary: true };
|
|
2289
|
+
|
|
2290
|
+
this.checkIMAPConnection(connectionOptions);
|
|
2291
|
+
|
|
2292
|
+
const connectionClient = await this.getImapConnection(connectionOptions, 'uploadMessage');
|
|
2293
|
+
|
|
2294
|
+
// Prepare raw message for upload
|
|
2295
|
+
let { raw, messageId, documentStoreUsed, referencedMessage } = await this.prepareRawMessage(data, null, { connectionClient });
|
|
2296
|
+
|
|
2297
|
+
// Upload message to selected folder
|
|
2298
|
+
try {
|
|
2299
|
+
let response = {};
|
|
2300
|
+
|
|
2301
|
+
let uploadResponse = await connectionClient.append(data.path, raw, data.flags, data.internalDate);
|
|
2302
|
+
|
|
2303
|
+
// Return to IDLE if using primary connection
|
|
2304
|
+
if (connectionClient === this.imapClient && this.imapClient.mailbox && !this.imapClient.idling) {
|
|
2305
|
+
// force back to IDLE
|
|
2306
|
+
this.imapClient.idle().catch(err => {
|
|
2307
|
+
this.logger.error({ msg: 'IDLE error', err });
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
// Pack response data
|
|
2312
|
+
if (uploadResponse.uid) {
|
|
2313
|
+
response.id = await this.packUid(uploadResponse.path || data.path, uploadResponse.uid);
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
response.path = uploadResponse.path;
|
|
2317
|
+
|
|
2318
|
+
if (uploadResponse.uid) {
|
|
2319
|
+
response.uid = uploadResponse.uid;
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
if (validUidValidity(uploadResponse.uidValidity)) {
|
|
2323
|
+
response.uidValidity = uploadResponse.uidValidity.toString();
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
if (uploadResponse.seq) {
|
|
2327
|
+
response.seq = uploadResponse.seq;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
if (messageId) {
|
|
2331
|
+
response.messageId = messageId;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
// Include reference information if provided
|
|
2335
|
+
if (data.reference && data.reference.message) {
|
|
2336
|
+
response.reference = {
|
|
2337
|
+
message: data.reference.message,
|
|
2338
|
+
documentStore: documentStoreUsed,
|
|
2339
|
+
success: referencedMessage ? true : false
|
|
2340
|
+
};
|
|
2341
|
+
|
|
2342
|
+
if (!referencedMessage) {
|
|
2343
|
+
response.reference.error = 'Referenced message was not found';
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
return response;
|
|
2348
|
+
} catch (err) {
|
|
2349
|
+
if (err.mailboxMissing) {
|
|
2350
|
+
// this mailbox is missing, refresh listing
|
|
2351
|
+
try {
|
|
2352
|
+
await this.getCurrentListing(false, { connectionClient });
|
|
2353
|
+
} catch (E) {
|
|
2354
|
+
this.logger.error({ msg: 'Missing mailbox', err, E });
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
err.code = 'UploadFail';
|
|
2359
|
+
err.statusCode = 502;
|
|
2360
|
+
throw err;
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
/**
|
|
2365
|
+
* Handles EXPUNGE and VANISHED notifications in order
|
|
2366
|
+
* @param {Object} payload - Expunge event data
|
|
2367
|
+
*/
|
|
2368
|
+
async expungeHandler(payload) {
|
|
2369
|
+
if (!payload || !payload.path || !this.mailboxes.has(normalizePath(payload.path))) {
|
|
2370
|
+
return; //?
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
let mailbox = this.mailboxes.get(normalizePath(payload.path));
|
|
2374
|
+
try {
|
|
2375
|
+
await mailbox.onExpunge(payload);
|
|
2376
|
+
} catch (err) {
|
|
2377
|
+
this.logger.error({ msg: 'Expunge error', err });
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
/**
|
|
2382
|
+
* Sets up subconnections for monitoring multiple folders simultaneously
|
|
2383
|
+
* @returns {number|null} Number of subconnections created
|
|
2384
|
+
*/
|
|
2385
|
+
async setupSubConnections() {
|
|
2386
|
+
const accountData = await this.accountObject.loadAccountData();
|
|
2387
|
+
|
|
2388
|
+
if (!accountData.subconnections?.length && !this.subconnections.length) {
|
|
2389
|
+
// Nothing to do here
|
|
2390
|
+
return null;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
const mailboxes = [];
|
|
2394
|
+
|
|
2395
|
+
const listing = await this.getCurrentListing(false, { allowSecondary: true });
|
|
2396
|
+
|
|
2397
|
+
// Process each configured subconnection
|
|
2398
|
+
for (const path of accountData.subconnections || []) {
|
|
2399
|
+
const entry = listing.find(entry => path === entry.path || path === entry.specialUse);
|
|
2400
|
+
|
|
2401
|
+
if (!entry) {
|
|
2402
|
+
// Mailbox not found - mark as disabled
|
|
2403
|
+
mailboxes.push({
|
|
2404
|
+
path,
|
|
2405
|
+
disabled: true,
|
|
2406
|
+
state: 'disabled',
|
|
2407
|
+
disabledReason: 'Mailbox folder not found'
|
|
2408
|
+
});
|
|
2409
|
+
continue;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
const accountPaths = [].concat(accountData.path || '*');
|
|
2413
|
+
if (!accountPaths.length) {
|
|
2414
|
+
accountPaths.push('*');
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
// Check if already covered by primary connection
|
|
2418
|
+
if (accountPaths[0] === entry.path) {
|
|
2419
|
+
mailboxes.push({
|
|
2420
|
+
path: entry.path,
|
|
2421
|
+
disabled: true,
|
|
2422
|
+
state: 'disabled',
|
|
2423
|
+
disabledReason: 'Covered by the primary connection'
|
|
2424
|
+
});
|
|
2425
|
+
continue;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
// Gmail-specific checks
|
|
2429
|
+
if (this.isGmail && accountPaths.includes('*') && !['\\Trash', '\\Junk'].includes(entry.specialUse)) {
|
|
2430
|
+
// no need to check this folder, as \All already covers it
|
|
2431
|
+
this.logger.info({ msg: 'Skip subconnection', path, reason: 'Covered by the All Mail folder' });
|
|
2432
|
+
mailboxes.push({
|
|
2433
|
+
path: entry.path,
|
|
2434
|
+
disabled: true,
|
|
2435
|
+
state: 'disabled',
|
|
2436
|
+
disabledReason: 'Covered by the "All Mail" folder'
|
|
2437
|
+
});
|
|
2438
|
+
continue;
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
// Non-Gmail checks
|
|
2442
|
+
if (!this.isGmail && accountPaths.includes('*') && entry.specialUse === '\\Inbox') {
|
|
2443
|
+
// already the default
|
|
2444
|
+
this.logger.info({ msg: 'Skip subconnection', path, reason: 'Trying to use the default folder' });
|
|
2445
|
+
mailboxes.push({
|
|
2446
|
+
path: entry.path,
|
|
2447
|
+
disabled: true,
|
|
2448
|
+
state: 'disabled',
|
|
2449
|
+
disabledReason: 'Can not use the default folder'
|
|
2450
|
+
});
|
|
2451
|
+
continue;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
mailboxes.push(entry);
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// remove unneeded subconnections
|
|
2458
|
+
for (let i = this.subconnections.length - 1; i >= 0; i--) {
|
|
2459
|
+
let subconnection = this.subconnections[i];
|
|
2460
|
+
if (!mailboxes.find(mailbox => mailbox.path === subconnection.path)) {
|
|
2461
|
+
// not listed anymore
|
|
2462
|
+
this.subconnections.splice(i, 1);
|
|
2463
|
+
|
|
2464
|
+
if (!subconnection.disabled) {
|
|
2465
|
+
try {
|
|
2466
|
+
subconnection.removeAllListeners();
|
|
2467
|
+
subconnection.close();
|
|
2468
|
+
} catch (err) {
|
|
2469
|
+
this.logger.error({ msg: 'Failed to close unlisted subconnection', path: subconnection.path, err });
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
// create missing subconnections
|
|
2476
|
+
for (const mailbox of mailboxes) {
|
|
2477
|
+
if (this.subconnections.find(subconnection => mailbox.path === subconnection.path)) {
|
|
2478
|
+
// already exists
|
|
2479
|
+
continue;
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
if (mailbox.disabled) {
|
|
2483
|
+
this.subconnections.push(mailbox);
|
|
2484
|
+
continue;
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
// create new subconnection
|
|
2488
|
+
const subconnection = new Subconnection({
|
|
2489
|
+
parent: this,
|
|
2490
|
+
account: this.account,
|
|
2491
|
+
mailbox,
|
|
2492
|
+
getImapConfig: async () => await this.getImapConfig(),
|
|
2493
|
+
logger: this.logger.child({
|
|
2494
|
+
cid: `${this.cid}:s:${this.connectionCount++}`,
|
|
2495
|
+
channel: 'subconnection',
|
|
2496
|
+
subconnection: mailbox.path
|
|
2497
|
+
})
|
|
2498
|
+
});
|
|
2499
|
+
this.subconnections.push(subconnection);
|
|
2500
|
+
|
|
2501
|
+
// Handle change notifications from subconnection
|
|
2502
|
+
subconnection.on('changes', path => {
|
|
2503
|
+
let mailbox;
|
|
2504
|
+
|
|
2505
|
+
if (this.mailboxes.has(normalizePath(path))) {
|
|
2506
|
+
mailbox = this.mailboxes.get(normalizePath(path));
|
|
2507
|
+
try {
|
|
2508
|
+
mailbox
|
|
2509
|
+
.sync()
|
|
2510
|
+
.then(() => this.ensureMainMailbox())
|
|
2511
|
+
.catch(err => {
|
|
2512
|
+
this.logger.error({ msg: 'Failed to sync mailbox', path, err });
|
|
2513
|
+
});
|
|
2514
|
+
} catch (err) {
|
|
2515
|
+
this.logger.error({ msg: 'Failed to sync mailbox', path, err });
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
});
|
|
2519
|
+
|
|
2520
|
+
await subconnection.init();
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
return this.subconnections.length;
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
/**
|
|
2527
|
+
* Closes all subconnections
|
|
2528
|
+
*/
|
|
2529
|
+
closeSubconnections() {
|
|
2530
|
+
const subconnections = [...this.subconnections];
|
|
2531
|
+
this.subconnections = [];
|
|
2532
|
+
|
|
2533
|
+
for (let subconnection of subconnections) {
|
|
2534
|
+
if (!subconnection.disabled) {
|
|
2535
|
+
try {
|
|
2536
|
+
subconnection.removeAllListeners();
|
|
2537
|
+
subconnection.close();
|
|
2538
|
+
} catch (err) {
|
|
2539
|
+
this.logger.error({ msg: 'Failed to close unlisted subconnection', path: subconnection.path, err });
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
/**
|
|
2546
|
+
* Pauses the IMAP connection
|
|
2547
|
+
* @returns {boolean} Success status
|
|
2548
|
+
*/
|
|
2549
|
+
async pause() {
|
|
2550
|
+
if (this.paused) {
|
|
2551
|
+
return false;
|
|
2552
|
+
}
|
|
2553
|
+
this.paused = true;
|
|
2554
|
+
this.logger.info({ msg: 'Closing connection', action: 'pause' });
|
|
2555
|
+
this.close();
|
|
2556
|
+
|
|
2557
|
+
this.state = 'paused';
|
|
2558
|
+
await this.setStateVal();
|
|
2559
|
+
await emitChangeEvent(this.logger, this.account, 'state', this.state);
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
/**
|
|
2563
|
+
* Resumes a paused IMAP connection
|
|
2564
|
+
* @returns {boolean} Success status
|
|
2565
|
+
*/
|
|
2566
|
+
async resume() {
|
|
2567
|
+
if (!this.paused) {
|
|
2568
|
+
return false;
|
|
2569
|
+
}
|
|
2570
|
+
this.paused = false;
|
|
2571
|
+
if (this.isClosed) {
|
|
2572
|
+
this.isClosed = false;
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
this.logger.info({ msg: 'Creating connection', action: 'resume' });
|
|
2576
|
+
// do not wait
|
|
2577
|
+
this.init().catch(err => this.logger.error({ msg: 'Resuming failed', action: 'resume', err }));
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
/**
|
|
2581
|
+
* Lists email signatures (Gmail only via API)
|
|
2582
|
+
* @returns {Object} Signatures list with support status
|
|
2583
|
+
*/
|
|
2584
|
+
async listSignatures() {
|
|
2585
|
+
const emptyResponse = { signatures: [], signaturesSupported: false };
|
|
2586
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
2587
|
+
|
|
2588
|
+
if (!accountData.oauth2.provider) {
|
|
2589
|
+
// Not an OAuth2 account
|
|
2590
|
+
return emptyResponse;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
if (accountData?._app?.provider && !['gmail'].includes(accountData?._app?.provider)) {
|
|
2594
|
+
// Signatures not supported for this provider
|
|
2595
|
+
return emptyResponse;
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
const { accessToken, oauth2App } = await this.loadOAuth2AccountCredentials(accountData, this, 'api');
|
|
2599
|
+
|
|
2600
|
+
if (oauth2App && !this.oAuth2Client) {
|
|
2601
|
+
this.oAuth2Client = await oauth2Apps.getClient(accountData.oauth2.provider, {
|
|
2602
|
+
logger: this.logger,
|
|
2603
|
+
logRaw: this.options.logRaw
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
if (!oauth2App || !this.oAuth2Client) {
|
|
2608
|
+
return emptyResponse;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
switch (oauth2App.provider) {
|
|
2612
|
+
case 'gmail': {
|
|
2613
|
+
// Fetch Gmail signatures via API
|
|
2614
|
+
const signatureListRes = await this.oAuth2Client.request(accessToken, `${GMAIL_API_BASE}/gmail/v1/users/me/settings/sendAs`, 'get');
|
|
2615
|
+
|
|
2616
|
+
let signatures = signatureListRes?.sendAs
|
|
2617
|
+
?.map(entry => ({ address: entry.sendAsEmail, signature: entry.signature }))
|
|
2618
|
+
.filter(entry => entry.signature);
|
|
2619
|
+
|
|
2620
|
+
return { signatures, signaturesSupported: true };
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
return emptyResponse;
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
module.exports = { IMAPClient };
|