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,3677 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { parentPort, threadId: workerThreadId } = require('worker_threads');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const logger = require('../logger');
|
|
6
|
+
const { webhooks: Webhooks } = require('../webhooks');
|
|
7
|
+
const { getESClient } = require('../document-store');
|
|
8
|
+
const { getThread } = require('../threads');
|
|
9
|
+
const settings = require('../settings');
|
|
10
|
+
const msgpack = require('msgpack5')();
|
|
11
|
+
const { templates } = require('../templates');
|
|
12
|
+
const { Gateway } = require('../gateway');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const punycode = require('punycode.js');
|
|
15
|
+
const { inlineHtml, inlineText, htmlToText, textToHtml, mimeHtml } = require('@postalsys/email-text-tools');
|
|
16
|
+
const { randomUUID: uuid } = require('crypto');
|
|
17
|
+
const { addTrackers } = require('../add-trackers');
|
|
18
|
+
const { getRawEmail } = require('../get-raw-email');
|
|
19
|
+
const { getTemplate } = require('@postalsys/templates');
|
|
20
|
+
const { deepEqual } = require('assert');
|
|
21
|
+
const { arfDetect } = require('../arf-detect');
|
|
22
|
+
const simpleParser = require('mailparser').simpleParser;
|
|
23
|
+
const libmime = require('libmime');
|
|
24
|
+
const { bounceDetect } = require('../bounce-detect');
|
|
25
|
+
const ical = require('ical.js');
|
|
26
|
+
const { llmPreProcess } = require('../llm-pre-process');
|
|
27
|
+
const { oauth2Apps } = require('../oauth2-apps');
|
|
28
|
+
const { Account } = require('../account');
|
|
29
|
+
const util = require('util');
|
|
30
|
+
const socks = require('socks');
|
|
31
|
+
const nodemailer = require('nodemailer');
|
|
32
|
+
const { removeBcc } = require('../get-raw-email');
|
|
33
|
+
const { oauth2ProviderData } = require('../oauth2-apps');
|
|
34
|
+
|
|
35
|
+
// Import various utility functions and constants
|
|
36
|
+
const {
|
|
37
|
+
getLocalAddress,
|
|
38
|
+
getSignedFormDataSync,
|
|
39
|
+
getServiceSecret,
|
|
40
|
+
convertDataUrisToAttachments,
|
|
41
|
+
genBaseBoundary,
|
|
42
|
+
getDuration,
|
|
43
|
+
getByteSize,
|
|
44
|
+
readEnvValue,
|
|
45
|
+
emitChangeEvent,
|
|
46
|
+
filterEmptyObjectValues,
|
|
47
|
+
resolveCredentials,
|
|
48
|
+
getDateBuckets
|
|
49
|
+
} = require('../tools');
|
|
50
|
+
|
|
51
|
+
// Import application constants
|
|
52
|
+
const {
|
|
53
|
+
AUTH_ERROR_NOTIFY,
|
|
54
|
+
ACCOUNT_INITIALIZED_NOTIFY,
|
|
55
|
+
REDIS_PREFIX,
|
|
56
|
+
MESSAGE_NEW_NOTIFY,
|
|
57
|
+
MESSAGE_DELETED_NOTIFY,
|
|
58
|
+
MESSAGE_UPDATED_NOTIFY,
|
|
59
|
+
EMAIL_BOUNCE_NOTIFY,
|
|
60
|
+
MAILBOX_DELETED_NOTIFY,
|
|
61
|
+
DEFAULT_DELIVERY_ATTEMPTS,
|
|
62
|
+
MIME_BOUNDARY_PREFIX,
|
|
63
|
+
DEFAULT_DOWNLOAD_CHUNK_SIZE,
|
|
64
|
+
EMAIL_COMPLAINT_NOTIFY,
|
|
65
|
+
MAX_INLINE_ATTACHMENT_SIZE,
|
|
66
|
+
DEFAULT_MAX_IMAP_AUTH_FAILURE_TIME,
|
|
67
|
+
TLS_DEFAULTS,
|
|
68
|
+
EMAIL_DELIVERY_ERROR_NOTIFY,
|
|
69
|
+
EMAIL_SENT_NOTIFY
|
|
70
|
+
} = require('../consts');
|
|
71
|
+
|
|
72
|
+
// Configure download chunk size from environment or use default
|
|
73
|
+
const DOWNLOAD_CHUNK_SIZE = getByteSize(readEnvValue('EENGINE_CHUNK_SIZE')) || DEFAULT_DOWNLOAD_CHUNK_SIZE;
|
|
74
|
+
|
|
75
|
+
// Configure maximum time to wait before disabling IMAP on authentication failures
|
|
76
|
+
const MAX_IMAP_AUTH_FAILURE_TIME = getDuration(readEnvValue('EENGINE_MAX_IMAP_AUTH_FAILURE_TIME')) || DEFAULT_MAX_IMAP_AUTH_FAILURE_TIME;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sends metrics data to the parent thread for aggregation
|
|
80
|
+
* @param {Object} meta - Metadata to include with the metric
|
|
81
|
+
* @param {Object} logger - Logger instance
|
|
82
|
+
* @param {string} key - Metric key identifier
|
|
83
|
+
* @param {string} method - Metric method (e.g., 'inc', 'dec')
|
|
84
|
+
* @param {...any} args - Additional arguments for the metric
|
|
85
|
+
*/
|
|
86
|
+
async function metricsMeta(meta, logger, key, method, ...args) {
|
|
87
|
+
try {
|
|
88
|
+
parentPort.postMessage({
|
|
89
|
+
cmd: 'metrics',
|
|
90
|
+
key,
|
|
91
|
+
method,
|
|
92
|
+
args,
|
|
93
|
+
meta: meta || {}
|
|
94
|
+
});
|
|
95
|
+
} catch (err) {
|
|
96
|
+
logger.error({ msg: 'Failed to post metrics to parent', err });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Map to track pending idempotency operations to prevent duplicate processing
|
|
101
|
+
const pendingIdempotencyOperations = new Map();
|
|
102
|
+
|
|
103
|
+
// Cache for SMTP connection pools to reuse connections
|
|
104
|
+
const SMTP_POOLS = new Map();
|
|
105
|
+
// Track last usage time for each pool to enable LRU-based cleanup
|
|
106
|
+
const SMTP_POOL_LAST_USED = new Map();
|
|
107
|
+
|
|
108
|
+
// Maximum idle time for SMTP pool connections (10 minutes of inactivity)
|
|
109
|
+
const SMTP_POOL_MAX_IDLE = 10 * 60 * 1000;
|
|
110
|
+
// Cleanup interval for idle SMTP pools (2 minutes)
|
|
111
|
+
const SMTP_POOL_CLEANUP_INTERVAL = 2 * 60 * 1000;
|
|
112
|
+
|
|
113
|
+
// Periodic cleanup of idle SMTP pool connections
|
|
114
|
+
let smtpPoolCleanupTimer = setInterval(() => {
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
const idleKeys = [];
|
|
117
|
+
|
|
118
|
+
for (const [poolKey, lastUsed] of SMTP_POOL_LAST_USED.entries()) {
|
|
119
|
+
if (now - lastUsed > SMTP_POOL_MAX_IDLE) {
|
|
120
|
+
idleKeys.push(poolKey);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const poolKey of idleKeys) {
|
|
125
|
+
const transporter = SMTP_POOLS.get(poolKey);
|
|
126
|
+
if (transporter) {
|
|
127
|
+
// Check if the transporter has active connections
|
|
128
|
+
if (transporter._connectionPool && transporter._connectionPool.size > 0) {
|
|
129
|
+
// Still has active connections, update last used time
|
|
130
|
+
SMTP_POOL_LAST_USED.set(poolKey, now);
|
|
131
|
+
logger.trace({ msg: 'SMTP pool still has active connections, skipping cleanup', poolKey });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
logger.debug({ msg: 'Cleaning up idle SMTP pool connection', poolKey, idleTime: now - SMTP_POOL_LAST_USED.get(poolKey) });
|
|
136
|
+
try {
|
|
137
|
+
transporter.close();
|
|
138
|
+
} catch (err) {
|
|
139
|
+
logger.error({ msg: 'Failed to close idle SMTP transporter', poolKey, err });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
SMTP_POOLS.delete(poolKey);
|
|
143
|
+
SMTP_POOL_LAST_USED.delete(poolKey);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (idleKeys.length > 0) {
|
|
147
|
+
logger.info({ msg: 'SMTP pool cleanup completed', cleaned: idleKeys.length, remaining: SMTP_POOLS.size });
|
|
148
|
+
}
|
|
149
|
+
}, SMTP_POOL_CLEANUP_INTERVAL);
|
|
150
|
+
|
|
151
|
+
// Prevent the timer from keeping the process alive
|
|
152
|
+
smtpPoolCleanupTimer.unref();
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Gets or creates a reusable SMTP transport for the given configuration
|
|
156
|
+
* Connection pooling improves performance by reusing SMTP connections
|
|
157
|
+
* @param {Object} smtpSettings - SMTP configuration settings
|
|
158
|
+
* @returns {Object} Nodemailer transport instance
|
|
159
|
+
*/
|
|
160
|
+
function getMailTransport(smtpSettings) {
|
|
161
|
+
// Extract only the settings that affect connection identity
|
|
162
|
+
let limitedSettings = {};
|
|
163
|
+
for (let key of ['name', 'localAddress', 'auth', 'host', 'port', 'secure', 'transactionLog', 'proxy']) {
|
|
164
|
+
limitedSettings[key] = smtpSettings[key];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Create a unique key for this SMTP configuration
|
|
168
|
+
let serializedSettings = JSON.stringify(limitedSettings);
|
|
169
|
+
let poolKey = crypto.createHash('sha256').update(serializedSettings).digest('hex');
|
|
170
|
+
|
|
171
|
+
// Return existing transport if available
|
|
172
|
+
let transporter;
|
|
173
|
+
if (SMTP_POOLS.has(poolKey)) {
|
|
174
|
+
transporter = SMTP_POOLS.get(poolKey);
|
|
175
|
+
// Update last used time for LRU tracking
|
|
176
|
+
SMTP_POOL_LAST_USED.set(poolKey, Date.now());
|
|
177
|
+
return transporter;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Configure connection pooling settings
|
|
181
|
+
smtpSettings.pool = true;
|
|
182
|
+
smtpSettings.maxConnections = 1;
|
|
183
|
+
smtpSettings.maxMessages = 100;
|
|
184
|
+
smtpSettings.socketTimeout = 2 * 60 * 1000; // 2 minute timeout
|
|
185
|
+
|
|
186
|
+
// Create new transport with pooling enabled
|
|
187
|
+
transporter = nodemailer.createTransport(smtpSettings);
|
|
188
|
+
transporter.set('proxy_socks_module', socks);
|
|
189
|
+
|
|
190
|
+
// Handle connection pool cleanup when idle
|
|
191
|
+
transporter.once('clear', () => {
|
|
192
|
+
// all emails processed and connection timed out
|
|
193
|
+
logger.trace({ msg: 'Clearing disconnected SMTP pool', poolKey });
|
|
194
|
+
SMTP_POOLS.delete(poolKey);
|
|
195
|
+
SMTP_POOL_LAST_USED.delete(poolKey);
|
|
196
|
+
try {
|
|
197
|
+
transporter.close();
|
|
198
|
+
} catch (closeErr) {
|
|
199
|
+
logger.error({ msg: 'Failed to close transporter', err: closeErr });
|
|
200
|
+
}
|
|
201
|
+
transporter = null;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Handle transport errors by removing from pool
|
|
205
|
+
transporter.once('error', err => {
|
|
206
|
+
// not sure what happned, but do not re-use this transporter object anymore
|
|
207
|
+
logger.error({ msg: 'Transporter failed', err });
|
|
208
|
+
SMTP_POOLS.delete(poolKey);
|
|
209
|
+
SMTP_POOL_LAST_USED.delete(poolKey);
|
|
210
|
+
try {
|
|
211
|
+
transporter.close();
|
|
212
|
+
} catch (closeErr) {
|
|
213
|
+
logger.error({ msg: 'Failed to close transporter', err: closeErr });
|
|
214
|
+
}
|
|
215
|
+
transporter = null;
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Cache the transport for reuse
|
|
219
|
+
SMTP_POOLS.set(poolKey, transporter);
|
|
220
|
+
SMTP_POOL_LAST_USED.set(poolKey, Date.now());
|
|
221
|
+
logger.trace({ msg: 'Created SMTP pool', poolKey });
|
|
222
|
+
|
|
223
|
+
return transporter;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Check if an error is a transient error that should be retried
|
|
228
|
+
* @param {Error} err - The error to check
|
|
229
|
+
* @returns {boolean} True if the error is transient
|
|
230
|
+
*/
|
|
231
|
+
function isTransientError(err) {
|
|
232
|
+
if (!err) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check for specific OAuth2 request failures (like Gmail 500 errors)
|
|
237
|
+
if (err.oauthRequest) {
|
|
238
|
+
const status = err.oauthRequest.status;
|
|
239
|
+
// 500, 502, 503, 504 are transient server errors
|
|
240
|
+
if (status >= 500 && status < 600) {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Check for network errors
|
|
246
|
+
if (err.code === 'ETIMEDOUT' || err.code === 'ECONNRESET' || err.code === 'ENOTFOUND' || err.code === 'EAI_AGAIN') {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check for specific error codes that indicate temporary issues
|
|
251
|
+
if (err.statusCode >= 500 && err.statusCode < 600) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Retry a function with exponential backoff for transient errors
|
|
260
|
+
* @param {Function} fn - Async function to retry
|
|
261
|
+
* @param {Object} options - Retry options
|
|
262
|
+
* @returns {Promise} Result of the function
|
|
263
|
+
*/
|
|
264
|
+
async function retryOnTransientError(fn, options = {}) {
|
|
265
|
+
const maxAttempts = options.maxAttempts || 3;
|
|
266
|
+
const baseDelay = options.baseDelay || 1000; // 1 second
|
|
267
|
+
const logger = options.logger;
|
|
268
|
+
|
|
269
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
270
|
+
try {
|
|
271
|
+
return await fn();
|
|
272
|
+
} catch (err) {
|
|
273
|
+
const isLastAttempt = attempt === maxAttempts;
|
|
274
|
+
const isTransient = isTransientError(err);
|
|
275
|
+
|
|
276
|
+
if (isTransient && !isLastAttempt) {
|
|
277
|
+
const delay = baseDelay * Math.pow(2, attempt - 1); // Exponential backoff
|
|
278
|
+
if (logger) {
|
|
279
|
+
logger.warn({
|
|
280
|
+
msg: 'Transient error occurred, retrying',
|
|
281
|
+
attempt,
|
|
282
|
+
maxAttempts,
|
|
283
|
+
delay,
|
|
284
|
+
err: err.message,
|
|
285
|
+
statusCode: err.statusCode || err.oauthRequest?.status
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
289
|
+
} else {
|
|
290
|
+
// Either non-transient error or last attempt - rethrow
|
|
291
|
+
throw err;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Base class for email client implementations
|
|
299
|
+
* Provides common functionality for IMAP/SMTP operations
|
|
300
|
+
* Subclasses should implement protocol-specific behavior
|
|
301
|
+
*/
|
|
302
|
+
class BaseClient {
|
|
303
|
+
constructor(account, options) {
|
|
304
|
+
this.account = account;
|
|
305
|
+
this.options = options || {};
|
|
306
|
+
|
|
307
|
+
// Generate unique connection ID for tracking
|
|
308
|
+
this.cid = this.getRandomId();
|
|
309
|
+
|
|
310
|
+
this.runIndex = this.options.runIndex;
|
|
311
|
+
|
|
312
|
+
// Core service dependencies
|
|
313
|
+
this.accountObject = this.options.accountObject;
|
|
314
|
+
this.accountLogger = this.options.accountLogger;
|
|
315
|
+
this.redis = this.options.redis;
|
|
316
|
+
|
|
317
|
+
// Message queue connections
|
|
318
|
+
this.notifyQueue = this.options.notifyQueue;
|
|
319
|
+
this.submitQueue = this.options.submitQueue;
|
|
320
|
+
this.documentsQueue = this.options.documentsQueue;
|
|
321
|
+
this.flowProducer = this.options.flowProducer;
|
|
322
|
+
|
|
323
|
+
// Inter-process communication handler
|
|
324
|
+
this.call = this.options.call;
|
|
325
|
+
|
|
326
|
+
this.logger = this.getLogger();
|
|
327
|
+
|
|
328
|
+
this.secret = this.options.secret;
|
|
329
|
+
|
|
330
|
+
// Track sub-connections (e.g., for IMAP IDLE)
|
|
331
|
+
this.subconnections = [];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Stub methods to be implemented by subclasses
|
|
335
|
+
|
|
336
|
+
async init() {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
close() {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async syncMailboxes() {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async delete() {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async resume() {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async reconnect() {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async subconnections() {
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async getQuota() {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Generates a random 20-character ID for connection tracking
|
|
370
|
+
* @returns {string} Random ID string
|
|
371
|
+
*/
|
|
372
|
+
getRandomId() {
|
|
373
|
+
let rid = BigInt('0x' + crypto.randomBytes(13).toString('hex')).toString(36);
|
|
374
|
+
if (rid.length < 20) {
|
|
375
|
+
rid = '0'.repeat(20 - rid.length) + rid;
|
|
376
|
+
} else if (rid.length > 20) {
|
|
377
|
+
rid = rid.substring(0, 20);
|
|
378
|
+
}
|
|
379
|
+
return rid;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Redis key generators for different data types
|
|
383
|
+
|
|
384
|
+
getAccountKey() {
|
|
385
|
+
return `${REDIS_PREFIX}iad:${this.account}`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
getMailboxListKey() {
|
|
389
|
+
return `${REDIS_PREFIX}ial:${this.account}`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
getMailboxHashKey() {
|
|
393
|
+
return `${REDIS_PREFIX}iah:${this.account}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
getSeenMessagesKey() {
|
|
397
|
+
return `${REDIS_PREFIX}iar:s:${this.account}`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
getLogKey() {
|
|
401
|
+
// this format ensures that the key is deleted when user is removed
|
|
402
|
+
return `${REDIS_PREFIX}iam:${this.account}:g`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
getLoggedAccountsKey() {
|
|
406
|
+
return `${REDIS_PREFIX}iaz:logged`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async currentState() {
|
|
410
|
+
return 'connected';
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Creates a logger instance that logs to both main logger and account logger
|
|
415
|
+
* @returns {Object} Synthetic logger that duplicates logs to account storage
|
|
416
|
+
*/
|
|
417
|
+
getLogger() {
|
|
418
|
+
this.mainLogger =
|
|
419
|
+
this.options.logger ||
|
|
420
|
+
logger.child({
|
|
421
|
+
component: 'connection-client',
|
|
422
|
+
account: this.account,
|
|
423
|
+
cid: this.cid
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
let syntheticLogger = {};
|
|
427
|
+
let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
|
|
428
|
+
|
|
429
|
+
// Create wrapper methods for each log level
|
|
430
|
+
for (let level of levels) {
|
|
431
|
+
syntheticLogger[level] = (...args) => {
|
|
432
|
+
// Log to main logger
|
|
433
|
+
this.mainLogger[level](...args);
|
|
434
|
+
|
|
435
|
+
// Also log to account-specific logger if enabled
|
|
436
|
+
if (this.accountLogger.enabled && args && args[0] && typeof args[0] === 'object') {
|
|
437
|
+
let entry = Object.assign({ level, t: Date.now(), cid: this.cid }, args[0]);
|
|
438
|
+
|
|
439
|
+
// Serialize error objects properly
|
|
440
|
+
if (entry.err && typeof entry.err === 'object') {
|
|
441
|
+
let err = entry.err;
|
|
442
|
+
entry.err = {
|
|
443
|
+
stack: err.stack
|
|
444
|
+
};
|
|
445
|
+
// enumerable error fields
|
|
446
|
+
Object.keys(err).forEach(key => {
|
|
447
|
+
entry.err[key] = err[key];
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
this.accountLogger.log(entry);
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
syntheticLogger.child = opts => this.mainLogger.child(opts);
|
|
457
|
+
|
|
458
|
+
return syntheticLogger;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Updates the connection state in Redis and tracks state changes
|
|
463
|
+
* Sends notification on first successful connection
|
|
464
|
+
*/
|
|
465
|
+
async setStateVal() {
|
|
466
|
+
let [[e1], [e2], [e3, prevVal], [e4, incrVal], [e5, stateVal]] = await this.redis
|
|
467
|
+
.multi()
|
|
468
|
+
.hSetExists(this.getAccountKey(), 'state', this.state)
|
|
469
|
+
.hSetBigger(this.getAccountKey(), 'runIndex', this.runIndex.toString())
|
|
470
|
+
.hget(this.getAccountKey(), `state:count:${this.state}`)
|
|
471
|
+
.hIncrbyExists(this.getAccountKey(), `state:count:${this.state}`, 1)
|
|
472
|
+
.hget(this.getAccountKey(), 'state')
|
|
473
|
+
.exec();
|
|
474
|
+
|
|
475
|
+
if (e1 || e2 || e3 || e4 || e5) {
|
|
476
|
+
throw e1 || e2 || e3 || e4 || e5;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Detect first successful connection
|
|
480
|
+
if (stateVal === 'connected' && incrVal === 1 && prevVal === '0') {
|
|
481
|
+
// first connected event!
|
|
482
|
+
await this.notify(false, ACCOUNT_INITIALIZED_NOTIFY, {
|
|
483
|
+
initialized: true
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Sets error state and manages error tracking/notifications
|
|
490
|
+
* Implements logic to disable IMAP after repeated authentication failures
|
|
491
|
+
* @param {string} event - Error event type
|
|
492
|
+
* @param {Object} data - Error details
|
|
493
|
+
* @returns {boolean} Whether this is the first occurrence of this error
|
|
494
|
+
*/
|
|
495
|
+
async setErrorState(event, data) {
|
|
496
|
+
// Retrieve previous error state for comparison
|
|
497
|
+
let prevLastErrorState = await this.redis.hget(this.getAccountKey(), 'lastErrorState');
|
|
498
|
+
if (prevLastErrorState) {
|
|
499
|
+
try {
|
|
500
|
+
prevLastErrorState = JSON.parse(prevLastErrorState);
|
|
501
|
+
} catch (err) {
|
|
502
|
+
// ignore
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
this.state = event;
|
|
507
|
+
await this.setStateVal();
|
|
508
|
+
|
|
509
|
+
let isFirstOccurrence = true;
|
|
510
|
+
|
|
511
|
+
// Store current error state
|
|
512
|
+
await this.redis.hSetExists(this.getAccountKey(), 'lastErrorState', JSON.stringify(data));
|
|
513
|
+
await emitChangeEvent(this.logger, this.account, 'state', event, { error: data });
|
|
514
|
+
|
|
515
|
+
// Check if this is a repeat of the same error
|
|
516
|
+
if (data && Object.keys(data).length && prevLastErrorState) {
|
|
517
|
+
// we have an error object, let's see if the error hasn't changed
|
|
518
|
+
|
|
519
|
+
if (data.serverResponseCode && data.serverResponseCode === prevLastErrorState.serverResponseCode) {
|
|
520
|
+
// error code did not change, assume it is the same error
|
|
521
|
+
isFirstOccurrence = false;
|
|
522
|
+
} else {
|
|
523
|
+
try {
|
|
524
|
+
deepEqual(data, prevLastErrorState);
|
|
525
|
+
// nothing changed
|
|
526
|
+
isFirstOccurrence = false;
|
|
527
|
+
} catch (err) {
|
|
528
|
+
// seems different
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Track error occurrences
|
|
534
|
+
if (isFirstOccurrence) {
|
|
535
|
+
await this.redis
|
|
536
|
+
.multi()
|
|
537
|
+
.hSetExists(this.getAccountKey(), 'lastError:errorCount', 1)
|
|
538
|
+
.hSetExists(this.getAccountKey(), 'lastError:first', new Date().toISOString())
|
|
539
|
+
.exec();
|
|
540
|
+
} else {
|
|
541
|
+
let errorCount;
|
|
542
|
+
let firstError;
|
|
543
|
+
|
|
544
|
+
let [[err1, ec], [err2, fe]] = await this.redis
|
|
545
|
+
.multi()
|
|
546
|
+
.hIncrbyExists(this.getAccountKey(), `lastError:errorCount`, 1)
|
|
547
|
+
.hget(this.getAccountKey(), 'lastError:first')
|
|
548
|
+
.exec();
|
|
549
|
+
|
|
550
|
+
if (!err1 && !err2) {
|
|
551
|
+
errorCount = ec || 0;
|
|
552
|
+
if (fe) {
|
|
553
|
+
fe = new Date(fe);
|
|
554
|
+
if (fe.toString() !== 'Invalid Date') {
|
|
555
|
+
firstError = fe;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
} else {
|
|
559
|
+
this.logger.error({ msg: 'Redis error while checking error state counters', err1, err2 });
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Handle repeated authentication errors by disabling IMAP
|
|
563
|
+
switch (event) {
|
|
564
|
+
case 'authenticationError':
|
|
565
|
+
if (errorCount > 0 && firstError && Date.now() - firstError.getTime() > MAX_IMAP_AUTH_FAILURE_TIME) {
|
|
566
|
+
// disable IMAP
|
|
567
|
+
let imapData;
|
|
568
|
+
let imapInfo = await this.redis.hget(this.getAccountKey(), 'imap');
|
|
569
|
+
if (imapInfo) {
|
|
570
|
+
try {
|
|
571
|
+
imapData = JSON.parse(imapInfo);
|
|
572
|
+
} catch (err) {
|
|
573
|
+
this.logger.error({ msg: 'Failed parsing IMAP data', err });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (imapData && !imapData.disabled) {
|
|
577
|
+
imapData.disabled = true;
|
|
578
|
+
|
|
579
|
+
// Disable IMAP and reset error counters
|
|
580
|
+
await this.redis
|
|
581
|
+
.multi()
|
|
582
|
+
.hSetExists(this.getAccountKey(), 'imap', JSON.stringify(imapData))
|
|
583
|
+
.hdel(this.getAccountKey(), 'lastError:errorCount', 'lastError:first')
|
|
584
|
+
.hSetExists(
|
|
585
|
+
this.getAccountKey(),
|
|
586
|
+
'lastErrorState',
|
|
587
|
+
JSON.stringify({
|
|
588
|
+
description: 'IMAP was disabled for the account due to exceeding the authentication error threshold',
|
|
589
|
+
response: data.response
|
|
590
|
+
})
|
|
591
|
+
)
|
|
592
|
+
.exec();
|
|
593
|
+
|
|
594
|
+
this.logger.info({
|
|
595
|
+
msg: 'IMAP was disabled for the account due to exceeding the authentication error threshold',
|
|
596
|
+
errorEvent: event,
|
|
597
|
+
firstError: firstError.toISOString(),
|
|
598
|
+
timeThreshold: MAX_IMAP_AUTH_FAILURE_TIME,
|
|
599
|
+
errorCount
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// Close the connection after disabling
|
|
603
|
+
setImmediate(() => {
|
|
604
|
+
this.close();
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return isFirstOccurrence;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Sends notifications for various email events through webhooks and queues
|
|
617
|
+
* Handles special processing for certain event types and document store integration
|
|
618
|
+
* @param {Object} mailbox - Mailbox information
|
|
619
|
+
* @param {string} event - Event type constant
|
|
620
|
+
* @param {Object} data - Event data payload
|
|
621
|
+
* @param {Object} extraOpts - Additional options
|
|
622
|
+
*/
|
|
623
|
+
async notify(mailbox, event, data, extraOpts) {
|
|
624
|
+
extraOpts = extraOpts || {};
|
|
625
|
+
const { skipWebhook, canSync = true } = extraOpts;
|
|
626
|
+
|
|
627
|
+
// Track event metrics
|
|
628
|
+
metricsMeta({ account: this.account }, this.logger, 'events', 'inc', {
|
|
629
|
+
event
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// Handle error state tracking for connection/auth errors
|
|
633
|
+
switch (event) {
|
|
634
|
+
case 'connectError':
|
|
635
|
+
case 'authenticationError': {
|
|
636
|
+
let shouldNotify = await this.setErrorState(event, data);
|
|
637
|
+
|
|
638
|
+
if (!shouldNotify) {
|
|
639
|
+
// do not send a webhook as nothing really changed
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
let serviceUrl = (await settings.get('serviceUrl')) || null;
|
|
647
|
+
|
|
648
|
+
// Build notification payload
|
|
649
|
+
let payload = {
|
|
650
|
+
serviceUrl,
|
|
651
|
+
account: this.account,
|
|
652
|
+
date: new Date().toISOString()
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
let path = (mailbox && mailbox.path) || (data && data.path);
|
|
656
|
+
if (path) {
|
|
657
|
+
payload.path = path;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (mailbox && mailbox.listingEntry && mailbox.listingEntry.specialUse) {
|
|
661
|
+
payload.specialUse = mailbox.listingEntry.specialUse;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (event) {
|
|
665
|
+
payload.event = event;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (data) {
|
|
669
|
+
payload.data = data;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
let queueKeep = (await settings.get('queueKeep')) || true;
|
|
673
|
+
|
|
674
|
+
// Determine if we need to sync with document store (ElasticSearch)
|
|
675
|
+
let addDocumentQueueJob =
|
|
676
|
+
canSync &&
|
|
677
|
+
this.documentsQueue &&
|
|
678
|
+
[MESSAGE_NEW_NOTIFY, MESSAGE_DELETED_NOTIFY, MESSAGE_UPDATED_NOTIFY, EMAIL_BOUNCE_NOTIFY, MAILBOX_DELETED_NOTIFY].includes(event) &&
|
|
679
|
+
(await settings.get('documentStoreEnabled'));
|
|
680
|
+
|
|
681
|
+
// Generate thread ID for new messages if needed
|
|
682
|
+
if (addDocumentQueueJob && payload.data && event === MESSAGE_NEW_NOTIFY && !payload.data.threadId) {
|
|
683
|
+
// Generate a thread ID for the email. This is also stored in ElasticSearch.
|
|
684
|
+
const { index, client } = await getESClient(logger);
|
|
685
|
+
try {
|
|
686
|
+
if (client) {
|
|
687
|
+
let thread = await getThread(client, index, this.account, payload.data, logger);
|
|
688
|
+
if (thread) {
|
|
689
|
+
payload.data.threadId = thread;
|
|
690
|
+
logger.info({
|
|
691
|
+
msg: 'Provisioned thread ID for a message',
|
|
692
|
+
account: this.account,
|
|
693
|
+
message: payload.data.id,
|
|
694
|
+
threadId: payload.data.threadId
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
} catch (err) {
|
|
699
|
+
if (logger.notifyError) {
|
|
700
|
+
logger.notifyError(err, event => {
|
|
701
|
+
event.setUser(this.account);
|
|
702
|
+
event.addMetadata('ee', {
|
|
703
|
+
index
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
logger.error({ msg: 'Failed to resolve thread', account: this.account, message: payload.data.id, err });
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Configure job options for queuing
|
|
712
|
+
const defaultJobOptions = {
|
|
713
|
+
removeOnComplete: queueKeep,
|
|
714
|
+
removeOnFail: queueKeep,
|
|
715
|
+
attempts: 10,
|
|
716
|
+
backoff: {
|
|
717
|
+
type: 'exponential',
|
|
718
|
+
delay: 5000
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
// use more attempts for ElasticSearch updates
|
|
723
|
+
const documentJobOptions = Object.assign(structuredClone(defaultJobOptions), { attempts: 16 });
|
|
724
|
+
|
|
725
|
+
// Process notifications with flow (webhook + document store) or separately
|
|
726
|
+
if (!skipWebhook && addDocumentQueueJob) {
|
|
727
|
+
// add both jobs as a Flow
|
|
728
|
+
|
|
729
|
+
let notifyPayload = await Webhooks.formatPayload(event, payload);
|
|
730
|
+
|
|
731
|
+
const queueFlow = [
|
|
732
|
+
{
|
|
733
|
+
name: event,
|
|
734
|
+
data: payload,
|
|
735
|
+
queueName: 'documents'
|
|
736
|
+
}
|
|
737
|
+
];
|
|
738
|
+
|
|
739
|
+
await Webhooks.pushToQueue(event, notifyPayload, {
|
|
740
|
+
routesOnly: true,
|
|
741
|
+
queueFlow
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
await this.flowProducer.add(
|
|
745
|
+
{
|
|
746
|
+
name: event,
|
|
747
|
+
data: notifyPayload,
|
|
748
|
+
queueName: 'notify',
|
|
749
|
+
children: queueFlow
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
queuesOptions: {
|
|
753
|
+
notify: {
|
|
754
|
+
defaultJobOptions
|
|
755
|
+
},
|
|
756
|
+
documents: {
|
|
757
|
+
defaultJobOptions: documentJobOptions
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
);
|
|
762
|
+
} else {
|
|
763
|
+
// add to queues as normal jobs
|
|
764
|
+
|
|
765
|
+
if (!skipWebhook) {
|
|
766
|
+
await Webhooks.pushToQueue(event, await Webhooks.formatPayload(event, payload));
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (addDocumentQueueJob) {
|
|
770
|
+
await this.documentsQueue.add(event, payload, documentJobOptions);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Loads OAuth2 credentials for login, handling token refresh if needed
|
|
777
|
+
* @param {Object} accountObject - Account instance
|
|
778
|
+
* @param {Object} accountData - Account configuration data
|
|
779
|
+
* @param {Object} ctx - Context object (usually 'this')
|
|
780
|
+
* @param {string} target - Target service ('imap' or 'smtp')
|
|
781
|
+
* @returns {Object} OAuth2 credentials including access token
|
|
782
|
+
*/
|
|
783
|
+
async loadOAuth2LoginCredentials(accountObject, accountData, ctx, target) {
|
|
784
|
+
const now = Date.now();
|
|
785
|
+
const oauth2User = accountData.oauth2.auth.delegatedUser || accountData.oauth2.auth.user;
|
|
786
|
+
let accessToken;
|
|
787
|
+
|
|
788
|
+
// Load OAuth2 app configuration
|
|
789
|
+
const oauth2App = await oauth2Apps.get(accountData.oauth2.provider);
|
|
790
|
+
if (!oauth2App) {
|
|
791
|
+
let error = new Error('Missing or disabled OAuth2 app');
|
|
792
|
+
error.code = 'AppNotFound';
|
|
793
|
+
throw error;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Verify app is configured for IMAP
|
|
797
|
+
if (oauth2App.baseScopes && oauth2App.baseScopes !== 'imap') {
|
|
798
|
+
let error = new Error('Invalid base scopes for references OAuth2 application');
|
|
799
|
+
error.code = 'InvalidBaseScopes';
|
|
800
|
+
throw error;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Check if token needs refresh (with 30 second buffer)
|
|
804
|
+
if (!accountData.oauth2.accessToken || !accountData.oauth2.expires || accountData.oauth2.expires < new Date(now - 30 * 1000)) {
|
|
805
|
+
// renew access token
|
|
806
|
+
try {
|
|
807
|
+
accountData = await accountObject.renewAccessToken({
|
|
808
|
+
logger: this.logger,
|
|
809
|
+
logRaw: this.options.logRaw
|
|
810
|
+
});
|
|
811
|
+
accessToken = accountData.oauth2.accessToken;
|
|
812
|
+
} catch (err) {
|
|
813
|
+
err.authenticationFailed = true;
|
|
814
|
+
let notifyData = {
|
|
815
|
+
response: err.message,
|
|
816
|
+
serverResponseCode: 'OauthRenewError'
|
|
817
|
+
};
|
|
818
|
+
if (err.tokenRequest) {
|
|
819
|
+
notifyData.tokenRequest = err.tokenRequest;
|
|
820
|
+
}
|
|
821
|
+
await ctx.notify(false, AUTH_ERROR_NOTIFY, notifyData);
|
|
822
|
+
ctx.logger.error({
|
|
823
|
+
account: accountObject.account,
|
|
824
|
+
err
|
|
825
|
+
});
|
|
826
|
+
ctx.state = 'authenticationError';
|
|
827
|
+
throw err;
|
|
828
|
+
}
|
|
829
|
+
} else {
|
|
830
|
+
// Use cached token
|
|
831
|
+
this.logger.info({
|
|
832
|
+
msg: 'Using cached OAuth2 access token',
|
|
833
|
+
action: 'ensureAccessToken',
|
|
834
|
+
target,
|
|
835
|
+
error: null,
|
|
836
|
+
user: accountData.oauth2.auth.user,
|
|
837
|
+
expires: accountData.oauth2.expires,
|
|
838
|
+
scopes: accountData.oauth2.scope,
|
|
839
|
+
oauth2App: accountData.oauth2.provider
|
|
840
|
+
});
|
|
841
|
+
accessToken = accountData.oauth2.accessToken;
|
|
842
|
+
}
|
|
843
|
+
return { oauth2User, accessToken, oauth2App };
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Resolves delegated account chain for OAuth2 authentication
|
|
848
|
+
* Follows delegation chain up to 20 hops, detecting loops
|
|
849
|
+
* @param {Object} accountData - Initial account data
|
|
850
|
+
* @returns {Object} Final delegated account data
|
|
851
|
+
*/
|
|
852
|
+
async getDelegatedAccount(accountData) {
|
|
853
|
+
let redirect = 0;
|
|
854
|
+
let providerAccountData = accountData;
|
|
855
|
+
let delegatedAccountData;
|
|
856
|
+
let seenAccounts = new Set();
|
|
857
|
+
let hopsAllowed = 20;
|
|
858
|
+
|
|
859
|
+
// Follow delegation chain
|
|
860
|
+
while (redirect++ < hopsAllowed) {
|
|
861
|
+
// Load delegated account if not cached
|
|
862
|
+
if (!this.delegatedAccountObject || this.delegatedAccountObject.account !== providerAccountData.oauth2.auth.delegatedAccount) {
|
|
863
|
+
this.delegatedAccountObject = new Account({
|
|
864
|
+
account: accountData.oauth2.auth.delegatedAccount,
|
|
865
|
+
redis: this.accountObject.redis,
|
|
866
|
+
call: this.accountObject.call,
|
|
867
|
+
secret: this.accountObject.secret,
|
|
868
|
+
timeout: this.accountObject.timeout
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
delegatedAccountData = await this.delegatedAccountObject.loadAccountData();
|
|
872
|
+
|
|
873
|
+
// Check if this account also delegates to another
|
|
874
|
+
if (delegatedAccountData.oauth2.auth.delegatedUser && delegatedAccountData.oauth2.auth.delegatedAccount) {
|
|
875
|
+
// Detect delegation loops
|
|
876
|
+
if (seenAccounts.has(providerAccountData.account)) {
|
|
877
|
+
// loop detected
|
|
878
|
+
let error = new Error('Delegation looping detected');
|
|
879
|
+
throw error;
|
|
880
|
+
}
|
|
881
|
+
seenAccounts.add(providerAccountData.account);
|
|
882
|
+
providerAccountData = delegatedAccountData;
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (redirect >= hopsAllowed) {
|
|
889
|
+
let error = new Error('Too many delegation hops');
|
|
890
|
+
throw error;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
return delegatedAccountData;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Loads OAuth2 credentials handling delegated accounts
|
|
898
|
+
* @param {Object} accountData - Account configuration
|
|
899
|
+
* @param {Object} ctx - Context object
|
|
900
|
+
* @param {string} target - Target service ('imap' or 'smtp')
|
|
901
|
+
* @returns {Object} OAuth2 credentials
|
|
902
|
+
*/
|
|
903
|
+
async loadOAuth2AccountCredentials(accountData, ctx, target) {
|
|
904
|
+
let oauthCredentials;
|
|
905
|
+
|
|
906
|
+
// Handle delegated accounts (shared mailboxes)
|
|
907
|
+
if (accountData.oauth2.auth.delegatedUser && accountData.oauth2.auth.delegatedAccount) {
|
|
908
|
+
const delegatedAccountData = await this.getDelegatedAccount(accountData);
|
|
909
|
+
oauthCredentials = await this.loadOAuth2LoginCredentials(this.delegatedAccountObject, delegatedAccountData, ctx, target);
|
|
910
|
+
if (target !== 'smtp') {
|
|
911
|
+
// Use delegated user for IMAP
|
|
912
|
+
oauthCredentials.oauth2User = accountData.oauth2.auth.delegatedUser || oauthCredentials.oauth2User;
|
|
913
|
+
}
|
|
914
|
+
} else {
|
|
915
|
+
// Direct OAuth2 authentication
|
|
916
|
+
oauthCredentials = await this.loadOAuth2LoginCredentials(this.accountObject, accountData, ctx, target);
|
|
917
|
+
if (accountData.oauth2.auth.delegatedUser && target === 'smtp') {
|
|
918
|
+
// override SMTP username, do not use the shared user
|
|
919
|
+
oauthCredentials.oauth2User = accountData.oauth2.auth.user;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
return oauthCredentials;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Checks and manages idempotency for operations to prevent duplicates
|
|
928
|
+
* Uses Redis to track operation status across workers
|
|
929
|
+
* @param {string} objName - Object type name for namespacing
|
|
930
|
+
* @param {string} idempotencyKey - Unique key for the operation
|
|
931
|
+
* @returns {Object|null} Idempotency data if exists
|
|
932
|
+
*/
|
|
933
|
+
async checkIdempotencyKey(objName, idempotencyKey) {
|
|
934
|
+
if (!idempotencyKey) {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const idempotencyKeyName = objName ? `${objName}/${idempotencyKey}` : idempotencyKey;
|
|
939
|
+
|
|
940
|
+
// check last 24-48 hours, so probably will return 2 keys, at rare cases 1
|
|
941
|
+
const { bucketKeys } = getDateBuckets(1 * 24 * 3600);
|
|
942
|
+
|
|
943
|
+
// Use custom Redis command to check idempotency atomically
|
|
944
|
+
let idempotencyResultStr = await this.redis.eeGetIdempotency(
|
|
945
|
+
`${REDIS_PREFIX}idempotency:bucket:`,
|
|
946
|
+
idempotencyKeyName,
|
|
947
|
+
this.runIndex,
|
|
948
|
+
workerThreadId,
|
|
949
|
+
bucketKeys.join(',')
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
let idempotencyData;
|
|
953
|
+
try {
|
|
954
|
+
idempotencyData = JSON.parse(idempotencyResultStr);
|
|
955
|
+
idempotencyData.idempotencyKey = idempotencyKey;
|
|
956
|
+
idempotencyData.idempotencyKeyName = idempotencyKeyName;
|
|
957
|
+
} catch (err) {
|
|
958
|
+
this.logger.error({ msg: 'Failed to parse idempotency data', idempotencyKey, cachedValue: idempotencyResultStr, err });
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Track pending operations to handle concurrent requests
|
|
962
|
+
if (idempotencyData?.status === 'new') {
|
|
963
|
+
if (pendingIdempotencyOperations.has(idempotencyKeyName)) {
|
|
964
|
+
let error = new Error('Cancelling pending operation');
|
|
965
|
+
for (let promise of pendingIdempotencyOperations.get(idempotencyKeyName)) {
|
|
966
|
+
promise.reject(error);
|
|
967
|
+
}
|
|
968
|
+
pendingIdempotencyOperations.delete(idempotencyKeyName);
|
|
969
|
+
}
|
|
970
|
+
pendingIdempotencyOperations.set(idempotencyKeyName, []);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// use existing response
|
|
974
|
+
switch (idempotencyData.status) {
|
|
975
|
+
case 'completed':
|
|
976
|
+
// Return cached result
|
|
977
|
+
idempotencyData.returnValue = Object.assign({}, idempotencyData.result, {
|
|
978
|
+
idempotency: { key: idempotencyData.idempotencyKey, status: 'HIT' }
|
|
979
|
+
});
|
|
980
|
+
break;
|
|
981
|
+
case 'pending': {
|
|
982
|
+
// Wait for operation to complete
|
|
983
|
+
let queueResult = await new Promise((resolve, reject) => {
|
|
984
|
+
if (!pendingIdempotencyOperations.has(idempotencyData.idempotencyKeyName)) {
|
|
985
|
+
pendingIdempotencyOperations.set(idempotencyData.idempotencyKeyName, []);
|
|
986
|
+
}
|
|
987
|
+
pendingIdempotencyOperations.get(idempotencyData.idempotencyKeyName).push({ resolve, reject });
|
|
988
|
+
});
|
|
989
|
+
idempotencyData.returnValue = Object.assign({}, queueResult, { idempotency: { key: idempotencyData.idempotencyKey, status: 'HIT' } });
|
|
990
|
+
break;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return idempotencyData || null;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Updates idempotency cache with operation result
|
|
999
|
+
* @param {Object} idempotencyData - Idempotency tracking data
|
|
1000
|
+
* @param {Object} result - Operation result to cache
|
|
1001
|
+
*/
|
|
1002
|
+
async updateIdempotencyData(idempotencyData, result) {
|
|
1003
|
+
if (idempotencyData?.bucketKey && idempotencyData?.idempotencyKeyName) {
|
|
1004
|
+
// update status and result
|
|
1005
|
+
try {
|
|
1006
|
+
await this.redis.hset(
|
|
1007
|
+
idempotencyData?.bucketKey,
|
|
1008
|
+
idempotencyData.idempotencyKeyName,
|
|
1009
|
+
JSON.stringify({
|
|
1010
|
+
status: 'completed',
|
|
1011
|
+
runIndex: idempotencyData.runIndex,
|
|
1012
|
+
threadId: idempotencyData.threadId,
|
|
1013
|
+
result
|
|
1014
|
+
})
|
|
1015
|
+
);
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
this.logger.error({
|
|
1018
|
+
msg: 'Failed to update idempotency data',
|
|
1019
|
+
idempotencyKey: idempotencyData.idempotencyKey,
|
|
1020
|
+
err
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Resolve any waiting promises
|
|
1025
|
+
for (let promise of pendingIdempotencyOperations.get(idempotencyData.idempotencyKeyName)) {
|
|
1026
|
+
promise.resolve(result);
|
|
1027
|
+
}
|
|
1028
|
+
pendingIdempotencyOperations.delete(idempotencyData.idempotencyKeyName);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Clears idempotency data on operation failure
|
|
1034
|
+
* @param {Object} idempotencyData - Idempotency tracking data
|
|
1035
|
+
* @param {Error} error - Error that caused the failure
|
|
1036
|
+
*/
|
|
1037
|
+
async clearIdempotencyData(idempotencyData, error) {
|
|
1038
|
+
if (idempotencyData?.status === 'new' && idempotencyData?.bucketKey && idempotencyData?.idempotencyKeyName) {
|
|
1039
|
+
// delete failed attempt information
|
|
1040
|
+
try {
|
|
1041
|
+
await this.redis.hdel(idempotencyData?.bucketKey, idempotencyData.idempotencyKeyName);
|
|
1042
|
+
} catch (err) {
|
|
1043
|
+
this.logger.error({
|
|
1044
|
+
msg: 'Failed to clear idempotency data',
|
|
1045
|
+
idempotencyKey: idempotencyData.idempotencyKey,
|
|
1046
|
+
err
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// Reject any waiting promises
|
|
1051
|
+
if (pendingIdempotencyOperations.has(idempotencyData.idempotencyKeyName)) {
|
|
1052
|
+
for (let promise of pendingIdempotencyOperations.get(idempotencyData.idempotencyKeyName)) {
|
|
1053
|
+
promise.reject(error);
|
|
1054
|
+
}
|
|
1055
|
+
pendingIdempotencyOperations.delete(idempotencyData.idempotencyKeyName);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Queues a message for sending with idempotency support
|
|
1062
|
+
* @param {Object} data - Message data
|
|
1063
|
+
* @param {Object} meta - Metadata
|
|
1064
|
+
* @param {Object} connectionOptions - Connection options
|
|
1065
|
+
* @returns {Object} Queue result
|
|
1066
|
+
*/
|
|
1067
|
+
async queueMessage(data, meta, connectionOptions) {
|
|
1068
|
+
let idempotencyData;
|
|
1069
|
+
|
|
1070
|
+
// Check for duplicate operations
|
|
1071
|
+
if (meta.idempotencyKey) {
|
|
1072
|
+
idempotencyData = await this.checkIdempotencyKey(`mq/${this.account}`, meta.idempotencyKey);
|
|
1073
|
+
if (idempotencyData?.returnValue) {
|
|
1074
|
+
return idempotencyData?.returnValue;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
let queueResult;
|
|
1079
|
+
try {
|
|
1080
|
+
queueResult = await this.queueMessageHandler(data, meta, connectionOptions);
|
|
1081
|
+
await this.updateIdempotencyData(idempotencyData, queueResult);
|
|
1082
|
+
} catch (err) {
|
|
1083
|
+
await this.clearIdempotencyData(idempotencyData, err);
|
|
1084
|
+
throw err;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (idempotencyData?.status === 'new') {
|
|
1088
|
+
return Object.assign({}, queueResult, { idempotency: { key: idempotencyData.idempotencyKey, status: 'MISS' } });
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
return queueResult;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Main handler for queuing messages for delivery
|
|
1096
|
+
* Processes templates, handles mail merge, and schedules delivery
|
|
1097
|
+
* @param {Object} data - Message data
|
|
1098
|
+
* @param {Object} meta - Metadata
|
|
1099
|
+
* @param {Object} connectionOptions - Connection options
|
|
1100
|
+
* @returns {Object} Queue result or mail merge results
|
|
1101
|
+
*/
|
|
1102
|
+
async queueMessageHandler(data, meta, connectionOptions) {
|
|
1103
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
1104
|
+
|
|
1105
|
+
// Load gateway configuration if specified
|
|
1106
|
+
let gatewayData;
|
|
1107
|
+
let gatewayObject;
|
|
1108
|
+
if (data.gateway) {
|
|
1109
|
+
gatewayObject = new Gateway({ gateway: data.gateway, redis: this.redis, secret: this.secret });
|
|
1110
|
+
try {
|
|
1111
|
+
gatewayData = await gatewayObject.loadGatewayData();
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
this.logger.info({ msg: 'Failed to load gateway data', messageId: data.messageId, gateway: data.gateway, err });
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Verify SMTP capability
|
|
1118
|
+
if (!accountData.smtp && !accountData.oauth2 && !gatewayData) {
|
|
1119
|
+
// can not make connection
|
|
1120
|
+
let err = new Error('SMTP configuration not found');
|
|
1121
|
+
err.code = 'SMTPUnavailable';
|
|
1122
|
+
err.statusCode = 404;
|
|
1123
|
+
throw err;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
let licenseInfo = await this.call({ cmd: 'license' });
|
|
1127
|
+
|
|
1128
|
+
// Configure message defaults
|
|
1129
|
+
data.disableFileAccess = true;
|
|
1130
|
+
data.disableUrlAccess = true;
|
|
1131
|
+
data.boundaryPrefix = MIME_BOUNDARY_PREFIX;
|
|
1132
|
+
data.baseBoundary = genBaseBoundary();
|
|
1133
|
+
|
|
1134
|
+
// convert data uri images to attachments
|
|
1135
|
+
convertDataUrisToAttachments(data);
|
|
1136
|
+
|
|
1137
|
+
// Process template if specified
|
|
1138
|
+
if (data.template) {
|
|
1139
|
+
let templateData = await templates.get(data.template);
|
|
1140
|
+
if (!templateData || (templateData.account && templateData.account !== accountData.account)) {
|
|
1141
|
+
let err = new Error(`Requested template was not found [${data.template}]`);
|
|
1142
|
+
err.code = 'TemplateNotFound';
|
|
1143
|
+
err.statusCode = 404;
|
|
1144
|
+
throw err;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
if (templateData.content && templateData.content.html && templateData.format) {
|
|
1148
|
+
data.render = data.render || {};
|
|
1149
|
+
data.render.format = templateData.format;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Merge template content into message
|
|
1153
|
+
for (let key of Object.keys(templateData.content || {})) {
|
|
1154
|
+
data[key] = templateData.content[key];
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
delete data.template;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Handle single message or mail merge
|
|
1161
|
+
if (!data.mailMerge || !data.mailMerge.length) {
|
|
1162
|
+
return this.queueMessageEntry(data, meta, licenseInfo, connectionOptions);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Process mail merge batch
|
|
1166
|
+
let mailMergeList = data.mailMerge;
|
|
1167
|
+
delete data.mailMerge;
|
|
1168
|
+
delete data.messageId;
|
|
1169
|
+
delete data.to;
|
|
1170
|
+
|
|
1171
|
+
let messageProcessors = [];
|
|
1172
|
+
|
|
1173
|
+
// Create individual messages for each recipient
|
|
1174
|
+
for (let mailMergeEntry of mailMergeList) {
|
|
1175
|
+
let messageCopy = structuredClone(data);
|
|
1176
|
+
if (messageCopy.sendAt) {
|
|
1177
|
+
// date values do not survive JSON based copying
|
|
1178
|
+
messageCopy.sendAt = new Date(messageCopy.sendAt);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
messageCopy.to = [mailMergeEntry.to];
|
|
1182
|
+
|
|
1183
|
+
// Apply recipient-specific overrides
|
|
1184
|
+
for (let key of ['messageId', 'sendAt']) {
|
|
1185
|
+
if (mailMergeEntry[key]) {
|
|
1186
|
+
messageCopy[key] = mailMergeEntry[key];
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// Apply recipient-specific template parameters
|
|
1191
|
+
if (mailMergeEntry.params) {
|
|
1192
|
+
messageCopy.render = messageCopy.render || {};
|
|
1193
|
+
messageCopy.render.params = mailMergeEntry.params;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
messageProcessors.push(this.queueMessageEntry(messageCopy, meta, licenseInfo, connectionOptions));
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
let response = {
|
|
1200
|
+
mailMerge: []
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
// Process all messages in parallel
|
|
1204
|
+
let results = await Promise.allSettled(messageProcessors);
|
|
1205
|
+
for (let i = 0; i < mailMergeList.length; i++) {
|
|
1206
|
+
let mailMergeEntry = mailMergeList[i];
|
|
1207
|
+
let resultEntry = results[i];
|
|
1208
|
+
|
|
1209
|
+
let result = Object.assign(
|
|
1210
|
+
{
|
|
1211
|
+
success: resultEntry.status === 'fulfilled',
|
|
1212
|
+
to: mailMergeEntry.to
|
|
1213
|
+
},
|
|
1214
|
+
resultEntry.status === 'fulfilled'
|
|
1215
|
+
? resultEntry.value.responseValue || {
|
|
1216
|
+
messageId: resultEntry.value.messageId,
|
|
1217
|
+
queueId: resultEntry.value.queueId,
|
|
1218
|
+
sendAt: resultEntry.value.sendAt
|
|
1219
|
+
}
|
|
1220
|
+
: {
|
|
1221
|
+
error: (resultEntry.reason && resultEntry.reason.message) || resultEntry.status,
|
|
1222
|
+
code: (resultEntry.reason && resultEntry.reason.code) || 'SubmitFail',
|
|
1223
|
+
statusCode: (resultEntry.reason && Number(resultEntry.reason.statusCode)) || null
|
|
1224
|
+
}
|
|
1225
|
+
);
|
|
1226
|
+
|
|
1227
|
+
response.mailMerge.push(result);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
return response;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// placeholder - to be implemented by subclasses
|
|
1234
|
+
async checkIMAPConnection() {
|
|
1235
|
+
return true;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Prepares raw email message for sending/storing
|
|
1240
|
+
* Handles references, inline content, and attachments
|
|
1241
|
+
* @param {Object} data - Message data
|
|
1242
|
+
* @param {Object} options - Processing options
|
|
1243
|
+
* @param {Object} connectionOptions - Connection options
|
|
1244
|
+
* @returns {Object} Raw message and metadata
|
|
1245
|
+
*/
|
|
1246
|
+
async prepareRawMessage(data, options, connectionOptions) {
|
|
1247
|
+
options = options || {};
|
|
1248
|
+
|
|
1249
|
+
// Configure message defaults
|
|
1250
|
+
data.disableFileAccess = true;
|
|
1251
|
+
data.disableUrlAccess = true;
|
|
1252
|
+
data.boundaryPrefix = MIME_BOUNDARY_PREFIX;
|
|
1253
|
+
data.baseBoundary = genBaseBoundary();
|
|
1254
|
+
|
|
1255
|
+
// convert data uri images to attachments
|
|
1256
|
+
convertDataUrisToAttachments(data);
|
|
1257
|
+
|
|
1258
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
1259
|
+
|
|
1260
|
+
// Check if send-only account is trying to use reference.action
|
|
1261
|
+
if (data.reference && data.reference.message && data.reference.action) {
|
|
1262
|
+
let app;
|
|
1263
|
+
if (accountData.oauth2 && accountData.oauth2.provider) {
|
|
1264
|
+
try {
|
|
1265
|
+
app = await this.accountObject.getOauth2App();
|
|
1266
|
+
} catch (err) {
|
|
1267
|
+
// ignore
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
if (this.accountObject.isSendOnlyAccount(accountData, app)) {
|
|
1272
|
+
let err = new Error('Reference actions (reply, forward) are not supported for send-only accounts');
|
|
1273
|
+
err.code = 'ReferenceNotSupported';
|
|
1274
|
+
err.statusCode = 400;
|
|
1275
|
+
throw err;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// Set default from address for reference actions
|
|
1280
|
+
if (!data.from && data.reference?.action) {
|
|
1281
|
+
data.from = {
|
|
1282
|
+
name: accountData.name,
|
|
1283
|
+
address: accountData.email
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// Configure locale and timezone for inline content
|
|
1288
|
+
let inlineOptions = {
|
|
1289
|
+
locale: data.locale || accountData.locale || (await settings.get('locale')),
|
|
1290
|
+
tz: data.tz || accountData.tz || (await settings.get('timezone'))
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
delete data.locale;
|
|
1294
|
+
delete data.tz;
|
|
1295
|
+
|
|
1296
|
+
let referencedMessage;
|
|
1297
|
+
let documentStoreUsed = false;
|
|
1298
|
+
|
|
1299
|
+
// Resolve reference and update reference/in-reply-to headers
|
|
1300
|
+
if (data.reference && data.reference.message) {
|
|
1301
|
+
// Try document store first if enabled
|
|
1302
|
+
if (data.reference.documentStore && (await settings.get('documentStoreEnabled'))) {
|
|
1303
|
+
try {
|
|
1304
|
+
referencedMessage = await this.accountObject.getMessage(data.reference.message, {
|
|
1305
|
+
documentStore: true,
|
|
1306
|
+
textType: '*'
|
|
1307
|
+
});
|
|
1308
|
+
} catch (err) {
|
|
1309
|
+
if (err.meta && err.meta.statusCode === 404) {
|
|
1310
|
+
// not found
|
|
1311
|
+
} else {
|
|
1312
|
+
let error = new Error('ElasticSearch request failed');
|
|
1313
|
+
error.info = {
|
|
1314
|
+
response: (err.meta && err.meta.body) || err.message,
|
|
1315
|
+
statusCode: err.meta && err.meta.statusCode
|
|
1316
|
+
};
|
|
1317
|
+
error.code = 'ESRequestError';
|
|
1318
|
+
error.statusCode = (err.meta && err.meta.statusCode) || 500;
|
|
1319
|
+
throw error;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
documentStoreUsed = true;
|
|
1323
|
+
} else {
|
|
1324
|
+
// Fetch from IMAP/API with retry logic for transient errors
|
|
1325
|
+
let extendedData = data.reference.inline || data.reference.forwardAttachments;
|
|
1326
|
+
|
|
1327
|
+
try {
|
|
1328
|
+
referencedMessage = await retryOnTransientError(
|
|
1329
|
+
async () => {
|
|
1330
|
+
return await this.getMessage(
|
|
1331
|
+
data.reference.message,
|
|
1332
|
+
{
|
|
1333
|
+
fields: !extendedData
|
|
1334
|
+
? {
|
|
1335
|
+
uid: true,
|
|
1336
|
+
flags: true,
|
|
1337
|
+
envelope: true,
|
|
1338
|
+
headers: ['references']
|
|
1339
|
+
}
|
|
1340
|
+
: false,
|
|
1341
|
+
header: extendedData ? true : false,
|
|
1342
|
+
textType: extendedData ? '*' : false
|
|
1343
|
+
},
|
|
1344
|
+
connectionOptions
|
|
1345
|
+
);
|
|
1346
|
+
},
|
|
1347
|
+
{
|
|
1348
|
+
maxAttempts: 3,
|
|
1349
|
+
baseDelay: 1000,
|
|
1350
|
+
logger: this.logger
|
|
1351
|
+
}
|
|
1352
|
+
);
|
|
1353
|
+
} catch (err) {
|
|
1354
|
+
// Check if this is a transient error that exhausted retries
|
|
1355
|
+
const isTransient = isTransientError(err);
|
|
1356
|
+
|
|
1357
|
+
if (isTransient && data.reference.ignoreMissing) {
|
|
1358
|
+
// Transient error but ignoreMissing is true - log and continue without reference
|
|
1359
|
+
this.logger.warn({
|
|
1360
|
+
msg: 'Failed to fetch referenced message due to transient error, continuing without reference',
|
|
1361
|
+
messageId: data.reference.message,
|
|
1362
|
+
err: err.message,
|
|
1363
|
+
statusCode: err.statusCode || (err.oauthRequest && err.oauthRequest.status)
|
|
1364
|
+
});
|
|
1365
|
+
referencedMessage = null;
|
|
1366
|
+
} else if (!isTransient && err.statusCode === 404) {
|
|
1367
|
+
// Permanent 404 error - respect ignoreMissing
|
|
1368
|
+
if (!data.reference.ignoreMissing) {
|
|
1369
|
+
let error = new Error('Referenced message was not found');
|
|
1370
|
+
error.code = 'MessageNotFound';
|
|
1371
|
+
error.statusCode = 404;
|
|
1372
|
+
throw error;
|
|
1373
|
+
}
|
|
1374
|
+
referencedMessage = null;
|
|
1375
|
+
} else {
|
|
1376
|
+
// Either transient error with ignoreMissing=false, or other permanent error
|
|
1377
|
+
throw err;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
if (!referencedMessage && !data.reference.ignoreMissing) {
|
|
1383
|
+
let err = new Error('Referenced message was not found');
|
|
1384
|
+
err.code = 'MessageNotFound';
|
|
1385
|
+
err.statusCode = 404;
|
|
1386
|
+
throw err;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
if (referencedMessage) {
|
|
1390
|
+
// Verify Message-ID if specified
|
|
1391
|
+
if (data.reference.messageId && data.reference.messageId !== referencedMessage.messageId) {
|
|
1392
|
+
let err = new Error('The referenced message was found, but its Message-ID does not match the expected value');
|
|
1393
|
+
err.code = 'MessageNotFound';
|
|
1394
|
+
err.statusCode = 404;
|
|
1395
|
+
throw err;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Build references header chain
|
|
1399
|
+
let references = []
|
|
1400
|
+
.concat(referencedMessage.messageId || [])
|
|
1401
|
+
.concat(referencedMessage.inReplyTo || [])
|
|
1402
|
+
.concat((referencedMessage.headers && referencedMessage.headers.references) || [])
|
|
1403
|
+
.flatMap(line => line.split(/\s+/))
|
|
1404
|
+
.map(ref => ref.trim())
|
|
1405
|
+
.filter(ref => ref)
|
|
1406
|
+
.map(ref => {
|
|
1407
|
+
// Ensure proper Message-ID format
|
|
1408
|
+
if (!/^</.test(ref)) {
|
|
1409
|
+
ref = '<' + ref;
|
|
1410
|
+
}
|
|
1411
|
+
if (!/>$/.test(ref)) {
|
|
1412
|
+
ref = ref + '>';
|
|
1413
|
+
}
|
|
1414
|
+
return ref;
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
references = Array.from(new Set(references));
|
|
1418
|
+
if (references.length) {
|
|
1419
|
+
if (!data.headers) {
|
|
1420
|
+
data.headers = {};
|
|
1421
|
+
}
|
|
1422
|
+
data.headers.references = references.join(' ');
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Set In-Reply-To header for replies
|
|
1426
|
+
if (['reply', 'reply-all'].includes(data.reference.action) && referencedMessage.messageId) {
|
|
1427
|
+
data.headers['in-reply-to'] = referencedMessage.messageId;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// Generate subject with appropriate prefix
|
|
1431
|
+
if (!data.subject && referencedMessage.subject) {
|
|
1432
|
+
let subject = referencedMessage.subject;
|
|
1433
|
+
let prefix;
|
|
1434
|
+
switch (data.reference.action) {
|
|
1435
|
+
case 'reply':
|
|
1436
|
+
case 'reply-all':
|
|
1437
|
+
if (!/^Re:/i.test(subject)) {
|
|
1438
|
+
prefix = 'Re';
|
|
1439
|
+
}
|
|
1440
|
+
break;
|
|
1441
|
+
case 'forward':
|
|
1442
|
+
if (!/^Fwd:/i.test(subject)) {
|
|
1443
|
+
prefix = 'Fwd';
|
|
1444
|
+
}
|
|
1445
|
+
break;
|
|
1446
|
+
}
|
|
1447
|
+
data.subject = `${prefix ? prefix + ': ' : ''}${subject}`;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
let cidAttachments = [];
|
|
1451
|
+
|
|
1452
|
+
// Process inline content for replies/forwards
|
|
1453
|
+
if (data.reference.inline) {
|
|
1454
|
+
let inlineMessageData = {
|
|
1455
|
+
text: referencedMessage.text && referencedMessage.text.plain,
|
|
1456
|
+
html: referencedMessage.text && referencedMessage.text.html
|
|
1457
|
+
};
|
|
1458
|
+
|
|
1459
|
+
for (let key of ['from', 'to', 'cc', 'bcc', 'date', 'subject']) {
|
|
1460
|
+
inlineMessageData[key] = referencedMessage[key];
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
if (inlineMessageData.html) {
|
|
1464
|
+
// Find inline attachments referenced in HTML
|
|
1465
|
+
if (referencedMessage.attachments) {
|
|
1466
|
+
// find all attachments that are referenced in the HTML
|
|
1467
|
+
for (let attachment of referencedMessage.attachments) {
|
|
1468
|
+
if (attachment.contentId && inlineMessageData.html.indexOf(`cid:${attachment.contentId.replace(/^<|>$/g, '')}`) >= 0) {
|
|
1469
|
+
cidAttachments.push(attachment);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
try {
|
|
1475
|
+
let html = data.html;
|
|
1476
|
+
if (!(html || '').toString().trim() && data.text) {
|
|
1477
|
+
html = textToHtml(data.text); // convert text to html
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
data.html = inlineHtml(data.reference.action, html, inlineMessageData, inlineOptions);
|
|
1481
|
+
} catch (err) {
|
|
1482
|
+
this.logger.error({ msg: 'Failed to generate inline HTML content', err });
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
if (data.text) {
|
|
1487
|
+
try {
|
|
1488
|
+
data.text = inlineText(data.reference.action, data.text, inlineMessageData, inlineOptions);
|
|
1489
|
+
} catch (err) {
|
|
1490
|
+
this.logger.error({ msg: 'Failed to generate inline text content', err });
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// Set recipient for reply
|
|
1496
|
+
if (!data.to && data.reference.action === 'reply') {
|
|
1497
|
+
data.to =
|
|
1498
|
+
referencedMessage.replyTo && referencedMessage.replyTo.length
|
|
1499
|
+
? referencedMessage.replyTo
|
|
1500
|
+
: referencedMessage.from
|
|
1501
|
+
? referencedMessage.from
|
|
1502
|
+
: false;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// Handle reply-all recipients
|
|
1506
|
+
if (data.reference.action === 'reply-all') {
|
|
1507
|
+
let envelope = {
|
|
1508
|
+
to: [].concat(
|
|
1509
|
+
referencedMessage.replyTo && referencedMessage.replyTo.length
|
|
1510
|
+
? referencedMessage.replyTo
|
|
1511
|
+
: referencedMessage.from
|
|
1512
|
+
? referencedMessage.from
|
|
1513
|
+
: []
|
|
1514
|
+
),
|
|
1515
|
+
cc: [],
|
|
1516
|
+
bcc: []
|
|
1517
|
+
};
|
|
1518
|
+
|
|
1519
|
+
// Deduplicate recipients
|
|
1520
|
+
let addressesSeen = new Set([].concat(data.from?.address || []).concat(envelope.to.map(addr => addr.address)));
|
|
1521
|
+
for (let rcpt of ['to', 'cc', 'bcc']) {
|
|
1522
|
+
for (let addr of referencedMessage[rcpt] || []) {
|
|
1523
|
+
if (addressesSeen.has(addr.address)) {
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
addressesSeen.add(addr.address);
|
|
1527
|
+
envelope[rcpt].push(addr);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
for (let addr of data[rcpt] || []) {
|
|
1531
|
+
if (addressesSeen.has(addr.address)) {
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
addressesSeen.add(addr.address);
|
|
1535
|
+
envelope[rcpt].push(addr);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
if (envelope[rcpt].length) {
|
|
1539
|
+
data[rcpt] = envelope[rcpt];
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// Determine which attachments to include
|
|
1545
|
+
let attachmentsToDownload;
|
|
1546
|
+
|
|
1547
|
+
if (
|
|
1548
|
+
data.reference.action === 'forward' &&
|
|
1549
|
+
data.reference.forwardAttachments &&
|
|
1550
|
+
referencedMessage.attachments &&
|
|
1551
|
+
referencedMessage.attachments.length
|
|
1552
|
+
) {
|
|
1553
|
+
// download all
|
|
1554
|
+
attachmentsToDownload = referencedMessage.attachments;
|
|
1555
|
+
} else if (cidAttachments.length) {
|
|
1556
|
+
// download referenced attachments
|
|
1557
|
+
attachmentsToDownload = cidAttachments;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// Download and attach referenced attachments
|
|
1561
|
+
if (attachmentsToDownload && attachmentsToDownload.length) {
|
|
1562
|
+
this.checkIMAPConnection(connectionOptions);
|
|
1563
|
+
|
|
1564
|
+
this.logger.info({
|
|
1565
|
+
msg: 'Fetching attachments from the referenced email',
|
|
1566
|
+
attachments: attachmentsToDownload.map(a => ({ id: a.id, hasContent: !!a.content }))
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
// fetch and add attachments to the message
|
|
1570
|
+
if (!data.attachments) {
|
|
1571
|
+
data.attachments = [];
|
|
1572
|
+
}
|
|
1573
|
+
for (let attachment of attachmentsToDownload) {
|
|
1574
|
+
let content;
|
|
1575
|
+
if (attachment.content) {
|
|
1576
|
+
// use local cache
|
|
1577
|
+
content = Buffer.from(attachment.content, 'base64');
|
|
1578
|
+
this.logger.trace({ msg: 'Using cached email content', attachment: attachment.id, size: content.length });
|
|
1579
|
+
} else {
|
|
1580
|
+
// fetch from IMAP
|
|
1581
|
+
content = await this.getAttachmentContent(
|
|
1582
|
+
attachment.id,
|
|
1583
|
+
{
|
|
1584
|
+
chunkSize: Math.max(DOWNLOAD_CHUNK_SIZE, 2 * 1024 * 1024),
|
|
1585
|
+
contentOnly: true
|
|
1586
|
+
},
|
|
1587
|
+
connectionOptions
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
if (!content) {
|
|
1591
|
+
// skip missing?
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
data.attachments.push({
|
|
1595
|
+
filename: attachment.filename,
|
|
1596
|
+
content,
|
|
1597
|
+
contentType: attachment.contentType,
|
|
1598
|
+
contentDisposition: attachment.inline ? 'inline' : 'attachment',
|
|
1599
|
+
cid: attachment.contentId || null
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// resolve referenced attachments
|
|
1607
|
+
for (let attachment of data.attachments || []) {
|
|
1608
|
+
if (attachment.reference && !attachment.content) {
|
|
1609
|
+
this.checkIMAPConnection(connectionOptions);
|
|
1610
|
+
|
|
1611
|
+
let content = await this.getAttachmentContent(
|
|
1612
|
+
attachment.reference,
|
|
1613
|
+
{
|
|
1614
|
+
chunkSize: Math.max(DOWNLOAD_CHUNK_SIZE, 2 * 1024 * 1024),
|
|
1615
|
+
contentOnly: true
|
|
1616
|
+
},
|
|
1617
|
+
connectionOptions
|
|
1618
|
+
);
|
|
1619
|
+
if (!content) {
|
|
1620
|
+
let error = new Error('Referenced attachment was not found');
|
|
1621
|
+
error.code = 'ReferenceNotFound';
|
|
1622
|
+
error.statusCode = 404;
|
|
1623
|
+
throw error;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
attachment.content = content;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// Final message configuration
|
|
1631
|
+
data.disableUrlAccess = true;
|
|
1632
|
+
data.disableFileAccess = true;
|
|
1633
|
+
data.boundaryPrefix = MIME_BOUNDARY_PREFIX;
|
|
1634
|
+
data.baseBoundary = genBaseBoundary();
|
|
1635
|
+
data.newline = '\r\n';
|
|
1636
|
+
|
|
1637
|
+
if (data.internalDate && !data.date) {
|
|
1638
|
+
// Update Date: header as well
|
|
1639
|
+
data.date = new Date(data.internalDate);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// Generate raw MIME message
|
|
1643
|
+
let { raw, emailObject, messageId } = await getRawEmail(data, null, options);
|
|
1644
|
+
|
|
1645
|
+
return { raw, emailObject, messageId, documentStoreUsed, referencedMessage };
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/**
|
|
1649
|
+
* Processes a single message for queuing
|
|
1650
|
+
* Handles templates, references, tracking, and scheduling
|
|
1651
|
+
* @param {Object} data - Message data
|
|
1652
|
+
* @param {Object} meta - Metadata
|
|
1653
|
+
* @param {Object} licenseInfo - License information
|
|
1654
|
+
* @param {Object} connectionOptions - Connection options
|
|
1655
|
+
* @returns {Object} Queue result
|
|
1656
|
+
*/
|
|
1657
|
+
async queueMessageEntry(data, meta, licenseInfo, connectionOptions) {
|
|
1658
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
1659
|
+
|
|
1660
|
+
// Configure message defaults
|
|
1661
|
+
data.disableFileAccess = true;
|
|
1662
|
+
data.disableUrlAccess = true;
|
|
1663
|
+
data.boundaryPrefix = MIME_BOUNDARY_PREFIX;
|
|
1664
|
+
data.baseBoundary = genBaseBoundary();
|
|
1665
|
+
|
|
1666
|
+
let baseUrl = data.baseUrl || (await settings.get('serviceUrl')) || null;
|
|
1667
|
+
|
|
1668
|
+
// Build template context
|
|
1669
|
+
let context = {
|
|
1670
|
+
params: (data.render && data.render.params) || {},
|
|
1671
|
+
account: {
|
|
1672
|
+
name: accountData.name,
|
|
1673
|
+
email: accountData.email
|
|
1674
|
+
},
|
|
1675
|
+
service: {
|
|
1676
|
+
url: baseUrl
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
|
|
1680
|
+
// Set default from address
|
|
1681
|
+
if (!data.from) {
|
|
1682
|
+
data.from = {
|
|
1683
|
+
name: accountData.name,
|
|
1684
|
+
address: accountData.email,
|
|
1685
|
+
_default: true
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// Handle list unsubscribe headers
|
|
1690
|
+
if (baseUrl && data.listId && data.to && data.to.length === 1 && data.to[0].address) {
|
|
1691
|
+
// check if not already blocked
|
|
1692
|
+
|
|
1693
|
+
let blockData;
|
|
1694
|
+
|
|
1695
|
+
try {
|
|
1696
|
+
blockData = await this.redis.hget(`${REDIS_PREFIX}lists:unsub:entries:${data.listId}`, data.to[0].address.toLowerCase().trim());
|
|
1697
|
+
blockData = JSON.parse(blockData);
|
|
1698
|
+
} catch (err) {
|
|
1699
|
+
blockData = false;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// Skip if recipient has unsubscribed
|
|
1703
|
+
if (blockData) {
|
|
1704
|
+
return {
|
|
1705
|
+
responseValue: {
|
|
1706
|
+
skipped: {
|
|
1707
|
+
reason: blockData.reason,
|
|
1708
|
+
listId: data.listId
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
if (!data.headers) {
|
|
1715
|
+
data.headers = {};
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// Generate List headers
|
|
1719
|
+
let baseDomain;
|
|
1720
|
+
try {
|
|
1721
|
+
baseDomain = (new URL(baseUrl).hostname || '').toLowerCase().trim();
|
|
1722
|
+
} catch (err) {
|
|
1723
|
+
// ignore error
|
|
1724
|
+
}
|
|
1725
|
+
baseDomain = baseDomain || (os.hostname() || '').toLowerCase().trim() || 'localhost';
|
|
1726
|
+
|
|
1727
|
+
if (baseDomain) {
|
|
1728
|
+
let unsubscribeUrlObj = new URL('/unsubscribe', baseUrl);
|
|
1729
|
+
|
|
1730
|
+
const serviceSecret = await getServiceSecret();
|
|
1731
|
+
|
|
1732
|
+
let fromDomain = ((data.from && data.from.address) || '').split('@').pop().trim().toLowerCase() || baseDomain;
|
|
1733
|
+
try {
|
|
1734
|
+
fromDomain = punycode.toASCII(fromDomain);
|
|
1735
|
+
} catch (err) {
|
|
1736
|
+
// ignore
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
data.headers['List-ID'] = `<${data.listId}.${baseDomain}>`;
|
|
1740
|
+
|
|
1741
|
+
if (!data.messageId) {
|
|
1742
|
+
data.messageId = `<${uuid()}@${fromDomain}>`;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// Create signed unsubscribe URL
|
|
1746
|
+
let { data: signedData, signature } = getSignedFormDataSync(
|
|
1747
|
+
serviceSecret,
|
|
1748
|
+
{
|
|
1749
|
+
act: 'unsub',
|
|
1750
|
+
acc: accountData.account,
|
|
1751
|
+
list: data.listId,
|
|
1752
|
+
rcpt: data.to[0].address,
|
|
1753
|
+
msg: data.messageId
|
|
1754
|
+
},
|
|
1755
|
+
true
|
|
1756
|
+
);
|
|
1757
|
+
|
|
1758
|
+
unsubscribeUrlObj.searchParams.append('data', signedData);
|
|
1759
|
+
if (signature) {
|
|
1760
|
+
unsubscribeUrlObj.searchParams.append('sig', signature);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
context.rcpt = Object.assign({}, data.to[0], { unsubscribeUrl: unsubscribeUrlObj.href });
|
|
1764
|
+
|
|
1765
|
+
// Add RFC 8058 compliant unsubscribe headers
|
|
1766
|
+
data.headers['List-Unsubscribe'] = `<${context.rcpt.unsubscribeUrl}>`;
|
|
1767
|
+
data.headers['List-Unsubscribe-Post'] = 'List-Unsubscribe=One-Click';
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// Render templates
|
|
1772
|
+
for (let key of ['subject', 'html', 'text', 'previewText']) {
|
|
1773
|
+
if (data.render || data.listId) {
|
|
1774
|
+
data[key] = this.render(data[key], context, key, data.render && data.render.format);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
delete data.render;
|
|
1779
|
+
|
|
1780
|
+
// Configure locale and timezone for inline content
|
|
1781
|
+
let inlineOptions = {
|
|
1782
|
+
locale: data.locale || accountData.locale || (await settings.get('locale')),
|
|
1783
|
+
tz: data.tz || accountData.tz || (await settings.get('timezone'))
|
|
1784
|
+
};
|
|
1785
|
+
|
|
1786
|
+
delete data.locale;
|
|
1787
|
+
delete data.tz;
|
|
1788
|
+
|
|
1789
|
+
// Generate plain text from HTML if needed
|
|
1790
|
+
if (data.html && !data.text) {
|
|
1791
|
+
try {
|
|
1792
|
+
data.text = htmlToText(data.html);
|
|
1793
|
+
} catch (err) {
|
|
1794
|
+
this.logger.error({ msg: 'Failed to generate plaintext content from html', err });
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
let referencedMessage;
|
|
1799
|
+
let documentStoreUsed = false;
|
|
1800
|
+
|
|
1801
|
+
// Resolve reference and update reference/in-reply-to headers
|
|
1802
|
+
if (data.reference && data.reference.message) {
|
|
1803
|
+
// Try document store first if enabled
|
|
1804
|
+
if (data.reference.documentStore && (await settings.get('documentStoreEnabled'))) {
|
|
1805
|
+
try {
|
|
1806
|
+
referencedMessage = await this.accountObject.getMessage(data.reference.message, {
|
|
1807
|
+
documentStore: true,
|
|
1808
|
+
textType: '*'
|
|
1809
|
+
});
|
|
1810
|
+
} catch (err) {
|
|
1811
|
+
if (err.meta && err.meta.statusCode === 404) {
|
|
1812
|
+
// not found
|
|
1813
|
+
} else {
|
|
1814
|
+
let error = new Error('ElasticSearch request failed');
|
|
1815
|
+
error.info = {
|
|
1816
|
+
response: (err.meta && err.meta.body) || err.message,
|
|
1817
|
+
statusCode: err.meta && err.meta.statusCode
|
|
1818
|
+
};
|
|
1819
|
+
error.code = 'ESRequestError';
|
|
1820
|
+
error.statusCode = (err.meta && err.meta.statusCode) || 500;
|
|
1821
|
+
throw error;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
documentStoreUsed = true;
|
|
1825
|
+
} else {
|
|
1826
|
+
// Fetch from IMAP/API with retry logic for transient errors
|
|
1827
|
+
this.checkIMAPConnection(connectionOptions);
|
|
1828
|
+
|
|
1829
|
+
let extendedData = data.reference.inline || data.reference.forwardAttachments;
|
|
1830
|
+
|
|
1831
|
+
try {
|
|
1832
|
+
referencedMessage = await retryOnTransientError(
|
|
1833
|
+
async () => {
|
|
1834
|
+
return await this.getMessage(
|
|
1835
|
+
data.reference.message,
|
|
1836
|
+
{
|
|
1837
|
+
fields: !extendedData
|
|
1838
|
+
? {
|
|
1839
|
+
uid: true,
|
|
1840
|
+
flags: true,
|
|
1841
|
+
envelope: true,
|
|
1842
|
+
headers: ['references']
|
|
1843
|
+
}
|
|
1844
|
+
: false,
|
|
1845
|
+
header: extendedData ? true : false,
|
|
1846
|
+
textType: extendedData ? '*' : false
|
|
1847
|
+
},
|
|
1848
|
+
connectionOptions
|
|
1849
|
+
);
|
|
1850
|
+
},
|
|
1851
|
+
{
|
|
1852
|
+
maxAttempts: 3,
|
|
1853
|
+
baseDelay: 1000,
|
|
1854
|
+
logger: this.logger
|
|
1855
|
+
}
|
|
1856
|
+
);
|
|
1857
|
+
} catch (err) {
|
|
1858
|
+
// Check if this is a transient error that exhausted retries
|
|
1859
|
+
const isTransient = isTransientError(err);
|
|
1860
|
+
|
|
1861
|
+
if (isTransient && data.reference.ignoreMissing) {
|
|
1862
|
+
// Transient error but ignoreMissing is true - log and continue without reference
|
|
1863
|
+
this.logger.warn({
|
|
1864
|
+
msg: 'Failed to fetch referenced message due to transient error, continuing without reference',
|
|
1865
|
+
messageId: data.reference.message,
|
|
1866
|
+
err: err.message,
|
|
1867
|
+
statusCode: err.statusCode || (err.oauthRequest && err.oauthRequest.status)
|
|
1868
|
+
});
|
|
1869
|
+
referencedMessage = null;
|
|
1870
|
+
} else if (!isTransient && err.statusCode === 404) {
|
|
1871
|
+
// Permanent 404 error - respect ignoreMissing
|
|
1872
|
+
if (!data.reference.ignoreMissing) {
|
|
1873
|
+
let error = new Error('Referenced message was not found');
|
|
1874
|
+
error.code = 'ReferenceNotFound';
|
|
1875
|
+
error.statusCode = 404;
|
|
1876
|
+
throw error;
|
|
1877
|
+
}
|
|
1878
|
+
referencedMessage = null;
|
|
1879
|
+
} else {
|
|
1880
|
+
// Either transient error with ignoreMissing=false, or other permanent error
|
|
1881
|
+
throw err;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
if (!referencedMessage && !data.reference.ignoreMissing) {
|
|
1887
|
+
let error = new Error('Referenced message was not found');
|
|
1888
|
+
error.code = 'ReferenceNotFound';
|
|
1889
|
+
error.statusCode = 404;
|
|
1890
|
+
throw error;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
if (referencedMessage) {
|
|
1894
|
+
// Verify Message-ID if specified
|
|
1895
|
+
if (data.reference.messageId && data.reference.messageId !== referencedMessage.messageId) {
|
|
1896
|
+
let err = new Error('The referenced message was found, but its Message-ID does not match the expected value');
|
|
1897
|
+
err.code = 'MessageNotFound';
|
|
1898
|
+
err.statusCode = 404;
|
|
1899
|
+
throw err;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
// Build references header chain
|
|
1903
|
+
let references = []
|
|
1904
|
+
.concat(referencedMessage.messageId || [])
|
|
1905
|
+
.concat(referencedMessage.inReplyTo || [])
|
|
1906
|
+
.concat((referencedMessage.headers && referencedMessage.headers.references) || [])
|
|
1907
|
+
.flatMap(line => line.split(/\s+/))
|
|
1908
|
+
.map(ref => ref.trim())
|
|
1909
|
+
.filter(ref => ref)
|
|
1910
|
+
.map(ref => {
|
|
1911
|
+
// Ensure proper Message-ID format
|
|
1912
|
+
if (!/^</.test(ref)) {
|
|
1913
|
+
ref = '<' + ref;
|
|
1914
|
+
}
|
|
1915
|
+
if (!/>$/.test(ref)) {
|
|
1916
|
+
ref = ref + '>';
|
|
1917
|
+
}
|
|
1918
|
+
return ref;
|
|
1919
|
+
});
|
|
1920
|
+
|
|
1921
|
+
references = Array.from(new Set(references));
|
|
1922
|
+
if (references.length) {
|
|
1923
|
+
if (!data.headers) {
|
|
1924
|
+
data.headers = {};
|
|
1925
|
+
}
|
|
1926
|
+
data.headers.references = references.join(' ');
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
// Set In-Reply-To header for replies
|
|
1930
|
+
if (['reply', 'reply-all'].includes(data.reference.action) && referencedMessage.messageId) {
|
|
1931
|
+
data.headers['in-reply-to'] = referencedMessage.messageId;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// Check if we need to update flags on referenced message
|
|
1935
|
+
let referenceFlags = ['\\Answered'].concat(data.reference.action === 'forward' ? '$Forwarded' : []);
|
|
1936
|
+
if (!referencedMessage.flags || !referencedMessage.flags.length || !referenceFlags.some(flag => referencedMessage.flags.includes(flag))) {
|
|
1937
|
+
data.reference.update = true;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// Preserve thread ID
|
|
1941
|
+
if (referencedMessage.threadId) {
|
|
1942
|
+
data.reference.threadId = referencedMessage.threadId;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// Generate subject with appropriate prefix
|
|
1946
|
+
if (!data.subject && referencedMessage.subject) {
|
|
1947
|
+
let subject = referencedMessage.subject;
|
|
1948
|
+
let prefix;
|
|
1949
|
+
switch (data.reference.action) {
|
|
1950
|
+
case 'reply':
|
|
1951
|
+
case 'reply-all':
|
|
1952
|
+
if (!/^Re:/i.test(subject)) {
|
|
1953
|
+
prefix = 'Re';
|
|
1954
|
+
}
|
|
1955
|
+
break;
|
|
1956
|
+
case 'forward':
|
|
1957
|
+
if (!/^Fwd:/i.test(subject)) {
|
|
1958
|
+
prefix = 'Fwd';
|
|
1959
|
+
}
|
|
1960
|
+
break;
|
|
1961
|
+
}
|
|
1962
|
+
data.subject = `${prefix ? prefix + ': ' : ''}${subject}`;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
let cidAttachments = [];
|
|
1966
|
+
|
|
1967
|
+
// Process inline content for replies/forwards
|
|
1968
|
+
if (data.reference.inline) {
|
|
1969
|
+
let inlineMessageData = {
|
|
1970
|
+
text: referencedMessage.text && referencedMessage.text.plain,
|
|
1971
|
+
html: referencedMessage.text && referencedMessage.text.html
|
|
1972
|
+
};
|
|
1973
|
+
|
|
1974
|
+
for (let key of ['from', 'to', 'cc', 'bcc', 'date', 'subject']) {
|
|
1975
|
+
inlineMessageData[key] = referencedMessage[key];
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
if (inlineMessageData.html) {
|
|
1979
|
+
// Find inline attachments referenced in HTML
|
|
1980
|
+
if (referencedMessage.attachments) {
|
|
1981
|
+
// find all attachments that are referenced in the HTML
|
|
1982
|
+
for (let attachment of referencedMessage.attachments) {
|
|
1983
|
+
if (attachment.contentId && inlineMessageData.html.indexOf(`cid:${attachment.contentId.replace(/^<|>$/g, '')}`) >= 0) {
|
|
1984
|
+
cidAttachments.push(attachment);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
try {
|
|
1990
|
+
let html = data.html;
|
|
1991
|
+
if (!(html || '').toString().trim() && data.text) {
|
|
1992
|
+
html = textToHtml(data.text); // convert text to html
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
data.html = inlineHtml(data.reference.action, html, inlineMessageData, inlineOptions);
|
|
1996
|
+
} catch (err) {
|
|
1997
|
+
this.logger.error({ msg: 'Failed to generate inline HTML content', err });
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
if (data.text) {
|
|
2002
|
+
try {
|
|
2003
|
+
data.text = inlineText(data.reference.action, data.text, inlineMessageData, inlineOptions);
|
|
2004
|
+
} catch (err) {
|
|
2005
|
+
this.logger.error({ msg: 'Failed to generate inline text content', err });
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
// Set recipient for reply
|
|
2011
|
+
if (!data.to && data.reference.action === 'reply') {
|
|
2012
|
+
data.to =
|
|
2013
|
+
referencedMessage.replyTo && referencedMessage.replyTo.length
|
|
2014
|
+
? referencedMessage.replyTo
|
|
2015
|
+
: referencedMessage.from
|
|
2016
|
+
? referencedMessage.from
|
|
2017
|
+
: false;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// Handle reply-all recipients
|
|
2021
|
+
if (data.reference.action === 'reply-all') {
|
|
2022
|
+
let envelope = {
|
|
2023
|
+
to: [].concat(
|
|
2024
|
+
referencedMessage.replyTo && referencedMessage.replyTo.length
|
|
2025
|
+
? referencedMessage.replyTo
|
|
2026
|
+
: referencedMessage.from
|
|
2027
|
+
? referencedMessage.from
|
|
2028
|
+
: []
|
|
2029
|
+
),
|
|
2030
|
+
cc: [],
|
|
2031
|
+
bcc: []
|
|
2032
|
+
};
|
|
2033
|
+
|
|
2034
|
+
// Deduplicate recipients
|
|
2035
|
+
let addressesSeen = new Set([].concat(data.from?.address || []).concat(envelope.to.map(addr => addr.address)));
|
|
2036
|
+
for (let rcpt of ['to', 'cc', 'bcc']) {
|
|
2037
|
+
for (let addr of referencedMessage[rcpt] || []) {
|
|
2038
|
+
if (addressesSeen.has(addr.address)) {
|
|
2039
|
+
continue;
|
|
2040
|
+
}
|
|
2041
|
+
addressesSeen.add(addr.address);
|
|
2042
|
+
envelope[rcpt].push(addr);
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
for (let addr of data[rcpt] || []) {
|
|
2046
|
+
if (addressesSeen.has(addr.address)) {
|
|
2047
|
+
continue;
|
|
2048
|
+
}
|
|
2049
|
+
addressesSeen.add(addr.address);
|
|
2050
|
+
envelope[rcpt].push(addr);
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
if (envelope[rcpt].length) {
|
|
2054
|
+
data[rcpt] = envelope[rcpt];
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// Determine which attachments to include
|
|
2060
|
+
let attachmentsToDownload;
|
|
2061
|
+
|
|
2062
|
+
if (
|
|
2063
|
+
data.reference.action === 'forward' &&
|
|
2064
|
+
data.reference.forwardAttachments &&
|
|
2065
|
+
referencedMessage.attachments &&
|
|
2066
|
+
referencedMessage.attachments.length
|
|
2067
|
+
) {
|
|
2068
|
+
// download all
|
|
2069
|
+
attachmentsToDownload = referencedMessage.attachments;
|
|
2070
|
+
} else if (cidAttachments.length) {
|
|
2071
|
+
// download referenced attachments
|
|
2072
|
+
attachmentsToDownload = cidAttachments;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// Download and attach referenced attachments
|
|
2076
|
+
if (attachmentsToDownload && attachmentsToDownload.length) {
|
|
2077
|
+
this.checkIMAPConnection(connectionOptions);
|
|
2078
|
+
|
|
2079
|
+
this.logger.info({
|
|
2080
|
+
msg: 'Fetching attachments from the referenced email',
|
|
2081
|
+
attachments: attachmentsToDownload.map(a => ({ id: a.id, hasContent: !!a.content }))
|
|
2082
|
+
});
|
|
2083
|
+
|
|
2084
|
+
// fetch and add attachments to the message
|
|
2085
|
+
if (!data.attachments) {
|
|
2086
|
+
data.attachments = [];
|
|
2087
|
+
}
|
|
2088
|
+
for (let attachment of attachmentsToDownload) {
|
|
2089
|
+
let content;
|
|
2090
|
+
if (attachment.content) {
|
|
2091
|
+
// use local cache
|
|
2092
|
+
content = Buffer.from(attachment.content, 'base64');
|
|
2093
|
+
this.logger.trace({ msg: 'Using cached email content', attachment: attachment.id, size: content.length });
|
|
2094
|
+
} else {
|
|
2095
|
+
// fetch from IMAP
|
|
2096
|
+
content = await this.getAttachmentContent(
|
|
2097
|
+
attachment.id,
|
|
2098
|
+
{
|
|
2099
|
+
chunkSize: Math.max(DOWNLOAD_CHUNK_SIZE, 2 * 1024 * 1024),
|
|
2100
|
+
contentOnly: true
|
|
2101
|
+
},
|
|
2102
|
+
connectionOptions
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
if (!content) {
|
|
2106
|
+
// skip missing?
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
data.attachments.push({
|
|
2110
|
+
filename: attachment.filename,
|
|
2111
|
+
content,
|
|
2112
|
+
contentType: attachment.contentType,
|
|
2113
|
+
contentDisposition: attachment.inline ? 'inline' : 'attachment',
|
|
2114
|
+
cid: attachment.contentId || null
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// resolve referenced attachments by ID
|
|
2122
|
+
for (let attachment of data.attachments || []) {
|
|
2123
|
+
if (attachment.reference && !attachment.content) {
|
|
2124
|
+
this.checkIMAPConnection(connectionOptions);
|
|
2125
|
+
|
|
2126
|
+
let content = await this.getAttachmentContent(
|
|
2127
|
+
attachment.reference,
|
|
2128
|
+
{
|
|
2129
|
+
chunkSize: Math.max(DOWNLOAD_CHUNK_SIZE, 2 * 1024 * 1024),
|
|
2130
|
+
contentOnly: true
|
|
2131
|
+
},
|
|
2132
|
+
connectionOptions
|
|
2133
|
+
);
|
|
2134
|
+
|
|
2135
|
+
if (!content) {
|
|
2136
|
+
let error = new Error('Referenced attachment was not found');
|
|
2137
|
+
error.code = 'ReferenceNotFound';
|
|
2138
|
+
error.statusCode = 404;
|
|
2139
|
+
throw error;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
attachment.content = content;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// Generate raw MIME message
|
|
2147
|
+
let { raw, hasBcc, envelope, subject, messageId, sendAt, deliveryAttempts, trackClicks, trackOpens, trackingEnabled, gateway } = await getRawEmail(
|
|
2148
|
+
data,
|
|
2149
|
+
licenseInfo
|
|
2150
|
+
);
|
|
2151
|
+
|
|
2152
|
+
// Handle dry run mode
|
|
2153
|
+
if (data.dryRun) {
|
|
2154
|
+
let response = {
|
|
2155
|
+
response: 'Dry run',
|
|
2156
|
+
messageId
|
|
2157
|
+
};
|
|
2158
|
+
|
|
2159
|
+
if (data.reference && data.reference.message) {
|
|
2160
|
+
response.reference = {
|
|
2161
|
+
message: data.reference.message,
|
|
2162
|
+
documentStore: documentStoreUsed,
|
|
2163
|
+
success: referencedMessage ? true : false
|
|
2164
|
+
};
|
|
2165
|
+
|
|
2166
|
+
if (!referencedMessage) {
|
|
2167
|
+
response.reference.error = 'Referenced message was not found';
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
response.preview = raw.toString('base64');
|
|
2172
|
+
|
|
2173
|
+
return response;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
// Load gateway configuration if specified
|
|
2177
|
+
let gatewayData;
|
|
2178
|
+
if (gateway) {
|
|
2179
|
+
let gatewayObject = new Gateway({ gateway, redis: this.redis, secret: this.secret });
|
|
2180
|
+
try {
|
|
2181
|
+
gatewayData = await gatewayObject.loadGatewayData();
|
|
2182
|
+
} catch (err) {
|
|
2183
|
+
this.logger.info({ msg: 'Failed to load gateway data', envelope, messageId, gateway, err });
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
// Verify SMTP capability
|
|
2188
|
+
if (!accountData.smtp && !accountData.oauth2 && !gatewayData) {
|
|
2189
|
+
// can not make connection
|
|
2190
|
+
let err = new Error('SMTP configuration not found');
|
|
2191
|
+
err.code = 'SMTPUnavailable';
|
|
2192
|
+
err.statusCode = 404;
|
|
2193
|
+
throw err;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
// Configure tracking settings
|
|
2197
|
+
if (typeof trackingEnabled !== 'boolean') {
|
|
2198
|
+
trackingEnabled = (await settings.get('trackSentMessages')) || false;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
if (typeof trackClicks !== 'boolean') {
|
|
2202
|
+
trackClicks = await settings.get('trackClicks');
|
|
2203
|
+
if (typeof trackClicks !== 'boolean') {
|
|
2204
|
+
trackClicks = trackingEnabled;
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
if (typeof trackOpens !== 'boolean') {
|
|
2209
|
+
trackOpens = await settings.get('trackOpens');
|
|
2210
|
+
if (typeof trackOpens !== 'boolean') {
|
|
2211
|
+
trackOpens = trackingEnabled;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// Add tracking pixels and link tracking
|
|
2216
|
+
if (raw && (trackClicks || trackOpens) && baseUrl) {
|
|
2217
|
+
// add open and click tracking
|
|
2218
|
+
raw = await addTrackers(raw, accountData.account, messageId, baseUrl, {
|
|
2219
|
+
trackClicks,
|
|
2220
|
+
trackOpens
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
let now = new Date();
|
|
2225
|
+
|
|
2226
|
+
//queue for later
|
|
2227
|
+
|
|
2228
|
+
// Use timestamp in the ID to make sure that jobs are ordered by send time
|
|
2229
|
+
let timeBuf = Buffer.allocUnsafe(8);
|
|
2230
|
+
|
|
2231
|
+
timeBuf.writeBigInt64BE(BigInt((sendAt && sendAt.getTime()) || Date.now()), 0);
|
|
2232
|
+
|
|
2233
|
+
let queueId = Buffer.concat([timeBuf.subarray(2), crypto.randomBytes(4)])
|
|
2234
|
+
.toString('hex')
|
|
2235
|
+
.substring(1); // first char is always 0
|
|
2236
|
+
|
|
2237
|
+
// Serialize message data for storage
|
|
2238
|
+
let msgEntry = msgpack.encode({
|
|
2239
|
+
queueId,
|
|
2240
|
+
gateway: gatewayData && gatewayData.gateway,
|
|
2241
|
+
hasBcc,
|
|
2242
|
+
envelope,
|
|
2243
|
+
messageId,
|
|
2244
|
+
reference: data.reference || {},
|
|
2245
|
+
sendAt: (sendAt && sendAt.getTime()) || false,
|
|
2246
|
+
created: now.getTime(),
|
|
2247
|
+
copy: data.copy,
|
|
2248
|
+
sentMailPath: data.sentMailPath,
|
|
2249
|
+
feedbackKey: data.feedbackKey || null,
|
|
2250
|
+
dsn: data.dsn || null,
|
|
2251
|
+
proxy: data.proxy || null,
|
|
2252
|
+
localAddress: data.localAddress || null,
|
|
2253
|
+
raw
|
|
2254
|
+
});
|
|
2255
|
+
|
|
2256
|
+
// Store message in Redis
|
|
2257
|
+
await this.redis.hsetBuffer(`${REDIS_PREFIX}iaq:${this.account}`, queueId, msgEntry);
|
|
2258
|
+
|
|
2259
|
+
let queueKeep = (await settings.get('queueKeep')) || true;
|
|
2260
|
+
|
|
2261
|
+
// Configure delivery retry settings
|
|
2262
|
+
let defaultDeliveryAttempts = await settings.get('deliveryAttempts');
|
|
2263
|
+
if (typeof defaultDeliveryAttempts !== 'number') {
|
|
2264
|
+
defaultDeliveryAttempts = DEFAULT_DELIVERY_ATTEMPTS;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
// Prepare job data for queue
|
|
2268
|
+
let jobData = Object.assign({}, meta || {}, {
|
|
2269
|
+
account: this.account,
|
|
2270
|
+
queueId,
|
|
2271
|
+
gateway: (gatewayData && gatewayData.gateway) || null,
|
|
2272
|
+
messageId,
|
|
2273
|
+
envelope,
|
|
2274
|
+
subject,
|
|
2275
|
+
proxy: data.proxy,
|
|
2276
|
+
localAddress: data.localAddress,
|
|
2277
|
+
created: now.getTime()
|
|
2278
|
+
});
|
|
2279
|
+
|
|
2280
|
+
// Configure queue job options
|
|
2281
|
+
let queueName = 'queued';
|
|
2282
|
+
let jobOpts = {
|
|
2283
|
+
jobId: queueId,
|
|
2284
|
+
removeOnComplete: queueKeep,
|
|
2285
|
+
removeOnFail: queueKeep,
|
|
2286
|
+
attempts: typeof deliveryAttempts === 'number' ? deliveryAttempts : defaultDeliveryAttempts,
|
|
2287
|
+
backoff: {
|
|
2288
|
+
type: 'exponential',
|
|
2289
|
+
delay: 5000
|
|
2290
|
+
}
|
|
2291
|
+
};
|
|
2292
|
+
|
|
2293
|
+
// Schedule delayed sending
|
|
2294
|
+
if (sendAt && sendAt > now) {
|
|
2295
|
+
queueName = 'delayed';
|
|
2296
|
+
jobOpts.delay = sendAt.getTime() - now.getTime();
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
// Add job to queue
|
|
2300
|
+
let job = await this.submitQueue.add(queueName, jobData, jobOpts);
|
|
2301
|
+
|
|
2302
|
+
try {
|
|
2303
|
+
await job.updateProgress({
|
|
2304
|
+
status: 'queued'
|
|
2305
|
+
});
|
|
2306
|
+
} catch (err) {
|
|
2307
|
+
// ignore
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
this.logger.info({ msg: 'Message queued for delivery', envelope, messageId, sendAt: (sendAt || now).toISOString(), queueId, job: job.id });
|
|
2311
|
+
|
|
2312
|
+
// Build response
|
|
2313
|
+
let response = {
|
|
2314
|
+
response: 'Queued for delivery',
|
|
2315
|
+
messageId,
|
|
2316
|
+
sendAt: (sendAt || now).toISOString(),
|
|
2317
|
+
queueId
|
|
2318
|
+
};
|
|
2319
|
+
|
|
2320
|
+
if (data.reference && data.reference.message) {
|
|
2321
|
+
response.reference = {
|
|
2322
|
+
message: data.reference.message,
|
|
2323
|
+
documentStore: documentStoreUsed,
|
|
2324
|
+
success: referencedMessage ? true : false
|
|
2325
|
+
};
|
|
2326
|
+
|
|
2327
|
+
if (!referencedMessage) {
|
|
2328
|
+
response.reference.error = 'Referenced message was not found';
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
return response;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
/**
|
|
2336
|
+
* Renders template strings with context data
|
|
2337
|
+
* @param {string} template - Template string
|
|
2338
|
+
* @param {Object} data - Context data
|
|
2339
|
+
* @param {string} key - Template key (subject, text, html, previewText)
|
|
2340
|
+
* @param {string} renderFormat - Override format for rendering
|
|
2341
|
+
* @returns {string} Rendered template
|
|
2342
|
+
*/
|
|
2343
|
+
render(template, data, key, renderFormat) {
|
|
2344
|
+
let format;
|
|
2345
|
+
|
|
2346
|
+
// Determine format based on key
|
|
2347
|
+
switch (key) {
|
|
2348
|
+
case 'subject':
|
|
2349
|
+
case 'text': {
|
|
2350
|
+
format = 'plain';
|
|
2351
|
+
break;
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
case 'html': {
|
|
2355
|
+
format = renderFormat ? renderFormat : 'html';
|
|
2356
|
+
break;
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
case 'previewText':
|
|
2360
|
+
default: {
|
|
2361
|
+
format = 'html';
|
|
2362
|
+
break;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
try {
|
|
2367
|
+
const compiledTemplate = getTemplate({
|
|
2368
|
+
format,
|
|
2369
|
+
template
|
|
2370
|
+
});
|
|
2371
|
+
|
|
2372
|
+
return compiledTemplate(data);
|
|
2373
|
+
} catch (err) {
|
|
2374
|
+
logger.error({ msg: `Failed rendering ${key} template`, err });
|
|
2375
|
+
let error = new Error(`Failed rendering ${key} template`);
|
|
2376
|
+
error.code = err.code || 'SubmitFail';
|
|
2377
|
+
error.statusCode = 422;
|
|
2378
|
+
throw error;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
/**
|
|
2383
|
+
* Detects if a message is an auto-reply based on headers and subject
|
|
2384
|
+
* @param {Object} messageData - Message data
|
|
2385
|
+
* @returns {boolean} True if message appears to be an auto-reply
|
|
2386
|
+
*/
|
|
2387
|
+
isAutoreply(messageData) {
|
|
2388
|
+
// Check subject patterns
|
|
2389
|
+
if (/^(auto:|Out of Office|OOF:|OOO:)/i.test(messageData.subject) && messageData.inReplyTo) {
|
|
2390
|
+
return true;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
if (!messageData.headers) {
|
|
2394
|
+
return false;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
// Check Precedence header
|
|
2398
|
+
if (messageData.headers.precedence && messageData.headers.precedence.some(e => /auto[_-]?reply/.test(e))) {
|
|
2399
|
+
return true;
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
// Check Auto-Submitted header
|
|
2403
|
+
if (messageData.headers['auto-submitted'] && messageData.headers['auto-submitted'].some(e => /auto[_-]?replied/.test(e))) {
|
|
2404
|
+
return true;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
// Check various vendor-specific headers
|
|
2408
|
+
for (let headerKey of ['x-autoresponder', 'x-autorespond', 'x-autoreply']) {
|
|
2409
|
+
if (messageData.headers[headerKey] && messageData.headers[headerKey].length) {
|
|
2410
|
+
return true;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
return false;
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
/**
|
|
2418
|
+
* Gets message fetch options based on settings
|
|
2419
|
+
* @returns {Object} Fetch options for IMAP message retrieval
|
|
2420
|
+
*/
|
|
2421
|
+
async getMessageFetchOptions() {
|
|
2422
|
+
let messageFetchOptions = {};
|
|
2423
|
+
|
|
2424
|
+
// Configure text content fetching
|
|
2425
|
+
let notifyText = await settings.get('notifyText');
|
|
2426
|
+
if (notifyText) {
|
|
2427
|
+
messageFetchOptions.textType = '*';
|
|
2428
|
+
let notifyTextSize = await settings.get('notifyTextSize');
|
|
2429
|
+
|
|
2430
|
+
if (notifyTextSize) {
|
|
2431
|
+
messageFetchOptions.maxBytes = notifyTextSize;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
// Configure header fetching
|
|
2436
|
+
let notifyHeaders = (await settings.get('notifyHeaders')) || [];
|
|
2437
|
+
if (notifyHeaders.length) {
|
|
2438
|
+
messageFetchOptions.headers = notifyHeaders.includes('*') ? true : notifyHeaders.length ? notifyHeaders : false;
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
// also request autoresponse headers
|
|
2442
|
+
if (messageFetchOptions.headers !== true) {
|
|
2443
|
+
let fetchHeaders = new Set(messageFetchOptions.headers || []);
|
|
2444
|
+
|
|
2445
|
+
// Headers for auto-reply detection
|
|
2446
|
+
fetchHeaders.add('x-autoreply');
|
|
2447
|
+
fetchHeaders.add('x-autorespond');
|
|
2448
|
+
fetchHeaders.add('auto-submitted');
|
|
2449
|
+
fetchHeaders.add('precedence');
|
|
2450
|
+
|
|
2451
|
+
// Headers for threading
|
|
2452
|
+
fetchHeaders.add('in-reply-to');
|
|
2453
|
+
fetchHeaders.add('references');
|
|
2454
|
+
|
|
2455
|
+
// Content type for detecting multipart/report
|
|
2456
|
+
fetchHeaders.add('content-type');
|
|
2457
|
+
|
|
2458
|
+
messageFetchOptions.fetchHeaders = Array.from(fetchHeaders);
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
return messageFetchOptions;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
/**
|
|
2465
|
+
* Main processor for new messages
|
|
2466
|
+
* Handles bounce detection, complaint processing, attachments,
|
|
2467
|
+
* calendar events, and AI-powered features
|
|
2468
|
+
* @param {Object} messageData - Message data
|
|
2469
|
+
* @param {Object} options - Processing options
|
|
2470
|
+
*/
|
|
2471
|
+
async processNew(messageData, options) {
|
|
2472
|
+
let requestedHeaders = options.headers;
|
|
2473
|
+
|
|
2474
|
+
let bounceNotifyInfo;
|
|
2475
|
+
let complaintNotifyInfo;
|
|
2476
|
+
let content;
|
|
2477
|
+
|
|
2478
|
+
// Check for abuse report format (ARF) complaints
|
|
2479
|
+
if (this.mightBeAComplaint(messageData)) {
|
|
2480
|
+
try {
|
|
2481
|
+
// Download attachments needed for complaint detection
|
|
2482
|
+
for (let attachment of messageData.attachments) {
|
|
2483
|
+
if (!['message/feedback-report', 'message/rfc822-headers', 'message/rfc822'].includes(attachment.contentType)) {
|
|
2484
|
+
continue;
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
Object.defineProperty(attachment, 'content', {
|
|
2488
|
+
value: (await this.getAttachment(attachment.id))?.data?.toString(),
|
|
2489
|
+
enumerable: false
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
// Parse ARF report
|
|
2494
|
+
const report = await arfDetect(messageData);
|
|
2495
|
+
|
|
2496
|
+
if (report && report.arf && report.arf['original-rcpt-to'] && report.arf['original-rcpt-to'].length) {
|
|
2497
|
+
// can send report
|
|
2498
|
+
let complaint = {};
|
|
2499
|
+
|
|
2500
|
+
// Convert headers to camelCase
|
|
2501
|
+
for (let subKey of ['arf', 'headers']) {
|
|
2502
|
+
for (let key of Object.keys(report[subKey])) {
|
|
2503
|
+
if (!complaint[subKey]) {
|
|
2504
|
+
complaint[subKey] = {};
|
|
2505
|
+
}
|
|
2506
|
+
complaint[subKey][key.replace(/-(.)/g, (o, c) => c.toUpperCase())] = report[subKey][key];
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
complaintNotifyInfo = Object.assign({ complaintMessage: messageData.id }, complaint);
|
|
2511
|
+
|
|
2512
|
+
messageData.isComplaint = true;
|
|
2513
|
+
|
|
2514
|
+
// Link to original message
|
|
2515
|
+
if (complaint.headers && complaint.headers.messageId) {
|
|
2516
|
+
messageData.relatedMessageId = complaint.headers.messageId;
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
} catch (err) {
|
|
2520
|
+
this.logger.error({
|
|
2521
|
+
msg: 'Failed to process ARF',
|
|
2522
|
+
id: messageData.id,
|
|
2523
|
+
uid: messageData.uid,
|
|
2524
|
+
messageId: messageData.messageId,
|
|
2525
|
+
err
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
// Check for delivery status notifications (DSN)
|
|
2531
|
+
if (this.mightBeDSNResponse(messageData)) {
|
|
2532
|
+
try {
|
|
2533
|
+
let raw = await this.getRawMessage(messageData.id);
|
|
2534
|
+
|
|
2535
|
+
let parsed = await simpleParser(raw, { keepDeliveryStatus: true });
|
|
2536
|
+
if (parsed) {
|
|
2537
|
+
content = { parsed };
|
|
2538
|
+
|
|
2539
|
+
// Extract delivery status attachment
|
|
2540
|
+
let deliveryStatus = parsed.attachments.find(attachment => attachment.contentType === 'message/delivery-status');
|
|
2541
|
+
if (deliveryStatus) {
|
|
2542
|
+
let deliveryEntries = libmime.decodeHeaders((deliveryStatus.content || '').toString().trim());
|
|
2543
|
+
let structured = {};
|
|
2544
|
+
|
|
2545
|
+
// Parse delivery status fields
|
|
2546
|
+
for (let key of Object.keys(deliveryEntries)) {
|
|
2547
|
+
if (!key) {
|
|
2548
|
+
continue;
|
|
2549
|
+
}
|
|
2550
|
+
let displayKey = key.replace(/-(.)/g, (m, c) => c.toUpperCase());
|
|
2551
|
+
let value = deliveryEntries[key].at(-1);
|
|
2552
|
+
if (typeof value === 'string') {
|
|
2553
|
+
// Parse structured fields
|
|
2554
|
+
let m = value.match(/^([^\s;]+);/);
|
|
2555
|
+
if (m) {
|
|
2556
|
+
value = {
|
|
2557
|
+
label: m[1],
|
|
2558
|
+
value: value.substring(m[0].length).trim()
|
|
2559
|
+
};
|
|
2560
|
+
} else {
|
|
2561
|
+
switch (key) {
|
|
2562
|
+
case 'arrival-date': {
|
|
2563
|
+
value.trim();
|
|
2564
|
+
let date = new Date(value);
|
|
2565
|
+
if (date.toString() !== 'Invalid Date') {
|
|
2566
|
+
value = date.toISOString();
|
|
2567
|
+
}
|
|
2568
|
+
structured[displayKey] = value;
|
|
2569
|
+
break;
|
|
2570
|
+
}
|
|
2571
|
+
default:
|
|
2572
|
+
structured[displayKey] = value.trim();
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
} else {
|
|
2576
|
+
// ???
|
|
2577
|
+
structured[displayKey] = value;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
// Check if this is a delivery or delay notification
|
|
2582
|
+
if (/^delivered|^delayed/i.test(structured.action)) {
|
|
2583
|
+
this.logger.debug({
|
|
2584
|
+
msg: 'Detected delivery report',
|
|
2585
|
+
id: messageData.id,
|
|
2586
|
+
uid: messageData.uid,
|
|
2587
|
+
messageId: messageData.messageId,
|
|
2588
|
+
report: structured
|
|
2589
|
+
});
|
|
2590
|
+
|
|
2591
|
+
messageData.deliveryReport = structured;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
} catch (err) {
|
|
2596
|
+
this.logger.error({
|
|
2597
|
+
msg: 'Failed to process DSN',
|
|
2598
|
+
id: messageData.id,
|
|
2599
|
+
uid: messageData.uid,
|
|
2600
|
+
messageId: messageData.messageId,
|
|
2601
|
+
err
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
// Check if this could be a bounce
|
|
2607
|
+
if (this.mightBeABounce(messageData)) {
|
|
2608
|
+
// parse for bounce
|
|
2609
|
+
try {
|
|
2610
|
+
if (!content) {
|
|
2611
|
+
content = await this.getRawMessage(messageData.id);
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
if (content) {
|
|
2615
|
+
let bounce = await bounceDetect(content);
|
|
2616
|
+
|
|
2617
|
+
if (bounce?.response?.message) {
|
|
2618
|
+
try {
|
|
2619
|
+
let bounceType = await this.call({
|
|
2620
|
+
cmd: 'bounceClassify',
|
|
2621
|
+
data: {
|
|
2622
|
+
message: bounce?.response?.message
|
|
2623
|
+
},
|
|
2624
|
+
timeout: 2 * 60 * 1000
|
|
2625
|
+
});
|
|
2626
|
+
if (bounceType?.label) {
|
|
2627
|
+
bounce.response.category = bounceType.label;
|
|
2628
|
+
}
|
|
2629
|
+
if (bounceType?.action) {
|
|
2630
|
+
bounce.response.recommendedAction = bounceType.action;
|
|
2631
|
+
}
|
|
2632
|
+
if (bounceType?.blocklist) {
|
|
2633
|
+
bounce.response.blocklist = bounceType.blocklist;
|
|
2634
|
+
}
|
|
2635
|
+
if (bounceType?.retryAfter) {
|
|
2636
|
+
bounce.response.retryAfter = bounceType.retryAfter;
|
|
2637
|
+
}
|
|
2638
|
+
} catch (err) {
|
|
2639
|
+
// ignore, just do not include this information
|
|
2640
|
+
this.logger.error({
|
|
2641
|
+
msg: 'Failed to classify bounce response',
|
|
2642
|
+
bounceResponse: bounce?.response?.message,
|
|
2643
|
+
err
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
let stored = 0;
|
|
2649
|
+
if (bounce.action && bounce.recipient && bounce.messageId) {
|
|
2650
|
+
bounceNotifyInfo = Object.assign({ bounceMessage: messageData.id }, bounce);
|
|
2651
|
+
|
|
2652
|
+
messageData.isBounce = true;
|
|
2653
|
+
messageData.relatedMessageId = bounce.messageId;
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
this.logger.debug({
|
|
2657
|
+
msg: 'Detected bounce message',
|
|
2658
|
+
id: messageData.id,
|
|
2659
|
+
uid: messageData.uid,
|
|
2660
|
+
messageId: messageData.messageId,
|
|
2661
|
+
bounce,
|
|
2662
|
+
stored
|
|
2663
|
+
});
|
|
2664
|
+
}
|
|
2665
|
+
} catch (err) {
|
|
2666
|
+
this.logger.error({
|
|
2667
|
+
msg: 'Failed to process potential bounce',
|
|
2668
|
+
id: messageData.id,
|
|
2669
|
+
uid: messageData.uid,
|
|
2670
|
+
messageId: messageData.messageId,
|
|
2671
|
+
err
|
|
2672
|
+
});
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
// Download attachment content if configured
|
|
2677
|
+
let notifyAttachments = await settings.get('notifyAttachments');
|
|
2678
|
+
let notifyAttachmentSize = await settings.get('notifyAttachmentSize');
|
|
2679
|
+
if (notifyAttachments && messageData.attachments?.length) {
|
|
2680
|
+
for (let attachment of messageData.attachments || []) {
|
|
2681
|
+
if (notifyAttachmentSize && attachment.encodedSize && attachment.encodedSize > notifyAttachmentSize) {
|
|
2682
|
+
// skip large attachments
|
|
2683
|
+
continue;
|
|
2684
|
+
}
|
|
2685
|
+
if (!attachment.content) {
|
|
2686
|
+
try {
|
|
2687
|
+
attachment.content = (await this.getAttachment(attachment.id))?.data?.toString('base64');
|
|
2688
|
+
} catch (err) {
|
|
2689
|
+
this.logger.error({ msg: 'Failed to load attachment content', attachment, err });
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
// Fetch inline attachments referenced in HTML
|
|
2696
|
+
if (messageData.attachments && messageData.attachments.length && messageData.text && messageData.text.html) {
|
|
2697
|
+
// fetch inline attachments
|
|
2698
|
+
for (let attachment of messageData.attachments) {
|
|
2699
|
+
if (attachment.encodedSize && attachment.encodedSize > MAX_INLINE_ATTACHMENT_SIZE) {
|
|
2700
|
+
// skip large attachments
|
|
2701
|
+
continue;
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
if (!attachment.content && attachment.contentId && messageData.text.html.indexOf(`cid:${attachment.contentId.replace(/^<|>$/g, '')}`) >= 0) {
|
|
2705
|
+
try {
|
|
2706
|
+
attachment.content = (await this.getAttachment(attachment.id))?.data?.toString('base64');
|
|
2707
|
+
} catch (err) {
|
|
2708
|
+
this.logger.error({ msg: 'Failed to load attachment content', attachment, err });
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
// Fetch and process calendar events if needed
|
|
2715
|
+
let notifyCalendarEvents = await settings.get('notifyCalendarEvents');
|
|
2716
|
+
if (notifyCalendarEvents && messageData.attachments && messageData.attachments.length) {
|
|
2717
|
+
let calendarEventMap = new Map();
|
|
2718
|
+
|
|
2719
|
+
// when iterating the attachment array, process text/calendar before application/ics
|
|
2720
|
+
let sortCalendarAttachments = (a, b) => {
|
|
2721
|
+
if (a.contentType !== b.contentType) {
|
|
2722
|
+
if (a.contentType === 'text/calendar') {
|
|
2723
|
+
return -1;
|
|
2724
|
+
}
|
|
2725
|
+
if (b.contentType === 'text/calendar') {
|
|
2726
|
+
return 1;
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
return a.contentType.localeCompare(b.contentType);
|
|
2730
|
+
};
|
|
2731
|
+
|
|
2732
|
+
// Process calendar attachments
|
|
2733
|
+
for (let attachment of [...messageData.attachments].sort(sortCalendarAttachments)) {
|
|
2734
|
+
if (['text/calendar', 'application/ics'].includes(attachment.contentType)) {
|
|
2735
|
+
if (!attachment.content) {
|
|
2736
|
+
try {
|
|
2737
|
+
let calendarBuf = (await this.getAttachment(attachment.id))?.data;
|
|
2738
|
+
attachment.content = calendarBuf.toString('base64');
|
|
2739
|
+
} catch (err) {
|
|
2740
|
+
this.logger.error({ msg: 'Failed to load attachment content', attachment, err });
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
if (attachment.content) {
|
|
2744
|
+
let contentBuf = Buffer.from(attachment.content, 'base64');
|
|
2745
|
+
try {
|
|
2746
|
+
// Parse iCalendar data
|
|
2747
|
+
const jcalData = ical.parse(contentBuf.toString());
|
|
2748
|
+
|
|
2749
|
+
const comp = new ical.Component(jcalData);
|
|
2750
|
+
if (!comp) {
|
|
2751
|
+
continue;
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
const vevent = comp.getFirstSubcomponent('vevent');
|
|
2755
|
+
if (!vevent) {
|
|
2756
|
+
continue;
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
// Extract method (REQUEST, CANCEL, etc.)
|
|
2760
|
+
let eventMethodProp = comp.getFirstProperty('method');
|
|
2761
|
+
let eventMethodValue = eventMethodProp ? eventMethodProp.getFirstValue() : null;
|
|
2762
|
+
|
|
2763
|
+
const event = new ical.Event(vevent);
|
|
2764
|
+
|
|
2765
|
+
if (!event || !event.uid) {
|
|
2766
|
+
continue;
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
// Skip if we already processed this event
|
|
2770
|
+
if (calendarEventMap.has(event.uid)) {
|
|
2771
|
+
if (attachment.filename) {
|
|
2772
|
+
let existingEntry = calendarEventMap.get(event.uid);
|
|
2773
|
+
if (!existingEntry.filename) {
|
|
2774
|
+
// inject filename
|
|
2775
|
+
existingEntry.filename = attachment.filename;
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
continue;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
// Extract timezone information
|
|
2782
|
+
let timezone;
|
|
2783
|
+
const vtz = comp.getFirstSubcomponent('vtimezone');
|
|
2784
|
+
if (vtz) {
|
|
2785
|
+
const tz = new ical.Timezone(vtz);
|
|
2786
|
+
timezone = tz && tz.tzid;
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
let startDate = event.startDate && event.startDate.toJSDate();
|
|
2790
|
+
let endDate = event.endDate && event.endDate.toJSDate();
|
|
2791
|
+
|
|
2792
|
+
// Store calendar event data
|
|
2793
|
+
calendarEventMap.set(
|
|
2794
|
+
event.uid,
|
|
2795
|
+
filterEmptyObjectValues({
|
|
2796
|
+
eventId: event.uid,
|
|
2797
|
+
attachment: attachment.id,
|
|
2798
|
+
method: attachment.method || eventMethodValue || null,
|
|
2799
|
+
|
|
2800
|
+
summary: event.summary || null,
|
|
2801
|
+
description: event.description || null,
|
|
2802
|
+
timezone: timezone || null,
|
|
2803
|
+
startDate: startDate ? startDate.toISOString() : null,
|
|
2804
|
+
endDate: endDate ? endDate.toISOString() : null,
|
|
2805
|
+
organizer: event.organizer && typeof event.organizer === 'string' ? event.organizer : null,
|
|
2806
|
+
|
|
2807
|
+
filename: attachment.filename,
|
|
2808
|
+
contentType: attachment.contentType,
|
|
2809
|
+
encoding: 'base64',
|
|
2810
|
+
content: attachment.content
|
|
2811
|
+
})
|
|
2812
|
+
);
|
|
2813
|
+
} catch (err) {
|
|
2814
|
+
this.logger.error({
|
|
2815
|
+
msg: 'Failed to parse calendar event',
|
|
2816
|
+
attachment: Object.assign({}, attachment, { content: `${contentBuf.length} bytes` }),
|
|
2817
|
+
err
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
if (calendarEventMap && calendarEventMap.size) {
|
|
2825
|
+
// Convert map to array and set default filenames
|
|
2826
|
+
messageData.calendarEvents = Array.from(calendarEventMap.values()).map(calendarEvent => {
|
|
2827
|
+
if (!calendarEvent.filename) {
|
|
2828
|
+
switch (calendarEvent.method && calendarEvent.method.toUpperCase()) {
|
|
2829
|
+
case 'CANCEL':
|
|
2830
|
+
case 'REQUEST':
|
|
2831
|
+
calendarEvent.filename = 'invite.ics';
|
|
2832
|
+
break;
|
|
2833
|
+
default:
|
|
2834
|
+
calendarEvent.filename = 'event.ics';
|
|
2835
|
+
break;
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
return calendarEvent;
|
|
2839
|
+
});
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
// Process with AI features for inbox messages
|
|
2844
|
+
if (messageData.messageSpecialUse === '\\Inbox') {
|
|
2845
|
+
let llmMessageData = Object.assign({ account: this.account }, messageData);
|
|
2846
|
+
|
|
2847
|
+
// Check if LLM processing is enabled
|
|
2848
|
+
let canUseLLM = await llmPreProcess.run(llmMessageData);
|
|
2849
|
+
|
|
2850
|
+
if (canUseLLM && (messageData.text.plain || messageData.text.html)) {
|
|
2851
|
+
// Generate email summary using AI
|
|
2852
|
+
if (canUseLLM.generateEmailSummary) {
|
|
2853
|
+
try {
|
|
2854
|
+
messageData.summary = await this.call({
|
|
2855
|
+
cmd: 'generateSummary',
|
|
2856
|
+
data: {
|
|
2857
|
+
message: {
|
|
2858
|
+
headers: Object.keys(messageData.headers || {}).map(key => ({ key, value: [].concat(messageData.headers[key] || []) })),
|
|
2859
|
+
attachments: messageData.attachments,
|
|
2860
|
+
from: messageData.from,
|
|
2861
|
+
subject: messageData.subject,
|
|
2862
|
+
text: messageData.text.plain,
|
|
2863
|
+
html: messageData.text.html
|
|
2864
|
+
},
|
|
2865
|
+
account: this.account
|
|
2866
|
+
},
|
|
2867
|
+
timeout: 2 * 60 * 1000
|
|
2868
|
+
});
|
|
2869
|
+
|
|
2870
|
+
if (messageData.summary) {
|
|
2871
|
+
// Clean up summary output
|
|
2872
|
+
for (let key of Object.keys(messageData.summary)) {
|
|
2873
|
+
// remove meta keys from output
|
|
2874
|
+
if (key.charAt(0) === '_' || messageData.summary[key] === '') {
|
|
2875
|
+
delete messageData.summary[key];
|
|
2876
|
+
}
|
|
2877
|
+
if (key === 'riskAssessment') {
|
|
2878
|
+
messageData.riskAssessment = messageData.summary.riskAssessment;
|
|
2879
|
+
delete messageData.summary.riskAssessment;
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
this.logger.trace({ msg: 'Fetched summary from OpenAI', summary: messageData.summary });
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
await this.redis.del(`${REDIS_PREFIX}:openai:error`);
|
|
2887
|
+
} catch (err) {
|
|
2888
|
+
// Track OpenAI errors
|
|
2889
|
+
await this.redis.set(
|
|
2890
|
+
`${REDIS_PREFIX}:openai:error`,
|
|
2891
|
+
JSON.stringify({
|
|
2892
|
+
message: err.message,
|
|
2893
|
+
code: err.code,
|
|
2894
|
+
statusCode: err.statusCode,
|
|
2895
|
+
created: Date.now()
|
|
2896
|
+
})
|
|
2897
|
+
);
|
|
2898
|
+
this.logger.error({ msg: 'Failed to fetch summary from OpenAI', err });
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
// Generate embeddings for semantic search
|
|
2903
|
+
if (canUseLLM.generateEmbeddings) {
|
|
2904
|
+
try {
|
|
2905
|
+
messageData.embeddings = await this.call({
|
|
2906
|
+
cmd: 'generateEmbeddings',
|
|
2907
|
+
data: {
|
|
2908
|
+
message: {
|
|
2909
|
+
headers: Object.keys(messageData.headers || {}).map(key => ({ key, value: [].concat(messageData.headers[key] || []) })),
|
|
2910
|
+
attachments: messageData.attachments,
|
|
2911
|
+
from: messageData.from,
|
|
2912
|
+
subject: messageData.subject,
|
|
2913
|
+
text: messageData.text.plain,
|
|
2914
|
+
html: messageData.text.html
|
|
2915
|
+
},
|
|
2916
|
+
account: this.account
|
|
2917
|
+
},
|
|
2918
|
+
timeout: 2 * 60 * 1000
|
|
2919
|
+
});
|
|
2920
|
+
} catch (err) {
|
|
2921
|
+
await this.redis.set(
|
|
2922
|
+
`${REDIS_PREFIX}:openai:error`,
|
|
2923
|
+
JSON.stringify({
|
|
2924
|
+
message: err.message,
|
|
2925
|
+
code: err.code,
|
|
2926
|
+
statusCode: err.statusCode,
|
|
2927
|
+
time: Date.now()
|
|
2928
|
+
})
|
|
2929
|
+
);
|
|
2930
|
+
this.logger.error({ msg: 'Failed to fetch embeddings OpenAI', err });
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
// Convert message HTML to web safe HTML
|
|
2937
|
+
let notifyWebSafeHtml = await settings.get('notifyWebSafeHtml');
|
|
2938
|
+
if (notifyWebSafeHtml && messageData.text && (messageData.text.html || messageData.text.plain)) {
|
|
2939
|
+
// convert to web safe
|
|
2940
|
+
messageData.text._generatedHtml = mimeHtml({
|
|
2941
|
+
html: messageData.text.html,
|
|
2942
|
+
text: messageData.text.plain
|
|
2943
|
+
});
|
|
2944
|
+
|
|
2945
|
+
// embed inline images as data URIs
|
|
2946
|
+
if (messageData.text.html && messageData.attachments) {
|
|
2947
|
+
let attachmentList = new Map();
|
|
2948
|
+
|
|
2949
|
+
// Find CID references in HTML
|
|
2950
|
+
for (let attachment of messageData.attachments) {
|
|
2951
|
+
let contentId = attachment.contentId && attachment.contentId.replace(/^<|>$/g, '');
|
|
2952
|
+
if (contentId && messageData.text.html.indexOf(contentId) >= 0) {
|
|
2953
|
+
if (attachment.content) {
|
|
2954
|
+
// already downloaded in a previous step
|
|
2955
|
+
continue;
|
|
2956
|
+
} else {
|
|
2957
|
+
attachment.content = (await this.getAttachment(attachment.id))?.data?.toString('base64');
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
attachmentList.set(contentId, {
|
|
2961
|
+
attachment,
|
|
2962
|
+
content: attachment.content || null
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
// Replace CID references with data URIs
|
|
2968
|
+
if (attachmentList.size) {
|
|
2969
|
+
messageData.text.html = messageData.text.html.replace(/\bcid:([^"'\s>]+)/g, (fullMatch, cidMatch) => {
|
|
2970
|
+
if (attachmentList.has(cidMatch)) {
|
|
2971
|
+
let { attachment, content } = attachmentList.get(cidMatch);
|
|
2972
|
+
if (content) {
|
|
2973
|
+
return `data:${attachment.contentType || 'application/octet-stream'};base64,${content}`;
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
return fullMatch;
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
messageData.text.webSafe = true;
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
// Filter headers based on request
|
|
2985
|
+
// we might have fetched more headers than was asked for, so filter out all the unneeded ones
|
|
2986
|
+
if (options.headers && Array.isArray(requestedHeaders)) {
|
|
2987
|
+
let filteredHeaders = {};
|
|
2988
|
+
for (let key of Object.keys(messageData.headers || {})) {
|
|
2989
|
+
if (requestedHeaders.includes(key)) {
|
|
2990
|
+
filteredHeaders[key] = messageData.headers[key];
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
messageData.headers = filteredHeaders;
|
|
2994
|
+
} else if (options.headers && requestedHeaders === false) {
|
|
2995
|
+
delete messageData.headers;
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
let path = messageData.path || this.path;
|
|
2999
|
+
let specialUse = messageData.path ? messageData.messageSpecialUse : this.listingEntry.specialUse;
|
|
3000
|
+
|
|
3001
|
+
if (messageData.path) {
|
|
3002
|
+
// unset path from the message level
|
|
3003
|
+
messageData.path = undefined;
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
// Send new message notification
|
|
3007
|
+
await this.notify(
|
|
3008
|
+
{
|
|
3009
|
+
path,
|
|
3010
|
+
specialUse
|
|
3011
|
+
},
|
|
3012
|
+
MESSAGE_NEW_NOTIFY,
|
|
3013
|
+
messageData
|
|
3014
|
+
);
|
|
3015
|
+
|
|
3016
|
+
// Send additional notifications for bounces and complaints
|
|
3017
|
+
if (bounceNotifyInfo) {
|
|
3018
|
+
// send bounce notification _after_ bounce email notification
|
|
3019
|
+
await this.notify(false, EMAIL_BOUNCE_NOTIFY, bounceNotifyInfo);
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
if (complaintNotifyInfo) {
|
|
3023
|
+
// send complaint notification _after_ complaint email notification
|
|
3024
|
+
await this.notify(false, EMAIL_COMPLAINT_NOTIFY, complaintNotifyInfo);
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
/**
|
|
3029
|
+
* Checks if message might be an abuse complaint based on headers and content
|
|
3030
|
+
* @param {Object} messageData - Message data
|
|
3031
|
+
* @returns {boolean} True if message might be a complaint
|
|
3032
|
+
*/
|
|
3033
|
+
mightBeAComplaint(messageData) {
|
|
3034
|
+
if (messageData.messageSpecialUse !== '\\Inbox') {
|
|
3035
|
+
return false;
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
let hasEmbeddedMessage = false;
|
|
3039
|
+
for (let attachment of messageData.attachments || []) {
|
|
3040
|
+
// ARF format indicator
|
|
3041
|
+
if (attachment.contentType === 'message/feedback-report') {
|
|
3042
|
+
return true;
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
if (['message/rfc822', 'message/rfc822-headers'].includes(attachment.contentType)) {
|
|
3046
|
+
hasEmbeddedMessage = true;
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
let fromAddress = (messageData.from && messageData.from.address) || '';
|
|
3051
|
+
|
|
3052
|
+
// Hotmail-specific complaint format
|
|
3053
|
+
if (hasEmbeddedMessage && fromAddress === 'staff@hotmail.com' && /complaint/i.test(messageData.subject)) {
|
|
3054
|
+
return true;
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
return false;
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
/**
|
|
3061
|
+
* Checks if message is a delivery status notification
|
|
3062
|
+
* @param {Object} messageData - Message data
|
|
3063
|
+
* @returns {boolean} True if message is a DSN
|
|
3064
|
+
*/
|
|
3065
|
+
mightBeDSNResponse(messageData) {
|
|
3066
|
+
if (messageData.messageSpecialUse !== '\\Inbox') {
|
|
3067
|
+
return false;
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
// Check for multipart/report with delivery-status
|
|
3071
|
+
if (messageData.headers && messageData.headers['content-type'] && messageData.headers['content-type'].length) {
|
|
3072
|
+
let parsedContentType = libmime.parseHeaderValue(messageData.headers['content-type'].at(-1));
|
|
3073
|
+
if (
|
|
3074
|
+
parsedContentType &&
|
|
3075
|
+
parsedContentType.value &&
|
|
3076
|
+
parsedContentType.value.toLowerCase().trim() === 'multipart/report' &&
|
|
3077
|
+
parsedContentType.params['report-type'] === 'delivery-status'
|
|
3078
|
+
) {
|
|
3079
|
+
return true;
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
return false;
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
/**
|
|
3087
|
+
* Checks if message might be a bounce based on various heuristics
|
|
3088
|
+
* @param {Object} messageData - Message data
|
|
3089
|
+
* @returns {boolean} True if message might be a bounce
|
|
3090
|
+
*/
|
|
3091
|
+
mightBeABounce(messageData) {
|
|
3092
|
+
if (!['\\Inbox', '\\Junk'].includes(messageData.messageSpecialUse)) {
|
|
3093
|
+
return false;
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
if (messageData.deliveryReport) {
|
|
3097
|
+
// already processed
|
|
3098
|
+
return false;
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
let name = (messageData.from && messageData.from.name) || '';
|
|
3102
|
+
let address = (messageData.from && messageData.from.address) || '';
|
|
3103
|
+
|
|
3104
|
+
// Common bounce sender names
|
|
3105
|
+
if (/Mail Delivery System|Mail Delivery Subsystem|Internet Mail Delivery/i.test(name)) {
|
|
3106
|
+
return true;
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
// Common bounce addresses
|
|
3110
|
+
if (/mailer-daemon@|postmaster@/i.test(address)) {
|
|
3111
|
+
return true;
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
// Exchange-style bounce
|
|
3115
|
+
if (/^Undeliverable:/.test(messageData.subject) && messageData.headers?.['auto-submitted']) {
|
|
3116
|
+
return true;
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
// Check for delivery-status attachment with undeliverable subject
|
|
3120
|
+
let hasDeliveryStatus = false;
|
|
3121
|
+
for (let attachment of messageData.attachments || []) {
|
|
3122
|
+
if (attachment.contentType === 'message/delivery-status') {
|
|
3123
|
+
hasDeliveryStatus = true;
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
if (hasDeliveryStatus && /Undeliver(able|ed)/i.test(messageData.subject)) {
|
|
3128
|
+
return true;
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
return false;
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
/**
|
|
3135
|
+
* Submits a message for SMTP delivery
|
|
3136
|
+
* Handles authentication, connection pooling, and error tracking
|
|
3137
|
+
* @param {Object} data - Message data with raw content and metadata
|
|
3138
|
+
* @returns {Object} Delivery result with response and messageId
|
|
3139
|
+
*/
|
|
3140
|
+
async submitMessage(data) {
|
|
3141
|
+
let accountData = await this.accountObject.loadAccountData();
|
|
3142
|
+
|
|
3143
|
+
// Verify SMTP capability
|
|
3144
|
+
if (!accountData.smtp && !accountData.oauth2 && !data.gateway) {
|
|
3145
|
+
// can not make connection
|
|
3146
|
+
let err = new Error('SMTP configuration not found');
|
|
3147
|
+
err.code = 'SMTPUnavailable';
|
|
3148
|
+
err.statusCode = 404;
|
|
3149
|
+
throw err;
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
// Load gateway configuration if specified
|
|
3153
|
+
let gatewayData;
|
|
3154
|
+
let gatewayObject;
|
|
3155
|
+
if (data.gateway) {
|
|
3156
|
+
gatewayObject = new Gateway({ gateway: data.gateway, redis: this.redis, secret: this.secret });
|
|
3157
|
+
try {
|
|
3158
|
+
gatewayData = await gatewayObject.loadGatewayData();
|
|
3159
|
+
} catch (err) {
|
|
3160
|
+
this.logger.info({ msg: 'Failed to load gateway data', messageId: data.messageId, gateway: data.gateway, err });
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
let smtpConnectionConfig;
|
|
3165
|
+
|
|
3166
|
+
// Configure SMTP connection based on authentication type
|
|
3167
|
+
if (gatewayData) {
|
|
3168
|
+
// Use gateway configuration
|
|
3169
|
+
smtpConnectionConfig = {
|
|
3170
|
+
host: gatewayData.host,
|
|
3171
|
+
port: gatewayData.port,
|
|
3172
|
+
secure: gatewayData.secure
|
|
3173
|
+
};
|
|
3174
|
+
if (gatewayData.user || gatewayData.pass) {
|
|
3175
|
+
smtpConnectionConfig.auth = {
|
|
3176
|
+
user: gatewayData.user || '',
|
|
3177
|
+
pass: gatewayData.pass || ''
|
|
3178
|
+
};
|
|
3179
|
+
}
|
|
3180
|
+
} else if (accountData.oauth2 && accountData.oauth2.auth) {
|
|
3181
|
+
// load OAuth2 tokens
|
|
3182
|
+
const { oauth2User, accessToken, oauth2App } = await this.loadOAuth2AccountCredentials(accountData, this, 'smtp');
|
|
3183
|
+
const providerData = oauth2ProviderData(oauth2App.provider, oauth2App.cloud);
|
|
3184
|
+
|
|
3185
|
+
smtpConnectionConfig = Object.assign(
|
|
3186
|
+
{
|
|
3187
|
+
auth: {
|
|
3188
|
+
user: oauth2User,
|
|
3189
|
+
accessToken
|
|
3190
|
+
},
|
|
3191
|
+
resyncDelay: 900
|
|
3192
|
+
},
|
|
3193
|
+
providerData.smtp || {}
|
|
3194
|
+
);
|
|
3195
|
+
} else {
|
|
3196
|
+
// deep copy of smtp settings
|
|
3197
|
+
smtpConnectionConfig = JSON.parse(JSON.stringify(accountData.smtp));
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
let { raw, hasBcc, envelope, messageId, queueId, reference, job: jobData } = data;
|
|
3201
|
+
|
|
3202
|
+
let smtpAuth = smtpConnectionConfig.auth;
|
|
3203
|
+
// If authentication server is set then it overrides authentication data
|
|
3204
|
+
if (smtpConnectionConfig.useAuthServer) {
|
|
3205
|
+
try {
|
|
3206
|
+
smtpAuth = await resolveCredentials(this.account, 'smtp');
|
|
3207
|
+
} catch (err) {
|
|
3208
|
+
err.authenticationFailed = true;
|
|
3209
|
+
this.logger.error({
|
|
3210
|
+
account: this.account,
|
|
3211
|
+
err
|
|
3212
|
+
});
|
|
3213
|
+
throw err;
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
// Select local address for outbound connection
|
|
3218
|
+
let { localAddress: address, name, addressSelector: selector } = await getLocalAddress(this.redis, 'smtp', this.account, data.localAddress);
|
|
3219
|
+
this.logger.info({
|
|
3220
|
+
msg: 'Selected local address',
|
|
3221
|
+
account: this.account,
|
|
3222
|
+
proto: 'SMTP',
|
|
3223
|
+
address,
|
|
3224
|
+
name,
|
|
3225
|
+
selector,
|
|
3226
|
+
requestedLocalAddress: data.localAddress
|
|
3227
|
+
});
|
|
3228
|
+
|
|
3229
|
+
// Configure SMTP logger
|
|
3230
|
+
let smtpLogger = {};
|
|
3231
|
+
let smtpSettings = Object.assign(
|
|
3232
|
+
{
|
|
3233
|
+
name,
|
|
3234
|
+
localAddress: address,
|
|
3235
|
+
transactionLog: true,
|
|
3236
|
+
logger: smtpLogger
|
|
3237
|
+
},
|
|
3238
|
+
smtpConnectionConfig
|
|
3239
|
+
);
|
|
3240
|
+
|
|
3241
|
+
// Set authentication
|
|
3242
|
+
if (smtpAuth) {
|
|
3243
|
+
smtpSettings.auth = {
|
|
3244
|
+
user: smtpAuth.user
|
|
3245
|
+
};
|
|
3246
|
+
|
|
3247
|
+
if (smtpAuth.accessToken) {
|
|
3248
|
+
smtpSettings.auth.type = 'OAuth2';
|
|
3249
|
+
smtpSettings.auth.accessToken = smtpAuth.accessToken;
|
|
3250
|
+
} else {
|
|
3251
|
+
smtpSettings.auth.pass = smtpAuth.pass;
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
// Configure TLS with defaults
|
|
3256
|
+
if (!smtpSettings.tls) {
|
|
3257
|
+
smtpSettings.tls = {};
|
|
3258
|
+
}
|
|
3259
|
+
for (let key of Object.keys(TLS_DEFAULTS)) {
|
|
3260
|
+
if (!(key in smtpSettings.tls)) {
|
|
3261
|
+
smtpSettings.tls[key] = TLS_DEFAULTS[key];
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
// Set up logger wrapper
|
|
3266
|
+
for (let level of ['trace', 'debug', 'info', 'warn', 'error', 'fatal']) {
|
|
3267
|
+
smtpLogger[level] = (data, message, ...args) => {
|
|
3268
|
+
if (args && args.length) {
|
|
3269
|
+
message = util.format(message, ...args);
|
|
3270
|
+
}
|
|
3271
|
+
data.msg = message;
|
|
3272
|
+
data.sub = 'nodemailer';
|
|
3273
|
+
if (typeof this.logger[level] === 'function') {
|
|
3274
|
+
this.logger[level](data);
|
|
3275
|
+
} else {
|
|
3276
|
+
this.logger.debug(data);
|
|
3277
|
+
}
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
// set up proxy if needed
|
|
3282
|
+
if (data.proxy) {
|
|
3283
|
+
smtpSettings.proxy = data.proxy;
|
|
3284
|
+
} else if (accountData.proxy) {
|
|
3285
|
+
smtpSettings.proxy = accountData.proxy;
|
|
3286
|
+
} else {
|
|
3287
|
+
let proxyUrl = await settings.get('proxyUrl');
|
|
3288
|
+
let proxyEnabled = await settings.get('proxyEnabled');
|
|
3289
|
+
if (proxyEnabled && proxyUrl && !smtpSettings.proxy) {
|
|
3290
|
+
smtpSettings.proxy = proxyUrl;
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
// Override EHLO hostname if configured
|
|
3295
|
+
if (accountData.smtpEhloName) {
|
|
3296
|
+
smtpSettings.name = accountData.smtpEhloName;
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
// Verify job still exists
|
|
3300
|
+
const submitJobEntry = await this.submitQueue.getJob(jobData.id);
|
|
3301
|
+
if (!submitJobEntry) {
|
|
3302
|
+
// already failed?
|
|
3303
|
+
this.logger.error({
|
|
3304
|
+
msg: 'Submit job was not found',
|
|
3305
|
+
job: jobData.id
|
|
3306
|
+
});
|
|
3307
|
+
return false;
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
// Handle certificate errors if configured
|
|
3311
|
+
const ignoreMailCertErrors = await settings.get('ignoreMailCertErrors');
|
|
3312
|
+
if (ignoreMailCertErrors && smtpSettings?.tls?.rejectUnauthorized !== false) {
|
|
3313
|
+
smtpSettings.tls = smtpSettings.tls || {};
|
|
3314
|
+
smtpSettings.tls.rejectUnauthorized = false;
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
// Prepare network routing info for notifications
|
|
3318
|
+
const networkRouting = smtpSettings.localAddress || smtpSettings.proxy ? {} : null;
|
|
3319
|
+
|
|
3320
|
+
if (networkRouting && smtpSettings.localAddress) {
|
|
3321
|
+
networkRouting.localAddress = smtpSettings.localAddress;
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
if (networkRouting && smtpSettings.proxy) {
|
|
3325
|
+
networkRouting.proxy = smtpSettings.proxy;
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
if (networkRouting && smtpSettings.name) {
|
|
3329
|
+
networkRouting.name = smtpSettings.name;
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
if (networkRouting && data.localAddress && data.localAddress !== networkRouting.localAddress) {
|
|
3333
|
+
networkRouting.requestedLocalAddress = data.localAddress;
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
// Get or create SMTP transport from pool
|
|
3337
|
+
const transporter = getMailTransport(smtpSettings);
|
|
3338
|
+
|
|
3339
|
+
try {
|
|
3340
|
+
try {
|
|
3341
|
+
// try to update job progress
|
|
3342
|
+
await submitJobEntry.updateProgress({
|
|
3343
|
+
status: 'smtp-starting'
|
|
3344
|
+
});
|
|
3345
|
+
} catch (err) {
|
|
3346
|
+
// ignore
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
// Send the message
|
|
3350
|
+
const info = await transporter.sendMail({
|
|
3351
|
+
envelope,
|
|
3352
|
+
messageId,
|
|
3353
|
+
// make sure that Bcc line is removed from the version sent to SMTP
|
|
3354
|
+
raw: !hasBcc ? raw : await removeBcc(raw),
|
|
3355
|
+
dsn: data.dsn || null
|
|
3356
|
+
});
|
|
3357
|
+
|
|
3358
|
+
// Store EHLO response for debugging
|
|
3359
|
+
if (info.ehlo) {
|
|
3360
|
+
await this.redis.hSetExists(this.getAccountKey(), 'smtpServerEhlo', JSON.stringify(info.ehlo));
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
// special rules for MTA servers
|
|
3364
|
+
|
|
3365
|
+
let originalMessageId;
|
|
3366
|
+
|
|
3367
|
+
// Hotmail - extract actual Message-ID from response
|
|
3368
|
+
let hotmailMessageIdMatch = (info.response || '').toString().match(/^250 2.0.0 OK (<[^>]+\.prod\.outlook\.com>)/);
|
|
3369
|
+
if (hotmailMessageIdMatch && hotmailMessageIdMatch[1] !== info.messageId) {
|
|
3370
|
+
// MessageId was overridden
|
|
3371
|
+
originalMessageId = info.messageId;
|
|
3372
|
+
info.messageId = hotmailMessageIdMatch[1];
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
// AWS SES - construct Message-ID from response
|
|
3376
|
+
let awsSesHostMatch = (smtpSettings.host || '').toString().match(/\.([^.]+)\.(amazonaws\.com|awsapps\.com)$/i);
|
|
3377
|
+
let awsSesMessageIdMatch = (info.response || '').toString().match(/^250 Ok ([0-9a-f-]+)$/);
|
|
3378
|
+
if (awsSesHostMatch && awsSesMessageIdMatch) {
|
|
3379
|
+
let region = awsSesHostMatch[1].toLowerCase().trim();
|
|
3380
|
+
let messageId = awsSesMessageIdMatch[1].toLowerCase().trim();
|
|
3381
|
+
if (region === 'us-east-1') {
|
|
3382
|
+
region = 'email';
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
// MessageId was overridden
|
|
3386
|
+
originalMessageId = info.messageId;
|
|
3387
|
+
info.messageId = '<' + messageId + (!/@/.test(messageId) ? '@' + region + '.amazonses.com' : '') + '>';
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
// done
|
|
3391
|
+
|
|
3392
|
+
try {
|
|
3393
|
+
// try to update job progress
|
|
3394
|
+
await submitJobEntry.updateProgress({
|
|
3395
|
+
status: 'smtp-completed',
|
|
3396
|
+
|
|
3397
|
+
response: info.response,
|
|
3398
|
+
messageId: info.messageId,
|
|
3399
|
+
originalMessageId
|
|
3400
|
+
});
|
|
3401
|
+
} catch (err) {
|
|
3402
|
+
// ignore
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3405
|
+
// Send success notification
|
|
3406
|
+
await this.notify(false, EMAIL_SENT_NOTIFY, {
|
|
3407
|
+
messageId: info.messageId,
|
|
3408
|
+
originalMessageId,
|
|
3409
|
+
response: info.response,
|
|
3410
|
+
queueId,
|
|
3411
|
+
envelope,
|
|
3412
|
+
networkRouting
|
|
3413
|
+
});
|
|
3414
|
+
|
|
3415
|
+
// clean up possible cached SMTP error
|
|
3416
|
+
try {
|
|
3417
|
+
await this.redis.hset(
|
|
3418
|
+
this.getAccountKey(),
|
|
3419
|
+
'smtpStatus',
|
|
3420
|
+
JSON.stringify({
|
|
3421
|
+
created: Date.now(),
|
|
3422
|
+
status: 'ok',
|
|
3423
|
+
response: info.response
|
|
3424
|
+
})
|
|
3425
|
+
);
|
|
3426
|
+
} catch (err) {
|
|
3427
|
+
// ignore?
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
// The default is to copy message to Sent Mail folder
|
|
3431
|
+
let shouldCopy = !Object.prototype.hasOwnProperty.call(accountData, 'copy');
|
|
3432
|
+
|
|
3433
|
+
// Account specific setting
|
|
3434
|
+
if (typeof accountData.copy === 'boolean') {
|
|
3435
|
+
shouldCopy = accountData.copy;
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
// Suppress uploads for Gmail and Outlook
|
|
3439
|
+
// Unfortunately, previous default schema for all added accounts was copy=true, so can't prefer account specific setting here
|
|
3440
|
+
|
|
3441
|
+
// Emails for delegated accounts will be uploaded as the sender is different.
|
|
3442
|
+
// SMTP is disabled for shared mailboxes, so we need to send using the main account.
|
|
3443
|
+
let skipIfOutlook = this.isOutlook && (!accountData.oauth2 || !accountData.oauth2.auth || !accountData.oauth2.auth.delegatedUser);
|
|
3444
|
+
|
|
3445
|
+
if ((this.isGmail || skipIfOutlook) && !gatewayData) {
|
|
3446
|
+
shouldCopy = false;
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
// Message specific setting, overrides all other settings
|
|
3450
|
+
if (typeof data.copy === 'boolean') {
|
|
3451
|
+
shouldCopy = data.copy;
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
// Check if IMAP is available
|
|
3455
|
+
if ((!accountData.imap && !accountData.oauth2) || (accountData.imap && accountData.imap.disabled)) {
|
|
3456
|
+
// IMAP is disabled for this account
|
|
3457
|
+
shouldCopy = false;
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
let connectionOptions = { allowSecondary: true };
|
|
3461
|
+
|
|
3462
|
+
if (shouldCopy) {
|
|
3463
|
+
// NB! IMAP only
|
|
3464
|
+
// Upload the message to Sent Mail folder
|
|
3465
|
+
|
|
3466
|
+
try {
|
|
3467
|
+
this.checkIMAPConnection(connectionOptions);
|
|
3468
|
+
|
|
3469
|
+
// Find or use specified sent folder
|
|
3470
|
+
let sentMailbox =
|
|
3471
|
+
data.sentMailPath && typeof data.sentMailPath === 'string'
|
|
3472
|
+
? {
|
|
3473
|
+
path: data.sentMailPath
|
|
3474
|
+
}
|
|
3475
|
+
: await this.getSpecialUseMailbox('\\Sent');
|
|
3476
|
+
|
|
3477
|
+
if (sentMailbox) {
|
|
3478
|
+
if (raw.buffer) {
|
|
3479
|
+
// convert from a Uint8Array to a Buffer
|
|
3480
|
+
raw = Buffer.from(raw);
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3483
|
+
const connectionClient = await this.getImapConnection(connectionOptions, 'submitMessage');
|
|
3484
|
+
|
|
3485
|
+
// Upload message with Seen flag
|
|
3486
|
+
await connectionClient.append(sentMailbox.path, raw, ['\\Seen']);
|
|
3487
|
+
|
|
3488
|
+
// Return to IDLE if using primary connection
|
|
3489
|
+
if (connectionClient === this.imapClient && this.imapClient.mailbox && !this.imapClient.idling) {
|
|
3490
|
+
// force back to IDLE
|
|
3491
|
+
this.imapClient.idle().catch(err => {
|
|
3492
|
+
this.logger.error({ msg: 'IDLE error', err });
|
|
3493
|
+
});
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
} catch (err) {
|
|
3497
|
+
this.logger.error({ msg: 'Failed to upload Sent mail', queueId, messageId, err });
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
// Add \Answered flag to referenced message if needed
|
|
3502
|
+
if (reference && reference.update) {
|
|
3503
|
+
try {
|
|
3504
|
+
this.checkIMAPConnection(connectionOptions);
|
|
3505
|
+
await this.updateMessage(
|
|
3506
|
+
reference.message,
|
|
3507
|
+
{
|
|
3508
|
+
flags: {
|
|
3509
|
+
add: ['\\Answered'].concat(reference.action === 'forward' ? '$Forwarded' : [])
|
|
3510
|
+
}
|
|
3511
|
+
},
|
|
3512
|
+
connectionOptions
|
|
3513
|
+
);
|
|
3514
|
+
} catch (err) {
|
|
3515
|
+
this.logger.error({ msg: 'Failed to update reference flags', queueId, messageId, reference, err });
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
// Update feedback key if provided
|
|
3520
|
+
if (data.feedbackKey) {
|
|
3521
|
+
await this.redis
|
|
3522
|
+
.multi()
|
|
3523
|
+
.hset(data.feedbackKey, 'success', 'true')
|
|
3524
|
+
.expire(1 * 60 * 60);
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
// Update gateway usage stats
|
|
3528
|
+
if (gatewayData) {
|
|
3529
|
+
try {
|
|
3530
|
+
await gatewayObject.update({
|
|
3531
|
+
lastError: null,
|
|
3532
|
+
lastUse: new Date(),
|
|
3533
|
+
deliveries: { inc: 1 }
|
|
3534
|
+
});
|
|
3535
|
+
} catch (err) {
|
|
3536
|
+
this.logger.error({ msg: 'Failed to update gateway', queueId, messageId, reference, gateway: gatewayData.gateway, err });
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
return {
|
|
3541
|
+
response: info.response,
|
|
3542
|
+
messageId: info.messageId
|
|
3543
|
+
};
|
|
3544
|
+
} catch (err) {
|
|
3545
|
+
// Handle permanent failures
|
|
3546
|
+
if (err.responseCode >= 500 && jobData.opts?.attempts <= jobData.attemptsMade) {
|
|
3547
|
+
jobData.nextAttempt = false;
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
// Track SMTP errors for diagnostics
|
|
3551
|
+
let smtpStatus = false;
|
|
3552
|
+
switch (err.code) {
|
|
3553
|
+
case 'ESOCKET':
|
|
3554
|
+
if (err.cert && err.reason) {
|
|
3555
|
+
smtpStatus = {
|
|
3556
|
+
description: `Certificate check for ${smtpSettings.host}:${smtpSettings.port} failed. ${err.reason}`
|
|
3557
|
+
};
|
|
3558
|
+
}
|
|
3559
|
+
break;
|
|
3560
|
+
case 'EMESSAGE':
|
|
3561
|
+
case 'ESTREAM':
|
|
3562
|
+
case 'EENVELOPE':
|
|
3563
|
+
// Ignore. Too generic or message related
|
|
3564
|
+
break;
|
|
3565
|
+
case 'ETIMEDOUT':
|
|
3566
|
+
// firewall?
|
|
3567
|
+
smtpStatus = {
|
|
3568
|
+
description: `Request timed out. Possibly a firewall issue or a wrong hostname/port (${smtpSettings.host}:${smtpSettings.port}).`
|
|
3569
|
+
};
|
|
3570
|
+
break;
|
|
3571
|
+
case 'ETLS':
|
|
3572
|
+
smtpStatus = {
|
|
3573
|
+
description: `EmailEngine failed to set up TLS session with ${smtpSettings.host}:${smtpSettings.port}`
|
|
3574
|
+
};
|
|
3575
|
+
break;
|
|
3576
|
+
case 'EDNS':
|
|
3577
|
+
smtpStatus = {
|
|
3578
|
+
description: `EmailEngine failed to resolve DNS record for ${smtpSettings.host}`
|
|
3579
|
+
};
|
|
3580
|
+
break;
|
|
3581
|
+
case 'ECONNECTION':
|
|
3582
|
+
smtpStatus = {
|
|
3583
|
+
description: `EmailEngine failed to establish TCP connection against ${smtpSettings.host}`
|
|
3584
|
+
};
|
|
3585
|
+
break;
|
|
3586
|
+
case 'EPROTOCOL':
|
|
3587
|
+
smtpStatus = {
|
|
3588
|
+
description: `Unexpected response from ${smtpSettings.host}`
|
|
3589
|
+
};
|
|
3590
|
+
break;
|
|
3591
|
+
case 'EAUTH':
|
|
3592
|
+
smtpStatus = {
|
|
3593
|
+
description: `Authentication failed`
|
|
3594
|
+
};
|
|
3595
|
+
break;
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
if (smtpStatus) {
|
|
3599
|
+
let lastError = Object.assign(
|
|
3600
|
+
{
|
|
3601
|
+
created: Date.now(),
|
|
3602
|
+
status: 'error',
|
|
3603
|
+
response: err.response,
|
|
3604
|
+
responseCode: err.responseCode,
|
|
3605
|
+
code: err.code,
|
|
3606
|
+
command: err.command,
|
|
3607
|
+
networkRouting
|
|
3608
|
+
},
|
|
3609
|
+
smtpStatus
|
|
3610
|
+
);
|
|
3611
|
+
|
|
3612
|
+
// store SMTP error for the account
|
|
3613
|
+
try {
|
|
3614
|
+
await this.redis.hset(this.getAccountKey(), 'smtpStatus', JSON.stringify(lastError));
|
|
3615
|
+
} catch (err) {
|
|
3616
|
+
// ignore?
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
// Update gateway error status
|
|
3620
|
+
if (gatewayData) {
|
|
3621
|
+
try {
|
|
3622
|
+
await gatewayObject.update({
|
|
3623
|
+
lastError,
|
|
3624
|
+
lastUse: new Date()
|
|
3625
|
+
});
|
|
3626
|
+
} catch (err) {
|
|
3627
|
+
// ignore?
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
// Update feedback key with failure status
|
|
3633
|
+
if (data.feedbackKey && !jobData.nextAttempt) {
|
|
3634
|
+
await this.redis
|
|
3635
|
+
.multi()
|
|
3636
|
+
.hset(data.feedbackKey, 'success', 'false')
|
|
3637
|
+
.hset(data.feedbackKey, 'error', ((smtpStatus && smtpStatus.description) || '').toString() || 'Failed to send email')
|
|
3638
|
+
.expire(data.feedbackKey, 1 * 60 * 60)
|
|
3639
|
+
.exec();
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
// Send delivery error notification
|
|
3643
|
+
await this.notify(false, EMAIL_DELIVERY_ERROR_NOTIFY, {
|
|
3644
|
+
queueId,
|
|
3645
|
+
envelope,
|
|
3646
|
+
|
|
3647
|
+
messageId: data.messageId,
|
|
3648
|
+
|
|
3649
|
+
error: err.message,
|
|
3650
|
+
errorCode: err.code,
|
|
3651
|
+
|
|
3652
|
+
smtpResponse: err.response,
|
|
3653
|
+
smtpResponseCode: err.responseCode,
|
|
3654
|
+
smtpCommand: err.command,
|
|
3655
|
+
|
|
3656
|
+
networkRouting,
|
|
3657
|
+
|
|
3658
|
+
job: jobData
|
|
3659
|
+
});
|
|
3660
|
+
|
|
3661
|
+
// Enhance error with additional context
|
|
3662
|
+
err.code = err.code || 'SubmitFail';
|
|
3663
|
+
err.statusCode = Number(err.responseCode) || null;
|
|
3664
|
+
|
|
3665
|
+
err.info = { networkRouting };
|
|
3666
|
+
|
|
3667
|
+
throw err;
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
// stub - to be implemented by subclasses
|
|
3672
|
+
async listSignatures() {
|
|
3673
|
+
return { signatures: [], signaturesSupported: false };
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
module.exports = { BaseClient, metricsMeta };
|