@omnizap-system/omnizap 2.5.12
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/.clusterfuzzlite/Dockerfile +10 -0
- package/.env.example +907 -0
- package/.github/codeql/codeql-config.yml +10 -0
- package/.github/dependabot.yml +35 -0
- package/.github/workflows/ci.yml +73 -0
- package/.github/workflows/codeql.yml +106 -0
- package/.github/workflows/db-migration-check.yml +98 -0
- package/.github/workflows/dependency-review.yml +22 -0
- package/.github/workflows/deploy.yml +95 -0
- package/.github/workflows/release.yml +106 -0
- package/.github/workflows/security-attest-provenance.yml +51 -0
- package/.github/workflows/security-gitleaks.yml +34 -0
- package/.github/workflows/security-runner-hardening.yml +31 -0
- package/.github/workflows/security-scorecard.yml +44 -0
- package/.github/workflows/security-zap-baseline.yml +44 -0
- package/.github/workflows/security-zap-full-scan.yml +43 -0
- package/.github/workflows/security-zizmor.yml +36 -0
- package/.github/workflows/wiki-sync.yml +44 -0
- package/.gitleaks.toml +15 -0
- package/.prettierrc +34 -0
- package/CODE_OF_CONDUCT.md +114 -0
- package/LICENSE +56 -0
- package/README.md +110 -0
- package/SECURITY.md +110 -0
- package/app/config/index.js +4 -0
- package/app/configParts/adminIdentity.js +92 -0
- package/app/configParts/baileysConfig.js +1818 -0
- package/app/configParts/groupUtils.js +692 -0
- package/app/configParts/loggerConfig.js +394 -0
- package/app/configParts/messagePersistenceService.js +305 -0
- package/app/connection/baileysCompatibility.test.js +40 -0
- package/app/connection/baileysDbAuthState.js +344 -0
- package/app/connection/socketController.js +2243 -0
- package/app/controllers/messageController.js +7 -0
- package/app/controllers/messagePipeline/commandMiddleware.js +146 -0
- package/app/controllers/messagePipeline/conversationMiddleware.js +183 -0
- package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +522 -0
- package/app/controllers/messagePipeline/postProcessingMiddleware.js +41 -0
- package/app/controllers/messagePipeline/preProcessingMiddlewares.js +166 -0
- package/app/controllers/messageProcessingPipeline.js +699 -0
- package/app/modules/adminModule/AGENT.md +4056 -0
- package/app/modules/adminModule/adminAiHelpService.js +56 -0
- package/app/modules/adminModule/adminConfigRuntime.js +177 -0
- package/app/modules/adminModule/commandConfig.json +7122 -0
- package/app/modules/adminModule/groupCommandHandlers.js +1823 -0
- package/app/modules/adminModule/groupCommandHandlers.test.js +350 -0
- package/app/modules/adminModule/groupEventHandlers.js +399 -0
- package/app/modules/aiModule/AGENT.md +547 -0
- package/app/modules/aiModule/aiAiHelpService.js +14 -0
- package/app/modules/aiModule/aiConfigRuntime.js +135 -0
- package/app/modules/aiModule/catCommand.js +967 -0
- package/app/modules/aiModule/commandConfig.json +981 -0
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
- package/app/modules/gameModule/AGENT.md +196 -0
- package/app/modules/gameModule/commandConfig.json +366 -0
- package/app/modules/gameModule/diceCommand.js +42 -0
- package/app/modules/gameModule/gameAiHelpService.js +14 -0
- package/app/modules/gameModule/gameConfigRuntime.js +68 -0
- package/app/modules/menuModule/AGENT.md +205 -0
- package/app/modules/menuModule/commandConfig.json +366 -0
- package/app/modules/menuModule/common.js +316 -0
- package/app/modules/menuModule/menuAiHelpService.js +14 -0
- package/app/modules/menuModule/menuConfigRuntime.js +68 -0
- package/app/modules/menuModule/menus.js +66 -0
- package/app/modules/playModule/AGENT.md +321 -0
- package/app/modules/playModule/commandConfig.json +584 -0
- package/app/modules/playModule/playAiHelpService.js +14 -0
- package/app/modules/playModule/playCommand.js +1417 -0
- package/app/modules/playModule/playConfigRuntime.js +68 -0
- package/app/modules/quoteModule/AGENT.md +199 -0
- package/app/modules/quoteModule/commandConfig.json +366 -0
- package/app/modules/quoteModule/quoteAiHelpService.js +14 -0
- package/app/modules/quoteModule/quoteCommand.js +842 -0
- package/app/modules/quoteModule/quoteConfigRuntime.js +68 -0
- package/app/modules/rpgPokemonModule/AGENT.md +229 -0
- package/app/modules/rpgPokemonModule/commandConfig.json +386 -0
- package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +795 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.js +2110 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.test.js +770 -0
- package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
- package/app/modules/rpgPokemonModule/rpgPokemonAiHelpService.js +14 -0
- package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +174 -0
- package/app/modules/rpgPokemonModule/rpgPokemonConfigRuntime.js +68 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
- package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
- package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
- package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1847 -0
- package/app/modules/rpgPokemonModule/rpgPokemonService.js +6839 -0
- package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
- package/app/modules/statsModule/AGENT.md +320 -0
- package/app/modules/statsModule/commandConfig.json +540 -0
- package/app/modules/statsModule/globalRankingCommand.js +64 -0
- package/app/modules/statsModule/rankingCommand.js +41 -0
- package/app/modules/statsModule/rankingCommon.js +1305 -0
- package/app/modules/statsModule/statsAiHelpService.js +14 -0
- package/app/modules/statsModule/statsConfigRuntime.js +68 -0
- package/app/modules/stickerModule/AGENT.md +692 -0
- package/app/modules/stickerModule/addStickerMetadata.js +239 -0
- package/app/modules/stickerModule/commandConfig.json +1216 -0
- package/app/modules/stickerModule/convertToWebp.js +367 -0
- package/app/modules/stickerModule/stickerAiHelpService.js +14 -0
- package/app/modules/stickerModule/stickerCommand.js +446 -0
- package/app/modules/stickerModule/stickerConfigRuntime.js +68 -0
- package/app/modules/stickerModule/stickerConvertCommand.js +159 -0
- package/app/modules/stickerModule/stickerTextCommand.js +653 -0
- package/app/modules/stickerPackModule/AGENT.md +215 -0
- package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
- package/app/modules/stickerPackModule/autoPackCollectorService.js +357 -0
- package/app/modules/stickerPackModule/commandConfig.json +387 -0
- package/app/modules/stickerPackModule/domainEventOutboxRepository.js +227 -0
- package/app/modules/stickerPackModule/domainEvents.js +52 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.js +429 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +75 -0
- package/app/modules/stickerPackModule/semanticThemeClusterService.js +544 -0
- package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +400 -0
- package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
- package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +175 -0
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +3702 -0
- package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +559 -0
- package/app/modules/stickerPackModule/stickerClassificationService.js +557 -0
- package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +249 -0
- package/app/modules/stickerPackModule/stickerDomainEventBus.js +65 -0
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +208 -0
- package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +99 -0
- package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
- package/app/modules/stickerPackModule/stickerPackAiHelpService.js +14 -0
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1148 -0
- package/app/modules/stickerPackModule/stickerPackConfigRuntime.js +68 -0
- package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +152 -0
- package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
- package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +101 -0
- package/app/modules/stickerPackModule/stickerPackItemRepository.js +432 -0
- package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +313 -0
- package/app/modules/stickerPackModule/stickerPackMessageService.js +268 -0
- package/app/modules/stickerPackModule/stickerPackRepository.js +450 -0
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +179 -0
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +271 -0
- package/app/modules/stickerPackModule/stickerPackService.js +733 -0
- package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +32 -0
- package/app/modules/stickerPackModule/stickerPackUtils.js +107 -0
- package/app/modules/stickerPackModule/stickerStorageService.js +559 -0
- package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +242 -0
- package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +242 -0
- package/app/modules/systemMetricsModule/AGENT.md +193 -0
- package/app/modules/systemMetricsModule/commandConfig.json +344 -0
- package/app/modules/systemMetricsModule/pingCommand.js +399 -0
- package/app/modules/systemMetricsModule/systemMetricsAiHelpService.js +14 -0
- package/app/modules/systemMetricsModule/systemMetricsConfigRuntime.js +68 -0
- package/app/modules/tiktokModule/AGENT.md +196 -0
- package/app/modules/tiktokModule/commandConfig.json +366 -0
- package/app/modules/tiktokModule/tiktokAiHelpService.js +14 -0
- package/app/modules/tiktokModule/tiktokCommand.js +716 -0
- package/app/modules/tiktokModule/tiktokConfigRuntime.js +68 -0
- package/app/modules/userModule/AGENT.md +200 -0
- package/app/modules/userModule/commandConfig.json +386 -0
- package/app/modules/userModule/userAiHelpService.js +14 -0
- package/app/modules/userModule/userCommand.js +1155 -0
- package/app/modules/userModule/userConfigRuntime.js +68 -0
- package/app/modules/waifuPicsModule/AGENT.md +431 -0
- package/app/modules/waifuPicsModule/commandConfig.json +780 -0
- package/app/modules/waifuPicsModule/waifuPicsAiHelpService.js +14 -0
- package/app/modules/waifuPicsModule/waifuPicsCommand.js +586 -0
- package/app/modules/waifuPicsModule/waifuPicsConfigRuntime.js +68 -0
- package/app/observability/metrics.js +766 -0
- package/app/services/ai/aiHelpResponseCacheRepository.js +280 -0
- package/app/services/ai/aiLearningRepository.js +400 -0
- package/app/services/ai/commandConfigEnrichmentRepository.js +769 -0
- package/app/services/ai/commandConfigEnrichmentService.js +452 -0
- package/app/services/ai/commandConfigValidationService.js +443 -0
- package/app/services/ai/commandToolBuilderService.js +192 -0
- package/app/services/ai/conversationRouterService.js +516 -0
- package/app/services/ai/geminiService.js +115 -0
- package/app/services/ai/geminiService.test.js +87 -0
- package/app/services/ai/globalModuleAiHelpService.js +1412 -0
- package/app/services/ai/globalToolCallingService.js +203 -0
- package/app/services/ai/messageCommandExecutionService.js +391 -0
- package/app/services/ai/moduleAiHelpCoreService.js +1099 -0
- package/app/services/ai/moduleAiHelpWrapperFactory.js +65 -0
- package/app/services/ai/moduleCommandConfigRuntimeService.js +113 -0
- package/app/services/ai/moduleToolExecutorService.js +464 -0
- package/app/services/ai/moduleToolRegistryService.js +178 -0
- package/app/services/ai/toolCandidateSelectorService.js +781 -0
- package/app/services/auth/googleWebLinkService.js +80 -0
- package/app/services/auth/whatsappLoginLinkService.js +230 -0
- package/app/services/external/pokeApiService.js +398 -0
- package/app/services/group/groupMetadataService.js +311 -0
- package/app/services/infra/dbWriteQueue.js +874 -0
- package/app/services/infra/featureFlagService.js +131 -0
- package/app/services/infra/queueUtils.js +55 -0
- package/app/services/messaging/captchaService.js +491 -0
- package/app/services/messaging/messagePersistenceService.js +1 -0
- package/app/services/messaging/newsBroadcastService.js +347 -0
- package/app/services/sticker/stickerFocusService.js +347 -0
- package/app/services/sticker/stickerFocusService.test.js +43 -0
- package/app/store/aiPromptStore.js +38 -0
- package/app/store/conversationSessionStore.js +131 -0
- package/app/store/groupConfigStore.js +58 -0
- package/app/store/premiumUserStore.js +54 -0
- package/app/utils/antiLink/antiLinkModule.js +700 -0
- package/app/utils/http/getImageBufferModule.js +18 -0
- package/app/utils/json/jsonSanitizer.js +113 -0
- package/app/utils/json/jsonSanitizer.test.js +40 -0
- package/app/utils/systemMetrics/systemMetricsModule.js +88 -0
- package/app/workers/aiLearningWorker.js +605 -0
- package/app/workers/commandConfigEnrichmentWorker.js +242 -0
- package/database/index.js +2075 -0
- package/database/init.js +151 -0
- package/database/migrations/.gitkeep +0 -0
- package/database/migrations/20260307_d0_hardening_down.sql +64 -0
- package/database/migrations/20260307_d0_hardening_up.sql +79 -0
- package/database/migrations/20260307_d1_terms_acceptance_down.sql +11 -0
- package/database/migrations/20260307_d1_terms_acceptance_up.sql +37 -0
- package/database/migrations/20260307_d2_auth_hardening_down.sql +75 -0
- package/database/migrations/20260307_d2_auth_hardening_up.sql +100 -0
- package/database/migrations/20260314_d7_canonical_sender_down.sql +53 -0
- package/database/migrations/20260314_d7_canonical_sender_up.sql +114 -0
- package/database/migrations/20260406_d30_security_analytics_down.sql +95 -0
- package/database/migrations/20260406_d30_security_analytics_up.sql +292 -0
- package/database/migrations/20260407_d31_web_google_session_token_hardening_down.sql +2 -0
- package/database/migrations/20260407_d31_web_google_session_token_hardening_up.sql +17 -0
- package/database/migrations/20260408_d32_ai_help_response_cache_down.sql +1 -0
- package/database/migrations/20260408_d32_ai_help_response_cache_up.sql +22 -0
- package/database/migrations/20260409_d33_ai_learning_tables_down.sql +4 -0
- package/database/migrations/20260409_d33_ai_learning_tables_up.sql +52 -0
- package/database/migrations/20260410_d34_command_config_enrichment_down.sql +3 -0
- package/database/migrations/20260410_d34_command_config_enrichment_up.sql +48 -0
- package/database/schema.sql +1186 -0
- package/docker-compose.yml +104 -0
- package/docs/audits/stickerCatalogController-out-of-scope.md +103 -0
- package/docs/audits/stickerCatalogController-symbols.md +58 -0
- package/docs/compliance/acceptable-use-policy-2026-03-07.md +35 -0
- package/docs/compliance/dpa-b2b-standard-2026-03-07.md +80 -0
- package/docs/compliance/monthly-compliance-checklist-2026-03-07.md +88 -0
- package/docs/compliance/notice-and-takedown-policy-2026-03-07.md +34 -0
- package/docs/compliance/privacy-policy-2026-03-07.md +75 -0
- package/docs/compliance/subprocessors-inventory-2026-03-07.md +16 -0
- package/docs/database/production-db-evolution-runbook-2026q1.md +365 -0
- package/docs/security/dsar-lgpd-runbook-2026-03-07.md +86 -0
- package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +77 -0
- package/docs/security/network-hardening-runbook-2026-03-07.md +137 -0
- package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +238 -0
- package/docs/seo/satellite-page-template.md +116 -0
- package/docs/seo/satellite-pages-phase1.json +364 -0
- package/docs/wiki/Home.md +120 -0
- package/docs/wiki/pair-extraordinaire-2026-03-08.md +3 -0
- package/docs/wiki/recent-changes-2026-03-08.md +47 -0
- package/ecosystem.prod.config.cjs +135 -0
- package/eslint.config.js +89 -0
- package/index.js +488 -0
- package/ml/clip_classifier/Dockerfile +18 -0
- package/ml/clip_classifier/README.md +118 -0
- package/ml/clip_classifier/adaptive_scoring.py +40 -0
- package/ml/clip_classifier/classifier.py +654 -0
- package/ml/clip_classifier/embedding_store.py +481 -0
- package/ml/clip_classifier/env_loader.py +15 -0
- package/ml/clip_classifier/llm_label_expander.py +144 -0
- package/ml/clip_classifier/main.py +213 -0
- package/ml/clip_classifier/requirements.txt +10 -0
- package/ml/clip_classifier/similarity_engine.py +74 -0
- package/new-logo.png +0 -0
- package/observability/alert-rules.yml +60 -0
- package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
- package/observability/grafana/dashboards/omnizap-overview.json +170 -0
- package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
- package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
- package/observability/loki-config.yml +38 -0
- package/observability/mysql-setup.sql +46 -0
- package/observability/prometheus.yml +35 -0
- package/observability/promtail-config.yml +84 -0
- package/observability/sticker-catalog-slo.md +83 -0
- package/observability/sticker-scale-hardening-rollout.md +128 -0
- package/package.json +144 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/css/commands-react.input.css +71 -0
- package/public/assets/css/create-pack-react.input.css +31 -0
- package/public/assets/css/home-react.input.css +106 -0
- package/public/assets/css/login-react.input.css +58 -0
- package/public/assets/css/stickers-react.input.css +18 -0
- package/public/assets/css/terms-react.input.css +115 -0
- package/public/assets/css/user-react.input.css +57 -0
- package/public/assets/images/brand-icon-192.png +0 -0
- package/public/assets/images/brand-logo-128.webp +0 -0
- package/public/assets/images/hero-banner-1280.jpg +0 -0
- package/public/comandos/commands-catalog.json +4517 -0
- package/public/css/api-docs.css +161 -0
- package/public/css/stickers-admin.css +1288 -0
- package/public/css/styles.css +679 -0
- package/public/css/systemadm/admin.css +474 -0
- package/public/css/systemadm/base.css +73 -0
- package/public/css/systemadm/components.css +662 -0
- package/public/css/systemadm/layout.css +229 -0
- package/public/css/systemadm/tokens.css +56 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/js/apps/apiDocsApp.js +235 -0
- package/public/js/apps/commandsReactApp.js +528 -0
- package/public/js/apps/createPackApp.js +1646 -0
- package/public/js/apps/homeReactApp.js +942 -0
- package/public/js/apps/loginReactApp.js +496 -0
- package/public/js/apps/stickersAdminApp.js +1753 -0
- package/public/js/apps/stickersApp.js +3797 -0
- package/public/js/apps/termsReactApp.js +528 -0
- package/public/js/apps/userApp.js +2540 -0
- package/public/js/apps/userProfile/actions.js +66 -0
- package/public/js/apps/userReactApp.js +547 -0
- package/public/js/catalog.js +950 -0
- package/public/pages/api-docs.html +40 -0
- package/public/pages/aup.html +158 -0
- package/public/pages/comandos.html +41 -0
- package/public/pages/dpa.html +227 -0
- package/public/pages/home.html +45 -0
- package/public/pages/licenca.html +182 -0
- package/public/pages/login.html +40 -0
- package/public/pages/notice-and-takedown.html +234 -0
- package/public/pages/politica-de-privacidade.html +251 -0
- package/public/pages/seo-bot-whatsapp-para-grupo.html +350 -0
- package/public/pages/seo-bot-whatsapp-sem-programar.html +350 -0
- package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +350 -0
- package/public/pages/seo-como-criar-comandos-whatsapp.html +350 -0
- package/public/pages/seo-como-evitar-spam-no-whatsapp.html +350 -0
- package/public/pages/seo-como-moderar-grupo-whatsapp.html +350 -0
- package/public/pages/seo-como-organizar-comunidade-whatsapp.html +350 -0
- package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +350 -0
- package/public/pages/stickers-admin.html +31 -0
- package/public/pages/stickers-create.html +41 -0
- package/public/pages/stickers.html +45 -0
- package/public/pages/suboperadores.html +237 -0
- package/public/pages/termos-de-uso-texto-integral.html +241 -0
- package/public/pages/termos-de-uso.html +41 -0
- package/public/pages/user-password-reset.html +32 -0
- package/public/pages/user-systemadm.html +508 -0
- package/public/pages/user.html +39 -0
- package/public/robots.txt +9 -0
- package/public/site.webmanifest +24 -0
- package/public/sitemap.xml +98 -0
- package/schemas/command-config.schema.json +582 -0
- package/scripts/baileys-compat-smoke.mjs +12 -0
- package/scripts/cache-bust.mjs +142 -0
- package/scripts/deploy.sh +916 -0
- package/scripts/email-broadcast-terms-update.mjs +170 -0
- package/scripts/enrich-command-discovery-fields.mjs +286 -0
- package/scripts/generate-command-config-schema.mjs +273 -0
- package/scripts/generate-commands-catalog.mjs +308 -0
- package/scripts/generate-module-agents.mjs +631 -0
- package/scripts/generate-seo-satellite-pages.mjs +400 -0
- package/scripts/github-deploy-notify.mjs +174 -0
- package/scripts/github-release-notify.mjs +219 -0
- package/scripts/release.sh +599 -0
- package/scripts/run-codeql-local.sh +116 -0
- package/scripts/run-prettier-all.mjs +25 -0
- package/scripts/security-smoketest.mjs +581 -0
- package/scripts/sticker-catalog-loadtest.mjs +210 -0
- package/scripts/sticker-worker-task.mjs +119 -0
- package/scripts/sync-readme-snapshot.mjs +133 -0
- package/scripts/validate-command-config-schema.mjs +130 -0
- package/scripts/validate-command-configs.mjs +15 -0
- package/scripts/wiki-sync.sh +191 -0
- package/server/auth/googleWebAuth/googleWebAuthRuntime.js +62 -0
- package/server/auth/googleWebAuth/googleWebAuthService.js +807 -0
- package/server/auth/jwt/webJwtService.js +147 -0
- package/server/auth/stickerCatalogAuthContext.js +165 -0
- package/server/auth/termsAcceptance/termsAcceptanceHandler.js +189 -0
- package/server/auth/userPassword/index.js +14 -0
- package/server/auth/userPassword/userPasswordAuthService.js +422 -0
- package/server/auth/userPassword/userPasswordCrypto.js +199 -0
- package/server/auth/userPassword/userPasswordCrypto.test.js +76 -0
- package/server/auth/userPassword/userPasswordRecoveryService.js +728 -0
- package/server/auth/validation/authSchemas.js +236 -0
- package/server/auth/webAccount/webAccountHandlers.js +1434 -0
- package/server/controllers/admin/adminBanService.js +138 -0
- package/server/controllers/admin/adminPanelHandlers.js +2083 -0
- package/server/controllers/admin/stickerCatalogAdminContext.js +17 -0
- package/server/controllers/admin/systemAdminController.js +201 -0
- package/server/controllers/email/emailAutomationController.js +239 -0
- package/server/controllers/metricsController.js +21 -0
- package/server/controllers/seo/stickerCatalogSeoContext.js +514 -0
- package/server/controllers/sticker/nonCatalogHandlers.js +303 -0
- package/server/controllers/sticker/stickerCatalogController.js +4700 -0
- package/server/controllers/system/contactController.js +115 -0
- package/server/controllers/system/githubController.js +137 -0
- package/server/controllers/system/stickerCatalogSystemContext.js +758 -0
- package/server/controllers/system/storageController.js +154 -0
- package/server/controllers/system/systemController.js +135 -0
- package/server/controllers/system/systemMetricsController.js +156 -0
- package/server/controllers/system/visitController.js +90 -0
- package/server/controllers/userController.js +145 -0
- package/server/email/emailAutomationRuntime.js +225 -0
- package/server/email/emailAutomationService.js +125 -0
- package/server/email/emailOutboxRepository.js +282 -0
- package/server/email/emailTemplateService.js +480 -0
- package/server/email/emailTransportService.js +156 -0
- package/server/http/clientIp.js +95 -0
- package/server/http/httpRequestUtils.js +262 -0
- package/server/http/httpRequestUtils.test.js +80 -0
- package/server/http/httpServer.js +180 -0
- package/server/http/requestContext.js +20 -0
- package/server/http/siteRoutingUtils.js +87 -0
- package/server/index.js +1 -0
- package/server/middleware/cachePolicy.js +26 -0
- package/server/middleware/cachePolicyHelpers.js +1 -0
- package/server/middleware/endpointRateLimit.js +181 -0
- package/server/middleware/rateLimit.js +70 -0
- package/server/middleware/requireAdminAuth.js +48 -0
- package/server/middleware/securityHeaders.js +97 -0
- package/server/routes/admin/systemAdminRouter.js +64 -0
- package/server/routes/email/emailAutomationRouter.js +46 -0
- package/server/routes/health/healthRouter.js +41 -0
- package/server/routes/indexRouter.js +234 -0
- package/server/routes/metrics/metricsRouter.js +58 -0
- package/server/routes/static/staticPageRouter.js +134 -0
- package/server/routes/sticker/catalogHandlers/catalogAdminHttp.js +105 -0
- package/server/routes/sticker/catalogHandlers/catalogAuthHttp.js +77 -0
- package/server/routes/sticker/catalogHandlers/catalogPublicHttp.js +120 -0
- package/server/routes/sticker/catalogHandlers/catalogUploadHttp.js +83 -0
- package/server/routes/sticker/catalogRouter.js +77 -0
- package/server/routes/sticker/stickerApiRouter.js +84 -0
- package/server/routes/sticker/stickerDataRouter.js +145 -0
- package/server/routes/sticker/stickerSiteRouter.js +43 -0
- package/server/routes/user/userApiPaths.js +66 -0
- package/server/routes/user/userRouter.js +65 -0
- package/server/utils/safePath.js +26 -0
- package/utils/logger/loggerModule.js +35 -0
- package/vite.config.mjs +38 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
const DEFAULT_SITE_ORIGIN = 'https://omnizap.shop';
|
|
2
|
+
const DEFAULT_BRAND_NAME = 'OmniZap';
|
|
3
|
+
|
|
4
|
+
const resolveSiteOrigin = () =>
|
|
5
|
+
String(process.env.SITE_ORIGIN || process.env.WHATSAPP_LOGIN_BASE_URL || DEFAULT_SITE_ORIGIN)
|
|
6
|
+
.trim()
|
|
7
|
+
.replace(/\/+$/, '') || DEFAULT_SITE_ORIGIN;
|
|
8
|
+
|
|
9
|
+
const normalizeTemplateKey = (value) =>
|
|
10
|
+
String(value || '')
|
|
11
|
+
.trim()
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/[^a-z0-9_:-]/g, '')
|
|
14
|
+
.slice(0, 64);
|
|
15
|
+
|
|
16
|
+
const normalizeText = (value, maxLength = 500) =>
|
|
17
|
+
String(value || '')
|
|
18
|
+
.trim()
|
|
19
|
+
.slice(0, maxLength);
|
|
20
|
+
|
|
21
|
+
const escapeHtml = (value) =>
|
|
22
|
+
String(value || '')
|
|
23
|
+
.replace(/&/g, '&')
|
|
24
|
+
.replace(/</g, '<')
|
|
25
|
+
.replace(/>/g, '>')
|
|
26
|
+
.replace(/"/g, '"')
|
|
27
|
+
.replace(/'/g, ''');
|
|
28
|
+
|
|
29
|
+
const normalizeHttpUrl = (value, fallback = '') => {
|
|
30
|
+
const normalized = String(value || '')
|
|
31
|
+
.trim()
|
|
32
|
+
.replace(/\s+/g, '');
|
|
33
|
+
if (!normalized) return fallback;
|
|
34
|
+
if (!/^https?:\/\//i.test(normalized)) return fallback;
|
|
35
|
+
return normalized.slice(0, 2_000);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const normalizeEmailAddress = (value) => {
|
|
39
|
+
const raw = String(value || '').trim();
|
|
40
|
+
if (!raw) return '';
|
|
41
|
+
const betweenAngles = raw.match(/<([^>]+)>/);
|
|
42
|
+
const candidate = String(betweenAngles?.[1] || raw)
|
|
43
|
+
.trim()
|
|
44
|
+
.replace(/^mailto:/i, '');
|
|
45
|
+
if (!candidate.includes('@')) return '';
|
|
46
|
+
return candidate.slice(0, 255);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const normalizePhoneDigits = (value, maxLength = 20) =>
|
|
50
|
+
String(value || '')
|
|
51
|
+
.replace(/\D+/g, '')
|
|
52
|
+
.slice(0, maxLength);
|
|
53
|
+
|
|
54
|
+
const isLikelyPhoneDigits = (digits) => {
|
|
55
|
+
const normalized = normalizePhoneDigits(digits, 20);
|
|
56
|
+
return normalized.length >= 10 && normalized.length <= 15;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const formatPhonePn = (digits) => {
|
|
60
|
+
const normalized = normalizePhoneDigits(digits, 20);
|
|
61
|
+
if (!normalized) return '';
|
|
62
|
+
|
|
63
|
+
if (normalized.length === 13 && normalized.startsWith('55')) {
|
|
64
|
+
return `+${normalized.slice(0, 2)} ${normalized.slice(2, 4)} ${normalized.slice(4, 9)}-${normalized.slice(9, 13)}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (normalized.length === 12 && normalized.startsWith('55')) {
|
|
68
|
+
return `+${normalized.slice(0, 2)} ${normalized.slice(2, 4)} ${normalized.slice(4, 8)}-${normalized.slice(8, 12)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return `+${normalized}`;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const resolveBrandConfig = (payload = {}) => {
|
|
75
|
+
const siteOrigin = normalizeHttpUrl(payload?.siteOrigin || resolveSiteOrigin(), resolveSiteOrigin());
|
|
76
|
+
const supportFallback = `${siteOrigin}/termos-de-uso/`;
|
|
77
|
+
const replyToAddress = normalizeEmailAddress(payload?.replyTo || process.env.SMTP_REPLY_TO || process.env.EMAIL_REPLY_TO || process.env.MAIL_REPLY_TO || '');
|
|
78
|
+
const fromAddress = normalizeEmailAddress(process.env.SMTP_FROM || process.env.EMAIL_FROM || process.env.MAIL_FROM || process.env.SMTP_USER || process.env.EMAIL_USER || process.env.MAIL_USER || '');
|
|
79
|
+
const supportPhoneCandidate = normalizePhoneDigits(payload?.supportPhone || process.env.EMAIL_BRAND_SUPPORT_PHONE || process.env.WHATSAPP_SUPPORT_NUMBER || process.env.OWNER_NUMBER || '', 20);
|
|
80
|
+
const supportPhoneDigits = isLikelyPhoneDigits(supportPhoneCandidate) ? supportPhoneCandidate : '';
|
|
81
|
+
const supportPhonePn = formatPhonePn(supportPhoneDigits);
|
|
82
|
+
const supportWhatsappUrl = supportPhoneDigits ? `https://wa.me/${supportPhoneDigits}` : '';
|
|
83
|
+
const resolvedSupportUrl = normalizeHttpUrl(payload?.supportUrl || supportWhatsappUrl || process.env.EMAIL_BRAND_SUPPORT_URL || supportFallback, supportFallback);
|
|
84
|
+
const resolvedSupportLabel = normalizeText(payload?.supportLabel || supportPhonePn || process.env.EMAIL_BRAND_SUPPORT_LABEL || 'Central de suporte', 80);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
siteOrigin,
|
|
88
|
+
brandName: normalizeText(payload?.brandName || process.env.EMAIL_BRAND_NAME || DEFAULT_BRAND_NAME, 80) || DEFAULT_BRAND_NAME,
|
|
89
|
+
brandTagline: normalizeText(payload?.brandTagline || process.env.EMAIL_BRAND_TAGLINE || 'Automação profissional para WhatsApp.', 120) || null,
|
|
90
|
+
brandLogoUrl: normalizeHttpUrl(payload?.brandLogoUrl || payload?.logoUrl || process.env.EMAIL_BRAND_LOGO_URL || '', ''),
|
|
91
|
+
supportUrl: resolvedSupportUrl,
|
|
92
|
+
supportLabel: resolvedSupportLabel || 'Central de suporte',
|
|
93
|
+
supportEmail: replyToAddress || fromAddress || '',
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const renderParagraphsHtml = (value, { maxParagraphs = 6, maxLength = 8_000 } = {}) => {
|
|
98
|
+
const normalized = String(value || '')
|
|
99
|
+
.replace(/\r/g, '')
|
|
100
|
+
.trim()
|
|
101
|
+
.slice(0, maxLength);
|
|
102
|
+
if (!normalized) return '';
|
|
103
|
+
|
|
104
|
+
const paragraphs = normalized
|
|
105
|
+
.split(/\n{2,}/)
|
|
106
|
+
.map((entry) => entry.trim())
|
|
107
|
+
.filter(Boolean)
|
|
108
|
+
.slice(0, maxParagraphs);
|
|
109
|
+
|
|
110
|
+
if (!paragraphs.length) return '';
|
|
111
|
+
|
|
112
|
+
return paragraphs.map((paragraph) => `<p style="margin:0 0 12px;color:#334155;font-size:15px;line-height:1.65;">${escapeHtml(paragraph)}</p>`).join('');
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const renderEmailLayout = ({ payload = {}, preheader = '', heading = '', greeting = '', intro = '', body = '', ctaLabel = '', ctaUrl = '', ctaHint = '', secondaryCtaLabel = '', secondaryCtaUrl = '', securityNote = '', footerMessage = '' } = {}) => {
|
|
116
|
+
const brand = resolveBrandConfig(payload);
|
|
117
|
+
const safePreheader = normalizeText(preheader, 160);
|
|
118
|
+
const safeHeading = normalizeText(heading, 120);
|
|
119
|
+
const safeGreeting = normalizeText(greeting, 150);
|
|
120
|
+
const safeIntro = normalizeText(intro, 300);
|
|
121
|
+
const safeCtaLabel = normalizeText(ctaLabel, 80);
|
|
122
|
+
const safeCtaUrl = normalizeHttpUrl(ctaUrl, '');
|
|
123
|
+
const safeCtaHint = normalizeText(ctaHint, 220);
|
|
124
|
+
const safeSecondaryCtaLabel = normalizeText(secondaryCtaLabel, 80);
|
|
125
|
+
const safeSecondaryCtaUrl = normalizeHttpUrl(secondaryCtaUrl, '');
|
|
126
|
+
const safeSecurityNote = normalizeText(securityNote, 220);
|
|
127
|
+
const safeFooterMessage = normalizeText(footerMessage, 220);
|
|
128
|
+
const year = new Date().getUTCFullYear();
|
|
129
|
+
|
|
130
|
+
const logoBlock = brand.brandLogoUrl ? `<img src="${escapeHtml(brand.brandLogoUrl)}" alt="${escapeHtml(brand.brandName)}" width="132" style="display:block;border:0;outline:none;text-decoration:none;height:auto;margin:0 auto;" />` : `<div style="display:inline-block;font-size:26px;font-weight:800;color:#0f172a;letter-spacing:0.2px;">${escapeHtml(brand.brandName)}</div>`;
|
|
131
|
+
|
|
132
|
+
const greetingBlock = safeGreeting ? `<p style="margin:0 0 10px;color:#0f172a;font-size:16px;font-weight:700;line-height:1.5;">${escapeHtml(safeGreeting)}</p>` : '';
|
|
133
|
+
const introBlock = safeIntro ? `<p style="margin:0 0 14px;color:#334155;font-size:15px;line-height:1.65;">${escapeHtml(safeIntro)}</p>` : '';
|
|
134
|
+
const bodyBlock = renderParagraphsHtml(body);
|
|
135
|
+
|
|
136
|
+
const ctaBlock =
|
|
137
|
+
safeCtaUrl && safeCtaLabel
|
|
138
|
+
? `
|
|
139
|
+
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:10px 0 10px;">
|
|
140
|
+
<tr>
|
|
141
|
+
<td align="center" bgcolor="#1d4ed8" style="border-radius:10px;">
|
|
142
|
+
<a href="${escapeHtml(safeCtaUrl)}" style="display:inline-block;padding:12px 20px;font-size:15px;line-height:1.2;font-weight:700;color:#ffffff;text-decoration:none;">${escapeHtml(safeCtaLabel)}</a>
|
|
143
|
+
</td>
|
|
144
|
+
</tr>
|
|
145
|
+
</table>
|
|
146
|
+
`.trim()
|
|
147
|
+
: '';
|
|
148
|
+
|
|
149
|
+
const ctaHintBlock = safeCtaHint ? `<p style="margin:4px 0 0;color:#64748b;font-size:13px;line-height:1.55;">${escapeHtml(safeCtaHint)}</p>` : '';
|
|
150
|
+
const secondaryCtaBlock = safeSecondaryCtaLabel && safeSecondaryCtaUrl ? `<p style="margin:10px 0 0;color:#1e293b;font-size:14px;line-height:1.6;">${escapeHtml(safeSecondaryCtaLabel)}: <a href="${escapeHtml(safeSecondaryCtaUrl)}" style="color:#2563eb;text-decoration:none;">${escapeHtml(safeSecondaryCtaUrl)}</a></p>` : '';
|
|
151
|
+
const fallbackLinkBlock = safeCtaUrl ? `<p style="margin:12px 0 0;color:#64748b;font-size:12px;line-height:1.6;word-break:break-all;">Se o botão não funcionar, copie e cole este link no navegador: ${escapeHtml(safeCtaUrl)}</p>` : '';
|
|
152
|
+
const securityNoteBlock = safeSecurityNote ? `<p style="margin:16px 0 0;padding:10px 12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;color:#475569;font-size:12px;line-height:1.6;">${escapeHtml(safeSecurityNote)}</p>` : '';
|
|
153
|
+
|
|
154
|
+
const supportEmailLine = brand.supportEmail ? `<span style="display:block;margin-top:6px;">E-mail: <a href="mailto:${escapeHtml(brand.supportEmail)}" style="color:#2563eb;text-decoration:none;">${escapeHtml(brand.supportEmail)}</a></span>` : '';
|
|
155
|
+
const footerMessageBlock = safeFooterMessage ? `<span style="display:block;margin-top:6px;color:#64748b;">${escapeHtml(safeFooterMessage)}</span>` : '';
|
|
156
|
+
const taglineBlock = brand.brandTagline ? `<p style="margin:8px 0 0;color:#64748b;font-size:13px;line-height:1.6;">${escapeHtml(brand.brandTagline)}</p>` : '';
|
|
157
|
+
|
|
158
|
+
return `
|
|
159
|
+
<!doctype html>
|
|
160
|
+
<html lang="pt-BR">
|
|
161
|
+
<body style="margin:0;padding:0;background:#f1f5f9;font-family:'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
|
|
162
|
+
<div style="display:none;max-height:0;overflow:hidden;opacity:0;color:transparent;">${escapeHtml(safePreheader)}</div>
|
|
163
|
+
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f1f5f9;padding:28px 10px;">
|
|
164
|
+
<tr>
|
|
165
|
+
<td align="center">
|
|
166
|
+
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="max-width:640px;">
|
|
167
|
+
<tr>
|
|
168
|
+
<td align="center" style="padding:0 0 16px;">
|
|
169
|
+
${logoBlock}
|
|
170
|
+
${taglineBlock}
|
|
171
|
+
</td>
|
|
172
|
+
</tr>
|
|
173
|
+
<tr>
|
|
174
|
+
<td style="background:#ffffff;border:1px solid #dbe3ef;border-radius:14px;padding:26px 24px;box-shadow:0 4px 24px rgba(15,23,42,0.06);">
|
|
175
|
+
<h1 style="margin:0 0 14px;color:#0f172a;font-size:25px;line-height:1.25;">${escapeHtml(safeHeading)}</h1>
|
|
176
|
+
${greetingBlock}
|
|
177
|
+
${introBlock}
|
|
178
|
+
${bodyBlock}
|
|
179
|
+
${ctaBlock}
|
|
180
|
+
${ctaHintBlock}
|
|
181
|
+
${secondaryCtaBlock}
|
|
182
|
+
${fallbackLinkBlock}
|
|
183
|
+
${securityNoteBlock}
|
|
184
|
+
</td>
|
|
185
|
+
</tr>
|
|
186
|
+
<tr>
|
|
187
|
+
<td style="padding:14px 2px 0;color:#64748b;font-size:12px;line-height:1.7;text-align:left;">
|
|
188
|
+
<span style="display:block;">${escapeHtml(brand.brandName)} © ${year}. Todos os direitos reservados.</span>
|
|
189
|
+
<span style="display:block;margin-top:6px;">Central de suporte: <a href="${escapeHtml(brand.supportUrl)}" style="color:#2563eb;text-decoration:none;">${escapeHtml(brand.supportLabel)}</a></span>
|
|
190
|
+
${supportEmailLine}
|
|
191
|
+
${footerMessageBlock}
|
|
192
|
+
</td>
|
|
193
|
+
</tr>
|
|
194
|
+
</table>
|
|
195
|
+
</td>
|
|
196
|
+
</tr>
|
|
197
|
+
</table>
|
|
198
|
+
</body>
|
|
199
|
+
</html>
|
|
200
|
+
`.trim();
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const resolveNavigationLinks = (payload = {}) => {
|
|
204
|
+
const siteOrigin = normalizeHttpUrl(payload?.siteOrigin || resolveSiteOrigin(), resolveSiteOrigin());
|
|
205
|
+
const defaultRedirectUrl = normalizeHttpUrl(process.env.EMAIL_DEFAULT_REDIRECT_URL || `${siteOrigin}/user/`, `${siteOrigin}/user/`);
|
|
206
|
+
const defaultHomeUrl = normalizeHttpUrl(process.env.EMAIL_DEFAULT_CTA_URL || `${siteOrigin}/`, `${siteOrigin}/`);
|
|
207
|
+
const redirectUrl = normalizeHttpUrl(payload?.redirectUrl || payload?.userUrl || payload?.loginUrl || defaultRedirectUrl, defaultRedirectUrl);
|
|
208
|
+
const homeUrl = normalizeHttpUrl(payload?.ctaUrl || payload?.homeUrl || payload?.link || defaultHomeUrl, defaultHomeUrl);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
siteOrigin,
|
|
212
|
+
redirectUrl,
|
|
213
|
+
homeUrl,
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const resolveWelcomeBotWhatsApp = (payload = {}) => {
|
|
218
|
+
const botPhoneCandidate = normalizePhoneDigits(payload?.botPhone || payload?.botNumber || process.env.EMAIL_WELCOME_BOT_PHONE || process.env.WHATSAPP_BOT_NUMBER || process.env.BOT_NUMBER || process.env.BOT_PHONE_NUMBER || process.env.PHONE_NUMBER || process.env.EMAIL_BRAND_SUPPORT_PHONE || '', 20);
|
|
219
|
+
const botPhoneDigits = isLikelyPhoneDigits(botPhoneCandidate) ? botPhoneCandidate : '';
|
|
220
|
+
const botPhonePn = formatPhonePn(botPhoneDigits);
|
|
221
|
+
const botWhatsAppUrl = botPhoneDigits ? `https://wa.me/${botPhoneDigits}` : '';
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
botPhonePn,
|
|
225
|
+
botWhatsAppUrl,
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const resolveTermsUrl = (payload = {}) => {
|
|
230
|
+
const siteOrigin = normalizeHttpUrl(payload?.siteOrigin || resolveSiteOrigin(), resolveSiteOrigin());
|
|
231
|
+
const defaultTermsUrl = normalizeHttpUrl(`${siteOrigin}/termos-de-uso/`, `${DEFAULT_SITE_ORIGIN}/termos-de-uso/`);
|
|
232
|
+
return normalizeHttpUrl(payload?.termsUrl || process.env.EMAIL_TERMS_URL || defaultTermsUrl, defaultTermsUrl);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const buildWelcomeTemplate = (payload = {}) => {
|
|
236
|
+
const name = normalizeText(payload?.name || payload?.firstName || '', 80) || 'você';
|
|
237
|
+
const { redirectUrl, homeUrl } = resolveNavigationLinks(payload);
|
|
238
|
+
const { botPhonePn, botWhatsAppUrl } = resolveWelcomeBotWhatsApp(payload);
|
|
239
|
+
const ctaUrl = botWhatsAppUrl || homeUrl;
|
|
240
|
+
const ctaLabel = botWhatsAppUrl ? 'Abrir WhatsApp do Bot' : 'Abrir OmniZap';
|
|
241
|
+
const ctaHint = botPhonePn ? `WhatsApp do bot: ${botPhonePn}` : `Link de redirecionamento: ${redirectUrl}`;
|
|
242
|
+
const subject = 'Bem-vindo(a) ao OmniZap';
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
subject,
|
|
246
|
+
text: [`Olá, ${name}!`, '', 'Sua conta no OmniZap foi preparada e já está pronta para uso.', `Redirecionamento da conta: ${redirectUrl}`, botWhatsAppUrl ? `WhatsApp do bot: ${botWhatsAppUrl}` : `Abrir plataforma: ${homeUrl}`, '', 'Se você não solicitou este e-mail, desconsidere esta mensagem.'].join('\n'),
|
|
247
|
+
html: renderEmailLayout({
|
|
248
|
+
payload,
|
|
249
|
+
preheader: 'Sua conta no OmniZap está pronta para uso.',
|
|
250
|
+
heading: 'Bem-vindo(a) ao OmniZap',
|
|
251
|
+
greeting: `Olá, ${name}!`,
|
|
252
|
+
intro: 'Sua conta foi preparada com sucesso. Use o botão abaixo para abrir o WhatsApp do bot.',
|
|
253
|
+
ctaLabel,
|
|
254
|
+
ctaUrl,
|
|
255
|
+
ctaHint,
|
|
256
|
+
securityNote: 'Se você não reconhece esta ação, ignore este e-mail.',
|
|
257
|
+
footerMessage: 'Este e-mail foi enviado automaticamente pelo sistema.',
|
|
258
|
+
}),
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const buildMagicLinkTemplate = (payload = {}) => {
|
|
263
|
+
const name = normalizeText(payload?.name || payload?.firstName || '', 80) || 'você';
|
|
264
|
+
const link = normalizeHttpUrl(payload?.link || payload?.magicLink || payload?.loginUrl || '', '');
|
|
265
|
+
if (!link) return null;
|
|
266
|
+
|
|
267
|
+
const expiresInMinutes = Number(payload?.expiresInMinutes);
|
|
268
|
+
const expirationMessage = Number.isFinite(expiresInMinutes) && expiresInMinutes > 0 ? `Este link expira em ${Math.max(1, Math.floor(expiresInMinutes))} minuto(s).` : 'Este link pode expirar em alguns minutos.';
|
|
269
|
+
const subject = 'Seu link de acesso ao OmniZap';
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
subject,
|
|
273
|
+
text: [`Olá, ${name}!`, '', 'Use o link abaixo para acessar sua conta com segurança:', link, '', expirationMessage, 'Se você não solicitou este acesso, ignore esta mensagem.'].join('\n'),
|
|
274
|
+
html: renderEmailLayout({
|
|
275
|
+
payload,
|
|
276
|
+
preheader: 'Use seu link de acesso seguro do OmniZap.',
|
|
277
|
+
heading: 'Seu link de acesso',
|
|
278
|
+
greeting: `Olá, ${name}!`,
|
|
279
|
+
intro: 'Clique no botão abaixo para entrar na sua conta.',
|
|
280
|
+
ctaLabel: 'Acessar conta',
|
|
281
|
+
ctaUrl: link,
|
|
282
|
+
ctaHint: expirationMessage,
|
|
283
|
+
securityNote: 'Não compartilhe este link com terceiros.',
|
|
284
|
+
footerMessage: 'Para sua segurança, este link é pessoal e temporário.',
|
|
285
|
+
}),
|
|
286
|
+
};
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const buildPasswordResetCodeTemplate = (payload = {}) => {
|
|
290
|
+
const name = normalizeText(payload?.name || payload?.firstName || '', 80) || 'você';
|
|
291
|
+
const codeDigits = String(payload?.code || '')
|
|
292
|
+
.replace(/\D+/g, '')
|
|
293
|
+
.slice(0, 6);
|
|
294
|
+
if (!/^\d{6}$/.test(codeDigits)) return null;
|
|
295
|
+
|
|
296
|
+
const purpose = normalizeText(payload?.purpose || 'reset', 24).toLowerCase() === 'setup' ? 'setup' : 'reset';
|
|
297
|
+
const expiresInMinutesRaw = Number(payload?.expiresInMinutes);
|
|
298
|
+
const expiresInMinutes = Number.isFinite(expiresInMinutesRaw) ? Math.max(1, Math.min(60, Math.floor(expiresInMinutesRaw))) : 15;
|
|
299
|
+
const purposeLabel = purpose === 'setup' ? 'criação de senha' : 'redefinição de senha';
|
|
300
|
+
const subject = purpose === 'setup' ? 'Codigo para criar sua senha no OmniZap' : 'Codigo para redefinir sua senha no OmniZap';
|
|
301
|
+
const codeDisplay = `${codeDigits.slice(0, 3)} ${codeDigits.slice(3, 6)}`;
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
subject,
|
|
305
|
+
text: [`Olá, ${name}!`, '', `Use este código para concluir a ${purposeLabel}:`, codeDigits, '', `Este código expira em ${expiresInMinutes} minuto(s).`, 'Se você não solicitou esta ação, ignore este e-mail.'].join('\n'),
|
|
306
|
+
html: renderEmailLayout({
|
|
307
|
+
payload,
|
|
308
|
+
preheader: `Seu código de ${purposeLabel} do OmniZap.`,
|
|
309
|
+
heading: purpose === 'setup' ? 'Criacao de senha' : 'Redefinicao de senha',
|
|
310
|
+
greeting: `Olá, ${name}!`,
|
|
311
|
+
intro: `Use o codigo abaixo para concluir a ${purposeLabel}.`,
|
|
312
|
+
body: `Codigo de verificacao: ${codeDisplay}\nValidade: ${expiresInMinutes} minuto(s).`,
|
|
313
|
+
ctaLabel: '',
|
|
314
|
+
ctaUrl: '',
|
|
315
|
+
ctaHint: '',
|
|
316
|
+
securityNote: 'Nao compartilhe este codigo com terceiros. A equipe OmniZap nunca pede esse codigo por mensagem.',
|
|
317
|
+
footerMessage: 'Mensagem automatica de seguranca do OmniZap.',
|
|
318
|
+
}),
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const buildProjectUpdateTemplate = (payload = {}) => {
|
|
323
|
+
const name = normalizeText(payload?.name || payload?.firstName || '', 80) || 'usuário';
|
|
324
|
+
const title = normalizeText(payload?.title || payload?.heading || 'Atualização do projeto OmniZap', 120) || 'Atualização do projeto OmniZap';
|
|
325
|
+
const message = normalizeText(payload?.message || payload?.body || 'Temos uma nova atualização do projeto. Confira os detalhes no painel.', 6_000) || 'Temos uma nova atualização do projeto. Confira os detalhes no painel.';
|
|
326
|
+
const details = normalizeText(payload?.details || '', 6_000);
|
|
327
|
+
const ctaUrl = normalizeHttpUrl(payload?.ctaUrl || payload?.link || payload?.loginUrl || '', '');
|
|
328
|
+
const ctaLabel = normalizeText(payload?.ctaLabel || 'Ver atualização', 80) || 'Ver atualização';
|
|
329
|
+
const subject = normalizeText(payload?.subject || title, 180) || title;
|
|
330
|
+
|
|
331
|
+
const textLines = [`Olá, ${name}!`, '', title, '', message];
|
|
332
|
+
if (details) {
|
|
333
|
+
textLines.push('', details);
|
|
334
|
+
}
|
|
335
|
+
if (ctaUrl) {
|
|
336
|
+
textLines.push('', `Acesse: ${ctaUrl}`);
|
|
337
|
+
}
|
|
338
|
+
textLines.push('', 'Mensagem automática do OmniZap.');
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
subject,
|
|
342
|
+
text: textLines.join('\n'),
|
|
343
|
+
html: renderEmailLayout({
|
|
344
|
+
payload,
|
|
345
|
+
preheader: title,
|
|
346
|
+
heading: title,
|
|
347
|
+
greeting: `Olá, ${name}!`,
|
|
348
|
+
intro: message,
|
|
349
|
+
body: details,
|
|
350
|
+
ctaLabel: ctaUrl ? ctaLabel : '',
|
|
351
|
+
ctaUrl,
|
|
352
|
+
ctaHint: ctaUrl ? 'Abra o link para ver os detalhes completos.' : '',
|
|
353
|
+
securityNote: 'Se você não reconhece esta comunicação, entre em contato com o suporte.',
|
|
354
|
+
footerMessage: 'Comunicado oficial do projeto OmniZap.',
|
|
355
|
+
}),
|
|
356
|
+
};
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const buildStandardTemplate = (payload = {}) => {
|
|
360
|
+
const { redirectUrl, homeUrl } = resolveNavigationLinks(payload);
|
|
361
|
+
const name = normalizeText(payload?.name || payload?.firstName || '', 80);
|
|
362
|
+
const subject = normalizeText(payload?.subject || payload?.title || 'Comunicado OmniZap', 180) || 'Comunicado OmniZap';
|
|
363
|
+
const heading = normalizeText(payload?.heading || payload?.title || 'Atualização OmniZap', 120) || 'Atualização OmniZap';
|
|
364
|
+
const intro = normalizeText(payload?.intro || payload?.message || payload?.summary || 'Temos uma nova comunicação para você.', 2_000);
|
|
365
|
+
const details = normalizeText(payload?.body || payload?.details || '', 6_000);
|
|
366
|
+
const ctaUrl = homeUrl;
|
|
367
|
+
const ctaLabel = normalizeText(payload?.ctaLabel || 'Abrir OmniZap', 80) || 'Abrir OmniZap';
|
|
368
|
+
const ctaHint = normalizeText(payload?.ctaHint || `Link de redirecionamento: ${redirectUrl}`, 220);
|
|
369
|
+
const securityNote = normalizeText(payload?.securityNote || 'Se não reconhece esta mensagem, ignore e entre em contato com o suporte.', 220) || 'Se não reconhece esta mensagem, ignore e entre em contato com o suporte.';
|
|
370
|
+
const footerMessage = normalizeText(payload?.footerMessage || 'Mensagem automática do projeto OmniZap.', 220);
|
|
371
|
+
const greeting = name ? `Olá, ${name}!` : '';
|
|
372
|
+
|
|
373
|
+
const textLines = [];
|
|
374
|
+
if (greeting) {
|
|
375
|
+
textLines.push(greeting, '');
|
|
376
|
+
}
|
|
377
|
+
textLines.push(heading, '');
|
|
378
|
+
if (intro) {
|
|
379
|
+
textLines.push(intro);
|
|
380
|
+
}
|
|
381
|
+
if (details) {
|
|
382
|
+
textLines.push('', details);
|
|
383
|
+
}
|
|
384
|
+
if (redirectUrl) {
|
|
385
|
+
textLines.push('', `Redirecionamento da conta: ${redirectUrl}`);
|
|
386
|
+
}
|
|
387
|
+
if (ctaUrl) {
|
|
388
|
+
textLines.push('', `Abrir plataforma: ${ctaUrl}`);
|
|
389
|
+
}
|
|
390
|
+
textLines.push('', 'Mensagem automática do OmniZap.');
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
subject,
|
|
394
|
+
text: textLines.join('\n'),
|
|
395
|
+
html: renderEmailLayout({
|
|
396
|
+
payload,
|
|
397
|
+
preheader: heading,
|
|
398
|
+
heading,
|
|
399
|
+
greeting,
|
|
400
|
+
intro,
|
|
401
|
+
body: details,
|
|
402
|
+
ctaLabel: ctaUrl ? ctaLabel : '',
|
|
403
|
+
ctaUrl,
|
|
404
|
+
ctaHint: ctaUrl ? ctaHint : '',
|
|
405
|
+
securityNote,
|
|
406
|
+
footerMessage,
|
|
407
|
+
}),
|
|
408
|
+
};
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const buildTermsUpdateTemplate = (payload = {}) => {
|
|
412
|
+
const name = normalizeText(payload?.name || payload?.firstName || '', 80) || 'usuário';
|
|
413
|
+
const { botPhonePn, botWhatsAppUrl } = resolveWelcomeBotWhatsApp(payload);
|
|
414
|
+
const termsUrl = resolveTermsUrl(payload);
|
|
415
|
+
const fallbackOpenUrl = normalizeHttpUrl(process.env.EMAIL_DEFAULT_CTA_URL || `${resolveSiteOrigin()}/`, `${resolveSiteOrigin()}/`);
|
|
416
|
+
const ctaUrl = botWhatsAppUrl || fallbackOpenUrl;
|
|
417
|
+
const ctaLabel = botWhatsAppUrl ? 'Abrir WhatsApp do Bot' : 'Abrir OmniZap';
|
|
418
|
+
const subject = normalizeText(payload?.subject || 'Atualização dos Termos de Serviço do OmniZap', 180) || 'Atualização dos Termos de Serviço do OmniZap';
|
|
419
|
+
const heading = normalizeText(payload?.heading || 'Atualização dos Termos de Serviço', 120) || 'Atualização dos Termos de Serviço';
|
|
420
|
+
const intro = normalizeText(payload?.intro || payload?.message || 'Atualizamos nossos Termos de Serviço para refletir melhorias operacionais, de segurança e de comunicação do projeto.', 2_000) || 'Atualizamos nossos Termos de Serviço para refletir melhorias operacionais, de segurança e de comunicação do projeto.';
|
|
421
|
+
const body = normalizeText(payload?.body || 'Recomendamos a leitura da nova versão para entender como tratamos os dados de login e os comunicados enviados por e-mail.', 6_000) || 'Recomendamos a leitura da nova versão para entender como tratamos os dados de login e os comunicados enviados por e-mail.';
|
|
422
|
+
const securityNote = normalizeText(payload?.securityNote || 'Se você tiver dúvidas sobre os novos termos, fale com nosso suporte oficial.', 220) || 'Se você tiver dúvidas sobre os novos termos, fale com nosso suporte oficial.';
|
|
423
|
+
|
|
424
|
+
const textLines = [`Olá, ${name}!`, '', heading, '', intro, '', body, '', `WhatsApp do bot: ${ctaUrl}`, `Novos Termos de Serviço: ${termsUrl}`, '', 'Mensagem automática do OmniZap.'];
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
subject,
|
|
428
|
+
text: textLines.join('\n'),
|
|
429
|
+
html: renderEmailLayout({
|
|
430
|
+
payload,
|
|
431
|
+
preheader: 'Atualizamos os Termos de Serviço do OmniZap.',
|
|
432
|
+
heading,
|
|
433
|
+
greeting: `Olá, ${name}!`,
|
|
434
|
+
intro,
|
|
435
|
+
body,
|
|
436
|
+
ctaLabel,
|
|
437
|
+
ctaUrl,
|
|
438
|
+
ctaHint: botPhonePn ? `WhatsApp do bot: ${botPhonePn}` : '',
|
|
439
|
+
secondaryCtaLabel: 'Ver novos Termos de Serviço',
|
|
440
|
+
secondaryCtaUrl: termsUrl,
|
|
441
|
+
securityNote,
|
|
442
|
+
footerMessage: 'Comunicado oficial sobre atualização de termos.',
|
|
443
|
+
}),
|
|
444
|
+
};
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const TEMPLATE_BUILDERS = {
|
|
448
|
+
standard: buildStandardTemplate,
|
|
449
|
+
default: buildStandardTemplate,
|
|
450
|
+
welcome: buildWelcomeTemplate,
|
|
451
|
+
magic_link: buildMagicLinkTemplate,
|
|
452
|
+
password_reset_code: buildPasswordResetCodeTemplate,
|
|
453
|
+
project_update: buildProjectUpdateTemplate,
|
|
454
|
+
terms_update: buildTermsUpdateTemplate,
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
export const renderEmailTemplate = (templateKey, payload = {}) => {
|
|
458
|
+
const normalizedTemplateKey = normalizeTemplateKey(templateKey);
|
|
459
|
+
if (!normalizedTemplateKey) return null;
|
|
460
|
+
const builder = TEMPLATE_BUILDERS[normalizedTemplateKey];
|
|
461
|
+
if (typeof builder !== 'function') return null;
|
|
462
|
+
|
|
463
|
+
const rendered = builder(payload || {});
|
|
464
|
+
if (!rendered) return null;
|
|
465
|
+
|
|
466
|
+
const subject = normalizeText(rendered.subject, 180);
|
|
467
|
+
const text = normalizeText(rendered.text, 120_000);
|
|
468
|
+
const html = normalizeText(rendered.html, 500_000);
|
|
469
|
+
|
|
470
|
+
if (!subject || (!text && !html)) return null;
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
subject,
|
|
474
|
+
text: text || null,
|
|
475
|
+
html: html || null,
|
|
476
|
+
template_key: normalizedTemplateKey,
|
|
477
|
+
};
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
export const listAvailableEmailTemplates = () => Object.keys(TEMPLATE_BUILDERS);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import nodemailer from 'nodemailer';
|
|
2
|
+
|
|
3
|
+
const parseEnvBool = (value, fallback) => {
|
|
4
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
5
|
+
const normalized = String(value).trim().toLowerCase();
|
|
6
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
|
|
7
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
|
|
8
|
+
return fallback;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const resolveSmtpConfig = () => {
|
|
12
|
+
const host = String(process.env.SMTP_HOST || process.env.EMAIL_HOST || process.env.MAIL_HOST || '').trim();
|
|
13
|
+
const port = Number(process.env.SMTP_PORT || process.env.EMAIL_PORT || process.env.MAIL_PORT || 465);
|
|
14
|
+
const user = String(process.env.SMTP_USER || process.env.EMAIL_USER || process.env.MAIL_USER || '').trim();
|
|
15
|
+
const pass = String(process.env.SMTP_PASS || process.env.EMAIL_PASS || process.env.MAIL_PASS || '').trim();
|
|
16
|
+
const from =
|
|
17
|
+
String(process.env.SMTP_FROM || process.env.EMAIL_FROM || process.env.MAIL_FROM || '')
|
|
18
|
+
.trim()
|
|
19
|
+
.slice(0, 255) || '';
|
|
20
|
+
const replyTo =
|
|
21
|
+
String(process.env.SMTP_REPLY_TO || process.env.EMAIL_REPLY_TO || process.env.MAIL_REPLY_TO || '')
|
|
22
|
+
.trim()
|
|
23
|
+
.slice(0, 255) || '';
|
|
24
|
+
const secure = parseEnvBool(process.env.SMTP_SECURE || process.env.EMAIL_SECURE || process.env.MAIL_SECURE, Number(port) === 465);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
host,
|
|
28
|
+
port: Number.isFinite(port) ? Math.max(1, Math.min(65535, Math.floor(port))) : 465,
|
|
29
|
+
secure,
|
|
30
|
+
user,
|
|
31
|
+
pass,
|
|
32
|
+
from: from || (user ? `OmniZap <${user}>` : ''),
|
|
33
|
+
replyTo: replyTo || null,
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const toConfigCacheKey = (config) => JSON.stringify(config);
|
|
38
|
+
|
|
39
|
+
let transportCacheKey = '';
|
|
40
|
+
let transporter = null;
|
|
41
|
+
|
|
42
|
+
const buildTransporter = () => {
|
|
43
|
+
const config = resolveSmtpConfig();
|
|
44
|
+
const cacheKey = toConfigCacheKey(config);
|
|
45
|
+
|
|
46
|
+
if (transporter && transportCacheKey === cacheKey) {
|
|
47
|
+
return { transporter, config };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
transportCacheKey = cacheKey;
|
|
51
|
+
transporter = nodemailer.createTransport({
|
|
52
|
+
host: config.host,
|
|
53
|
+
port: config.port,
|
|
54
|
+
secure: config.secure,
|
|
55
|
+
auth: {
|
|
56
|
+
user: config.user,
|
|
57
|
+
pass: config.pass,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return { transporter, config };
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const isEmailTransportConfigured = () => {
|
|
65
|
+
const config = resolveSmtpConfig();
|
|
66
|
+
return Boolean(config.host && config.port && config.user && config.pass && config.from);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const verifyEmailTransport = async () => {
|
|
70
|
+
if (!isEmailTransportConfigured()) {
|
|
71
|
+
const error = new Error('Transporte SMTP não configurado.');
|
|
72
|
+
error.code = 'EMAIL_TRANSPORT_NOT_CONFIGURED';
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const built = buildTransporter();
|
|
77
|
+
await built.transporter.verify();
|
|
78
|
+
return true;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const sendEmailMessage = async ({ to, subject, text = null, html = null, replyTo = null, cc = null, bcc = null, headers = null } = {}) => {
|
|
82
|
+
if (!isEmailTransportConfigured()) {
|
|
83
|
+
const error = new Error('Transporte SMTP não configurado.');
|
|
84
|
+
error.code = 'EMAIL_TRANSPORT_NOT_CONFIGURED';
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const normalizedTo =
|
|
89
|
+
String(to || '')
|
|
90
|
+
.trim()
|
|
91
|
+
.slice(0, 255) || '';
|
|
92
|
+
const normalizedSubject =
|
|
93
|
+
String(subject || '')
|
|
94
|
+
.trim()
|
|
95
|
+
.slice(0, 180) || '';
|
|
96
|
+
const normalizedText =
|
|
97
|
+
String(text || '')
|
|
98
|
+
.trim()
|
|
99
|
+
.slice(0, 120_000) || '';
|
|
100
|
+
const normalizedHtml =
|
|
101
|
+
String(html || '')
|
|
102
|
+
.trim()
|
|
103
|
+
.slice(0, 500_000) || '';
|
|
104
|
+
|
|
105
|
+
if (!normalizedTo || !normalizedTo.includes('@')) {
|
|
106
|
+
const error = new Error('Destinatário de e-mail inválido.');
|
|
107
|
+
error.code = 'EMAIL_INVALID_RECIPIENT';
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!normalizedSubject) {
|
|
112
|
+
const error = new Error('Assunto de e-mail inválido.');
|
|
113
|
+
error.code = 'EMAIL_INVALID_SUBJECT';
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!normalizedText && !normalizedHtml) {
|
|
118
|
+
const error = new Error('Corpo do e-mail vazio.');
|
|
119
|
+
error.code = 'EMAIL_EMPTY_BODY';
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const built = buildTransporter();
|
|
124
|
+
|
|
125
|
+
const info = await built.transporter.sendMail({
|
|
126
|
+
from: built.config.from,
|
|
127
|
+
to: normalizedTo,
|
|
128
|
+
subject: normalizedSubject,
|
|
129
|
+
text: normalizedText || undefined,
|
|
130
|
+
html: normalizedHtml || undefined,
|
|
131
|
+
replyTo: replyTo || built.config.replyTo || undefined,
|
|
132
|
+
cc: cc || undefined,
|
|
133
|
+
bcc: bcc || undefined,
|
|
134
|
+
headers: headers && typeof headers === 'object' ? headers : undefined,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
messageId: String(info?.messageId || '').trim() || null,
|
|
139
|
+
accepted: Array.isArray(info?.accepted) ? info.accepted : [],
|
|
140
|
+
rejected: Array.isArray(info?.rejected) ? info.rejected : [],
|
|
141
|
+
response: String(info?.response || '').trim() || null,
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const getEmailTransportMetadata = () => {
|
|
146
|
+
const config = resolveSmtpConfig();
|
|
147
|
+
return {
|
|
148
|
+
configured: isEmailTransportConfigured(),
|
|
149
|
+
host: config.host || null,
|
|
150
|
+
port: config.port,
|
|
151
|
+
secure: Boolean(config.secure),
|
|
152
|
+
from: config.from || null,
|
|
153
|
+
reply_to: config.replyTo || null,
|
|
154
|
+
user: config.user ? `${config.user.slice(0, 3)}***` : null,
|
|
155
|
+
};
|
|
156
|
+
};
|