@julioventura/opensquad 0.1.17
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/README.md +433 -0
- package/_opensquad/config/playwright.config.json +11 -0
- package/_opensquad/core/architect.agent.yaml +112 -0
- package/_opensquad/core/best-practices/_catalog.yaml +126 -0
- package/_opensquad/core/best-practices/blog-post.md +132 -0
- package/_opensquad/core/best-practices/blog-seo.md +127 -0
- package/_opensquad/core/best-practices/brand-resolution-checklist.md +172 -0
- package/_opensquad/core/best-practices/copywriting.md +441 -0
- package/_opensquad/core/best-practices/data-analysis.md +401 -0
- package/_opensquad/core/best-practices/email-newsletter.md +118 -0
- package/_opensquad/core/best-practices/email-sales.md +110 -0
- package/_opensquad/core/best-practices/image-design.md +348 -0
- package/_opensquad/core/best-practices/instagram-feed.md +235 -0
- package/_opensquad/core/best-practices/instagram-reels.md +112 -0
- package/_opensquad/core/best-practices/instagram-stories.md +107 -0
- package/_opensquad/core/best-practices/linkedin-article.md +116 -0
- package/_opensquad/core/best-practices/linkedin-post.md +121 -0
- package/_opensquad/core/best-practices/researching.md +349 -0
- package/_opensquad/core/best-practices/review.md +269 -0
- package/_opensquad/core/best-practices/run-recovery.md +61 -0
- package/_opensquad/core/best-practices/social-networks-publishing.md +327 -0
- package/_opensquad/core/best-practices/squad-creation-checklist.md +32 -0
- package/_opensquad/core/best-practices/strategist.md +344 -0
- package/_opensquad/core/best-practices/technical-writing.md +365 -0
- package/_opensquad/core/best-practices/twitter-post.md +105 -0
- package/_opensquad/core/best-practices/twitter-thread.md +122 -0
- package/_opensquad/core/best-practices/whatsapp-broadcast.md +107 -0
- package/_opensquad/core/best-practices/youtube-script.md +122 -0
- package/_opensquad/core/best-practices/youtube-shorts.md +112 -0
- package/_opensquad/core/defaults/youtube-video-assembly.json +84 -0
- package/_opensquad/core/prompts/build.prompt.md +613 -0
- package/_opensquad/core/prompts/design.prompt.md +606 -0
- package/_opensquad/core/prompts/discovery.prompt.md +377 -0
- package/_opensquad/core/prompts/sherlock-instagram.md +123 -0
- package/_opensquad/core/prompts/sherlock-linkedin.md +73 -0
- package/_opensquad/core/prompts/sherlock-shared.md +684 -0
- package/_opensquad/core/prompts/sherlock-twitter.md +78 -0
- package/_opensquad/core/prompts/sherlock-youtube.md +85 -0
- package/_opensquad/core/runner.pipeline.md +743 -0
- package/_opensquad/core/skills.engine.md +384 -0
- package/bin/opensquad.js +108 -0
- package/dashboard/index.html +15 -0
- package/dashboard/package-lock.json +1964 -0
- package/dashboard/package.json +28 -0
- package/dashboard/public/assets/avatars/Female1_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Female1_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Female1_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female1_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female2_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Female2_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Female2_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female2_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female3_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female3_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female3_wave.png +0 -0
- package/dashboard/public/assets/avatars/Female4_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female4_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female4_wave.png +0 -0
- package/dashboard/public/assets/avatars/Female5_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female5_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female5_wave.png +0 -0
- package/dashboard/public/assets/avatars/Female6_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female6_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female6_wave.png +0 -0
- package/dashboard/public/assets/avatars/Male1_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Male1_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Male1_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male1_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male2_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Male2_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Male2_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male2_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male3_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male3_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male3_wave.png +0 -0
- package/dashboard/public/assets/avatars/Male4_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male4_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male4_wave.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_down.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_down_coding-1.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_down_coding.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_up.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_down.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_down_coding-1.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_down_coding.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_up.png +0 -0
- package/dashboard/public/assets/furniture/armchair_tan.png +0 -0
- package/dashboard/public/assets/furniture/armchair_tan_down.png +0 -0
- package/dashboard/public/assets/furniture/backpack_blue.png +0 -0
- package/dashboard/public/assets/furniture/backpack_red.png +0 -0
- package/dashboard/public/assets/furniture/blinds.png +0 -0
- package/dashboard/public/assets/furniture/blinds_large_closed_white.png +0 -0
- package/dashboard/public/assets/furniture/bookshelf.png +0 -0
- package/dashboard/public/assets/furniture/bookshelf_purple_tall.png +0 -0
- package/dashboard/public/assets/furniture/bulletin_board.png +0 -0
- package/dashboard/public/assets/furniture/clock.png +0 -0
- package/dashboard/public/assets/furniture/coffee_mug.png +0 -0
- package/dashboard/public/assets/furniture/coffee_mug_blue.png +0 -0
- package/dashboard/public/assets/furniture/coffee_table.png +0 -0
- package/dashboard/public/assets/furniture/coffeepot_right.png +0 -0
- package/dashboard/public/assets/furniture/coffeetable_black_horizontal.png +0 -0
- package/dashboard/public/assets/furniture/couch.png +0 -0
- package/dashboard/public/assets/furniture/couch_tan_down.png +0 -0
- package/dashboard/public/assets/furniture/cushion_blue.png +0 -0
- package/dashboard/public/assets/furniture/cushion_tan.png +0 -0
- package/dashboard/public/assets/furniture/desk_wood.png +0 -0
- package/dashboard/public/assets/furniture/fancy_rug.png +0 -0
- package/dashboard/public/assets/furniture/fancy_rug_wide.png +0 -0
- package/dashboard/public/assets/furniture/flowers1.png +0 -0
- package/dashboard/public/assets/furniture/flowers2.png +0 -0
- package/dashboard/public/assets/furniture/lamp_tan.png +0 -0
- package/dashboard/public/assets/furniture/lantern.png +0 -0
- package/dashboard/public/assets/furniture/monstera.png +0 -0
- package/dashboard/public/assets/furniture/monstera_small.png +0 -0
- package/dashboard/public/assets/furniture/picture_frame.png +0 -0
- package/dashboard/public/assets/furniture/plant1.png +0 -0
- package/dashboard/public/assets/furniture/plant2.png +0 -0
- package/dashboard/public/assets/furniture/plant3.png +0 -0
- package/dashboard/public/assets/furniture/plant_poof.png +0 -0
- package/dashboard/public/assets/furniture/plant_spindly.png +0 -0
- package/dashboard/public/assets/furniture/poster_blue.png +0 -0
- package/dashboard/public/assets/furniture/rug.png +0 -0
- package/dashboard/public/assets/furniture/succulent_blue.png +0 -0
- package/dashboard/public/assets/furniture/succulent_green.png +0 -0
- package/dashboard/public/assets/furniture/treasurechest_closed_gold.png +0 -0
- package/dashboard/public/assets/furniture/water_cooler_better.png +0 -0
- package/dashboard/public/assets/furniture/whiteboard.png +0 -0
- package/dashboard/public/assets/furniture/whiteboard_stand_graph.png +0 -0
- package/dashboard/public/assets/furniture/window_blinds_open.png +0 -0
- package/dashboard/src/App.tsx +46 -0
- package/dashboard/src/components/RunDashboardButton.tsx +92 -0
- package/dashboard/src/components/SquadCard.tsx +49 -0
- package/dashboard/src/components/SquadSelector.tsx +67 -0
- package/dashboard/src/components/StatusBadge.tsx +32 -0
- package/dashboard/src/components/StatusBar.tsx +116 -0
- package/dashboard/src/hooks/useSquadSocket.ts +135 -0
- package/dashboard/src/lib/formatTime.ts +16 -0
- package/dashboard/src/lib/normalizeState.ts +25 -0
- package/dashboard/src/main.tsx +10 -0
- package/dashboard/src/office/AgentSprite.ts +241 -0
- package/dashboard/src/office/OfficeScene.ts +153 -0
- package/dashboard/src/office/PhaserGame.tsx +80 -0
- package/dashboard/src/office/RoomBuilder.ts +190 -0
- package/dashboard/src/office/assetKeys.ts +150 -0
- package/dashboard/src/office/palette.ts +32 -0
- package/dashboard/src/plugin/squadWatcher.ts +397 -0
- package/dashboard/src/store/useSquadStore.ts +56 -0
- package/dashboard/src/styles/globals.css +36 -0
- package/dashboard/src/types/state.ts +63 -0
- package/dashboard/src/vite-env.d.ts +1 -0
- package/dashboard/tsconfig.json +24 -0
- package/dashboard/vite.config.ts +13 -0
- package/package.json +59 -0
- package/public/sfx/slide-transition-sfx.mp3 +0 -0
- package/skills/README.md +84 -0
- package/skills/apify/SKILL.md +55 -0
- package/skills/blotato/SKILL.md +63 -0
- package/skills/canva/SKILL.md +60 -0
- package/skills/higgsfield/SKILL.md +147 -0
- package/skills/image-ai-generator/SKILL.md +124 -0
- package/skills/image-ai-generator/scripts/generate.py +175 -0
- package/skills/image-creator/SKILL.md +166 -0
- package/skills/image-creator/editorial-slide-template.js +645 -0
- package/skills/image-fetcher/SKILL.md +91 -0
- package/skills/imgbb-uploader/SKILL.md +73 -0
- package/skills/imgbb-uploader/scripts/upload.js +125 -0
- package/skills/instagram-publisher/README.md +36 -0
- package/skills/instagram-publisher/SKILL.md +231 -0
- package/skills/instagram-publisher/scripts/publish-playwright.js +418 -0
- package/skills/instagram-publisher/scripts/publish.js +521 -0
- package/skills/opensquad-agent-creator/SKILL.md +192 -0
- package/skills/opensquad-skill-creator/SKILL.md +420 -0
- package/skills/opensquad-skill-creator/agents/analyzer.md +274 -0
- package/skills/opensquad-skill-creator/agents/comparator.md +202 -0
- package/skills/opensquad-skill-creator/agents/grader.md +223 -0
- package/skills/opensquad-skill-creator/assets/eval_review.html +146 -0
- package/skills/opensquad-skill-creator/eval-viewer/generate_review.py +471 -0
- package/skills/opensquad-skill-creator/eval-viewer/viewer.html +1325 -0
- package/skills/opensquad-skill-creator/references/schemas.md +430 -0
- package/skills/opensquad-skill-creator/references/skill-format.md +235 -0
- package/skills/opensquad-skill-creator/scripts/__init__.py +0 -0
- package/skills/opensquad-skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/skills/opensquad-skill-creator/scripts/quick_validate.py +103 -0
- package/skills/opensquad-skill-creator/scripts/run_eval.py +310 -0
- package/skills/opensquad-skill-creator/scripts/utils.py +47 -0
- package/skills/pdf-extractor/SKILL.md +57 -0
- package/skills/pdf-extractor/scripts/extract.py +82 -0
- package/skills/resend/SKILL.md +80 -0
- package/skills/run-dashboard/README.md +93 -0
- package/skills/run-dashboard/SKILL.md +173 -0
- package/skills/run-dashboard/scripts/finalize-state.js +273 -0
- package/skills/run-dashboard/scripts/generate.js +1296 -0
- package/skills/run-dashboard/scripts/serve.js +135 -0
- package/skills/run-dashboard/templates/run-dashboard-simple.template.html +191 -0
- package/skills/run-dashboard/templates/run-dashboard.template.html +1164 -0
- package/skills/smtp-sender/SKILL.md +88 -0
- package/skills/smtp-sender/scripts/send.js +478 -0
- package/skills/template-designer/SKILL.md +201 -0
- package/skills/template-designer/base-templates/model-a.html +27 -0
- package/skills/template-designer/base-templates/model-b.html +31 -0
- package/skills/template-designer/base-templates/model-c.html +42 -0
- package/skills/youtube-publisher/SKILL.md +232 -0
- package/skills/youtube-publisher/scripts/publish.js +2078 -0
- package/src/agents-cli.js +158 -0
- package/src/agents.js +134 -0
- package/src/i18n.js +48 -0
- package/src/init.js +442 -0
- package/src/locales/en.json +79 -0
- package/src/locales/es.json +78 -0
- package/src/locales/pt-BR.json +78 -0
- package/src/logger.js +38 -0
- package/src/prompt.js +46 -0
- package/src/readme/README.md +146 -0
- package/src/runs.js +318 -0
- package/src/skills-cli.js +157 -0
- package/src/skills.js +146 -0
- package/src/supabase-cli.js +584 -0
- package/src/update.js +169 -0
- package/templates/_opensquad/.opensquad-version +1 -0
- package/templates/_opensquad/_investigations/.gitkeep +0 -0
- package/templates/ide-templates/antigravity/.agent/rules/opensquad.md +68 -0
- package/templates/ide-templates/antigravity/.agent/workflows/opensquad.md +102 -0
- package/templates/ide-templates/claude-code/.claude/skills/opensquad/SKILL.md +182 -0
- package/templates/ide-templates/claude-code/.mcp.json +8 -0
- package/templates/ide-templates/claude-code/CLAUDE.md +57 -0
- package/templates/ide-templates/codex/.agents/skills/opensquad/SKILL.md +6 -0
- package/templates/ide-templates/codex/AGENTS.md +120 -0
- package/templates/ide-templates/cursor/.cursor/commands/opensquad.md +9 -0
- package/templates/ide-templates/cursor/.cursor/mcp.json +8 -0
- package/templates/ide-templates/cursor/.cursor/rules/opensquad.mdc +62 -0
- package/templates/ide-templates/cursor/.cursorignore +3 -0
- package/templates/ide-templates/gemini-cli/.gemini/settings.json +8 -0
- package/templates/ide-templates/gemini-cli/.gemini/skills/opensquad/SKILL.md +186 -0
- package/templates/ide-templates/gemini-cli/GEMINI.md +57 -0
- package/templates/ide-templates/opencode/.opencode/commands/opensquad.md +9 -0
- package/templates/ide-templates/opencode/AGENTS.md +120 -0
- package/templates/ide-templates/qwen-code/.qwen/settings.json +8 -0
- package/templates/ide-templates/qwen-code/.qwen/skills/opensquad/SKILL.md +182 -0
- package/templates/ide-templates/qwen-code/QWEN.md +57 -0
- package/templates/ide-templates/trae/.trae/mcp.json +8 -0
- package/templates/ide-templates/trae/.trae/rules/opensquad.md +64 -0
- package/templates/ide-templates/vscode-copilot/.github/copilot-instructions.md +59 -0
- package/templates/ide-templates/vscode-copilot/.github/prompts/opensquad.prompt.md +209 -0
- package/templates/ide-templates/vscode-copilot/.vscode/mcp.json +8 -0
- package/templates/ide-templates/vscode-copilot/.vscode/settings.json +3 -0
- package/templates/package.json +8 -0
- package/templates/squads/.gitkeep +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: smtp-sender
|
|
3
|
+
description: >
|
|
4
|
+
Send HTML plus plain-text emails through a native SMTP connection using local .env credentials.
|
|
5
|
+
Useful when a squad needs direct SMTP delivery without relying on an external email API.
|
|
6
|
+
description_pt-BR: >
|
|
7
|
+
Envia emails HTML e texto puro por conexao SMTP nativa usando credenciais locais do .env.
|
|
8
|
+
Util quando um squad precisa de entrega SMTP direta sem depender de uma API externa.
|
|
9
|
+
description_es: >
|
|
10
|
+
Envia correos HTML y texto plano mediante una conexion SMTP nativa usando credenciales locales del .env.
|
|
11
|
+
Util cuando un squad necesita entrega SMTP directa sin depender de una API externa.
|
|
12
|
+
type: script
|
|
13
|
+
version: "1.0.0"
|
|
14
|
+
script:
|
|
15
|
+
path: scripts/send.js
|
|
16
|
+
runtime: node
|
|
17
|
+
invoke: "node --env-file=.env {skill_path}/scripts/send.js --html \"{html}\" --text \"{text}\" --newsletter-preview \"{newsletter_preview}\" --preview \"{send_preview}\" --output \"{output}\" --smtp-host-env \"{smtp_host_env}\" --smtp-port-env \"{smtp_port_env}\" --smtp-user-env \"{smtp_user_env}\" --smtp-pass-env \"{smtp_pass_env}\""
|
|
18
|
+
env:
|
|
19
|
+
- SMTP_HOST
|
|
20
|
+
- SMTP_PORT
|
|
21
|
+
- SMTP_USER
|
|
22
|
+
- SMTP_PASS
|
|
23
|
+
categories: [email, smtp, automation]
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# SMTP Sender
|
|
27
|
+
|
|
28
|
+
## When to use
|
|
29
|
+
|
|
30
|
+
Use this skill when a squad already has the final `newsletter.html` and `newsletter.txt` files and needs to deliver them through a direct SMTP server such as a brand-owned mailbox.
|
|
31
|
+
|
|
32
|
+
## Workflow
|
|
33
|
+
|
|
34
|
+
1. Confirm the final send metadata in `send-preview.md` and `newsletter-preview.md`.
|
|
35
|
+
2. Run the sender script with the HTML file, text file, preview files, and output path.
|
|
36
|
+
3. Pass the brand-specific SMTP env keys explicitly when the workspace serves multiple brands, or persist them in `send-preview.md` so the sender can resolve them automatically.
|
|
37
|
+
4. Read `email-send-result.md` after the command finishes and confirm status, message ID, sender, and audience.
|
|
38
|
+
|
|
39
|
+
## Command
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
node --env-file=.env {skill_path}/scripts/send.js \
|
|
43
|
+
--html "squads/jornal-matutino/output/2026-05-18-201500/newsletter.html" \
|
|
44
|
+
--text "squads/jornal-matutino/output/2026-05-18-201500/newsletter.txt" \
|
|
45
|
+
--newsletter-preview "squads/jornal-matutino/output/2026-05-18-201500/newsletter-preview.md" \
|
|
46
|
+
--preview "squads/jornal-matutino/output/2026-05-18-201500/send-preview.md" \
|
|
47
|
+
--output "squads/jornal-matutino/output/2026-05-18-201500/email-send-result.md"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
When `send-preview.md` does not include the SMTP env key mapping, append the brand-specific overrides explicitly:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
--smtp-host-env "BRAND_SMTP_HOST" \
|
|
54
|
+
--smtp-port-env "BRAND_SMTP_PORT" \
|
|
55
|
+
--smtp-user-env "BRAND_SMTP_USER" \
|
|
56
|
+
--smtp-pass-env "BRAND_SMTP_PASS"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Output
|
|
60
|
+
|
|
61
|
+
The script writes `email-send-result.md` in this format:
|
|
62
|
+
|
|
63
|
+
```markdown
|
|
64
|
+
# Email Send Result
|
|
65
|
+
|
|
66
|
+
**Status:** sent | failed
|
|
67
|
+
**Mode:** test | live
|
|
68
|
+
**Provider:** SMTP
|
|
69
|
+
**Message ID:** ...
|
|
70
|
+
**From:** ...
|
|
71
|
+
**To/Audience:** ...
|
|
72
|
+
**Scheduled at:** ...
|
|
73
|
+
**Sent at:** ...
|
|
74
|
+
**Notes:** ...
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Constraints
|
|
78
|
+
|
|
79
|
+
- Current native flow supports immediate `test` and `live` sends.
|
|
80
|
+
- `scheduled` mode is not implemented in the local SMTP sender.
|
|
81
|
+
- The sender always opens a secure `SSL/TLS` SMTP connection.
|
|
82
|
+
- If `send-preview.md` contains `SMTP Host Env`, `SMTP Port Env`, `SMTP User Env`, and `SMTP Pass Env`, those mappings are used automatically.
|
|
83
|
+
|
|
84
|
+
## Available operations
|
|
85
|
+
|
|
86
|
+
- **Send multipart email** -- Deliver HTML plus plain-text content over SMTP
|
|
87
|
+
- **Brand env mapping** -- Use explicit env key names for multi-brand workspaces
|
|
88
|
+
- **Delivery logging** -- Persist the provider result to `email-send-result.md`
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import tls from 'node:tls';
|
|
6
|
+
import { dirname, resolve } from 'node:path';
|
|
7
|
+
import process from 'node:process';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_ENV_KEYS = {
|
|
11
|
+
smtpHost: 'SMTP_HOST',
|
|
12
|
+
smtpPort: 'SMTP_PORT',
|
|
13
|
+
smtpUser: 'SMTP_USER',
|
|
14
|
+
smtpPass: 'SMTP_PASS',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function stripWrapping(value) {
|
|
18
|
+
return value
|
|
19
|
+
.replace(/^[<'"`]+/, '')
|
|
20
|
+
.replace(/[>'"`]+$/, '')
|
|
21
|
+
.trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeLineEndings(value) {
|
|
25
|
+
return value.replace(/\r?\n/g, '\r\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseArgs(argv) {
|
|
29
|
+
const args = {
|
|
30
|
+
htmlPath: '',
|
|
31
|
+
textPath: '',
|
|
32
|
+
newsletterPreviewPath: '',
|
|
33
|
+
previewPath: '',
|
|
34
|
+
outputPath: '',
|
|
35
|
+
subject: '',
|
|
36
|
+
from: '',
|
|
37
|
+
to: '',
|
|
38
|
+
replyTo: '',
|
|
39
|
+
mode: '',
|
|
40
|
+
scheduledAt: '',
|
|
41
|
+
smtpHostEnv: null,
|
|
42
|
+
smtpPortEnv: null,
|
|
43
|
+
smtpUserEnv: null,
|
|
44
|
+
smtpPassEnv: null,
|
|
45
|
+
dryRun: false,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
for (let index = 2; index < argv.length; index++) {
|
|
49
|
+
const current = argv[index];
|
|
50
|
+
|
|
51
|
+
if (current === '--html' && index + 1 < argv.length) {
|
|
52
|
+
args.htmlPath = argv[++index];
|
|
53
|
+
} else if (current === '--text' && index + 1 < argv.length) {
|
|
54
|
+
args.textPath = argv[++index];
|
|
55
|
+
} else if (current === '--newsletter-preview' && index + 1 < argv.length) {
|
|
56
|
+
args.newsletterPreviewPath = argv[++index];
|
|
57
|
+
} else if (current === '--preview' && index + 1 < argv.length) {
|
|
58
|
+
args.previewPath = argv[++index];
|
|
59
|
+
} else if (current === '--output' && index + 1 < argv.length) {
|
|
60
|
+
args.outputPath = argv[++index];
|
|
61
|
+
} else if (current === '--subject' && index + 1 < argv.length) {
|
|
62
|
+
args.subject = argv[++index];
|
|
63
|
+
} else if (current === '--from' && index + 1 < argv.length) {
|
|
64
|
+
args.from = argv[++index];
|
|
65
|
+
} else if (current === '--to' && index + 1 < argv.length) {
|
|
66
|
+
args.to = argv[++index];
|
|
67
|
+
} else if (current === '--reply-to' && index + 1 < argv.length) {
|
|
68
|
+
args.replyTo = argv[++index];
|
|
69
|
+
} else if (current === '--mode' && index + 1 < argv.length) {
|
|
70
|
+
args.mode = argv[++index];
|
|
71
|
+
} else if (current === '--scheduled-at' && index + 1 < argv.length) {
|
|
72
|
+
args.scheduledAt = argv[++index];
|
|
73
|
+
} else if (current === '--smtp-host-env' && index + 1 < argv.length) {
|
|
74
|
+
args.smtpHostEnv = argv[++index];
|
|
75
|
+
} else if (current === '--smtp-port-env' && index + 1 < argv.length) {
|
|
76
|
+
args.smtpPortEnv = argv[++index];
|
|
77
|
+
} else if (current === '--smtp-user-env' && index + 1 < argv.length) {
|
|
78
|
+
args.smtpUserEnv = argv[++index];
|
|
79
|
+
} else if (current === '--smtp-pass-env' && index + 1 < argv.length) {
|
|
80
|
+
args.smtpPassEnv = argv[++index];
|
|
81
|
+
} else if (current === '--dry-run') {
|
|
82
|
+
args.dryRun = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return args;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function loadEnvVars(targetDir = process.cwd()) {
|
|
90
|
+
const envVars = { ...process.env };
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const raw = await readFile(resolve(targetDir, '.env'), 'utf-8');
|
|
94
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
95
|
+
const trimmed = line.trim();
|
|
96
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
97
|
+
const separatorIndex = trimmed.indexOf('=');
|
|
98
|
+
if (separatorIndex === -1) continue;
|
|
99
|
+
const key = trimmed.slice(0, separatorIndex).trim();
|
|
100
|
+
const value = trimmed.slice(separatorIndex + 1).trim().replace(/^['"]|['"]$/g, '');
|
|
101
|
+
if (key) {
|
|
102
|
+
envVars[key] = value;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Optional .env
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return envVars;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function resolveConfiguredEnv(env, options = {}) {
|
|
113
|
+
const smtpHostKey = options.smtpHostEnv || DEFAULT_ENV_KEYS.smtpHost;
|
|
114
|
+
const smtpPortKey = options.smtpPortEnv || DEFAULT_ENV_KEYS.smtpPort;
|
|
115
|
+
const smtpUserKey = options.smtpUserEnv || DEFAULT_ENV_KEYS.smtpUser;
|
|
116
|
+
const smtpPassKey = options.smtpPassEnv || DEFAULT_ENV_KEYS.smtpPass;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
smtpHostKey,
|
|
120
|
+
smtpPortKey,
|
|
121
|
+
smtpUserKey,
|
|
122
|
+
smtpPassKey,
|
|
123
|
+
smtpHost: env[smtpHostKey],
|
|
124
|
+
smtpPort: env[smtpPortKey],
|
|
125
|
+
smtpUser: env[smtpUserKey],
|
|
126
|
+
smtpPass: env[smtpPassKey],
|
|
127
|
+
secure: true,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function parseMarkdownFields(content) {
|
|
132
|
+
const result = {};
|
|
133
|
+
const matches = content.matchAll(/^\*\*(.+?):\*\*\s*(.+)$/gm);
|
|
134
|
+
for (const match of matches) {
|
|
135
|
+
result[match[1].trim()] = stripWrapping(match[2].trim());
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function resolveSmtpEnvOptions(args, previewFields = {}) {
|
|
141
|
+
return {
|
|
142
|
+
smtpHostEnv: args.smtpHostEnv || previewFields['SMTP Host Env'] || null,
|
|
143
|
+
smtpPortEnv: args.smtpPortEnv || previewFields['SMTP Port Env'] || null,
|
|
144
|
+
smtpUserEnv: args.smtpUserEnv || previewFields['SMTP User Env'] || null,
|
|
145
|
+
smtpPassEnv: args.smtpPassEnv || previewFields['SMTP Pass Env'] || null,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function parseRecipients(value) {
|
|
150
|
+
return value
|
|
151
|
+
.split(/[;,\n]/)
|
|
152
|
+
.map((entry) => stripWrapping(entry.trim()))
|
|
153
|
+
.filter(Boolean);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function extractEmailAddress(value) {
|
|
157
|
+
const match = value.match(/<([^>]+)>/);
|
|
158
|
+
return stripWrapping(match ? match[1] : value);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function formatMailboxAddress(name, email) {
|
|
162
|
+
if (!name) return email;
|
|
163
|
+
return `${encodeHeader(name)} <${email}>`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function encodeHeader(value) {
|
|
167
|
+
if (/^[\x20-\x7E]*$/.test(value)) return value;
|
|
168
|
+
return `=?UTF-8?B?${Buffer.from(value, 'utf8').toString('base64')}?=`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function encodeBodyPart(value) {
|
|
172
|
+
return Buffer.from(normalizeLineEndings(value), 'utf8').toString('base64').replace(/.{1,76}/g, '$&\r\n').trimEnd();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function buildMessageHeaderId(from) {
|
|
176
|
+
const domain = (from.split('@')[1] || 'opensquad.local').replace(/[^a-z0-9.-]/gi, '') || 'opensquad.local';
|
|
177
|
+
return `<${Date.now()}.${Math.random().toString(16).slice(2)}@${domain}>`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function formatUnknownError(error) {
|
|
181
|
+
if (error instanceof Error && error.message) return error.message;
|
|
182
|
+
if (typeof error === 'string' && error.trim()) return error;
|
|
183
|
+
if (error && typeof error === 'object') {
|
|
184
|
+
const details = [error.code, error.reason, error.name, error.command].filter(Boolean).join(' | ');
|
|
185
|
+
if (details) return details;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const json = JSON.stringify(error);
|
|
189
|
+
if (json && json !== '{}') return json;
|
|
190
|
+
} catch {
|
|
191
|
+
// Fall through
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return 'Unknown SMTP sender failure';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function escapeDataBody(value) {
|
|
199
|
+
return normalizeLineEndings(value).replace(/^\./gm, '..');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function buildMimeMessage({ from, to, replyTo, subject, html, text }) {
|
|
203
|
+
const fromEmail = extractEmailAddress(from);
|
|
204
|
+
const boundary = `opensquad_${Date.now().toString(16)}_${Math.random().toString(16).slice(2)}`;
|
|
205
|
+
const messageHeaderId = buildMessageHeaderId(fromEmail);
|
|
206
|
+
const headers = [
|
|
207
|
+
`From: ${from}`,
|
|
208
|
+
`To: ${to.join(', ')}`,
|
|
209
|
+
replyTo ? `Reply-To: ${replyTo}` : null,
|
|
210
|
+
`Subject: ${encodeHeader(subject)}`,
|
|
211
|
+
`Message-ID: ${messageHeaderId}`,
|
|
212
|
+
`Date: ${new Date().toUTCString()}`,
|
|
213
|
+
'X-Mailer: Opensquad SMTP Sender',
|
|
214
|
+
'MIME-Version: 1.0',
|
|
215
|
+
`Content-Type: multipart/alternative; boundary="${boundary}"`,
|
|
216
|
+
'',
|
|
217
|
+
`--${boundary}`,
|
|
218
|
+
'Content-Type: text/plain; charset="UTF-8"',
|
|
219
|
+
'Content-Transfer-Encoding: base64',
|
|
220
|
+
'',
|
|
221
|
+
escapeDataBody(encodeBodyPart(text)),
|
|
222
|
+
`--${boundary}`,
|
|
223
|
+
'Content-Type: text/html; charset="UTF-8"',
|
|
224
|
+
'Content-Transfer-Encoding: base64',
|
|
225
|
+
'',
|
|
226
|
+
escapeDataBody(encodeBodyPart(html)),
|
|
227
|
+
`--${boundary}--`,
|
|
228
|
+
'',
|
|
229
|
+
].filter((entry) => entry !== null);
|
|
230
|
+
|
|
231
|
+
return headers.join('\r\n');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function createResponseReader(socket) {
|
|
235
|
+
let buffer = '';
|
|
236
|
+
let currentLines = [];
|
|
237
|
+
const queue = [];
|
|
238
|
+
|
|
239
|
+
function flush(response) {
|
|
240
|
+
const next = queue.shift();
|
|
241
|
+
if (next) {
|
|
242
|
+
clearTimeout(next.timeoutId);
|
|
243
|
+
next.resolve(response);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
socket.on('data', (chunk) => {
|
|
248
|
+
buffer += chunk.toString('utf8');
|
|
249
|
+
|
|
250
|
+
while (buffer.includes('\n')) {
|
|
251
|
+
const newlineIndex = buffer.indexOf('\n');
|
|
252
|
+
const line = buffer.slice(0, newlineIndex).replace(/\r$/, '');
|
|
253
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
254
|
+
if (!line) continue;
|
|
255
|
+
|
|
256
|
+
currentLines.push(line);
|
|
257
|
+
const match = line.match(/^(\d{3})([ -])(.*)$/);
|
|
258
|
+
if (!match) continue;
|
|
259
|
+
if (match[2] === ' ') {
|
|
260
|
+
const code = Number(match[1]);
|
|
261
|
+
const lines = currentLines;
|
|
262
|
+
currentLines = [];
|
|
263
|
+
flush({ code, lines, message: lines.join('\n') });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
function waitForResponse(timeoutMs = 15000) {
|
|
269
|
+
return new Promise((resolve, reject) => {
|
|
270
|
+
const timeoutId = setTimeout(() => {
|
|
271
|
+
const pending = queue.findIndex((entry) => entry.timeoutId === timeoutId);
|
|
272
|
+
if (pending !== -1) queue.splice(pending, 1);
|
|
273
|
+
reject(new Error(`SMTP response timeout after ${timeoutMs}ms`));
|
|
274
|
+
}, timeoutMs);
|
|
275
|
+
|
|
276
|
+
queue.push({ resolve, reject, timeoutId });
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { waitForResponse };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function connectSmtp({ host, port, secure }) {
|
|
284
|
+
return new Promise((resolve, reject) => {
|
|
285
|
+
const options = { host, port, servername: host, family: 4 };
|
|
286
|
+
const socket = secure
|
|
287
|
+
? tls.connect(options, () => resolve(socket))
|
|
288
|
+
: net.createConnection({ host, port }, () => resolve(socket));
|
|
289
|
+
|
|
290
|
+
socket.once('error', reject);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function sendCommand(socket, reader, command, expectedCodes) {
|
|
295
|
+
if (command != null) {
|
|
296
|
+
socket.write(`${command}\r\n`);
|
|
297
|
+
}
|
|
298
|
+
const response = await reader.waitForResponse();
|
|
299
|
+
if (expectedCodes && !expectedCodes.includes(response.code)) {
|
|
300
|
+
throw new Error(`SMTP command failed for '${command ?? '[initial]'}': ${response.message}`);
|
|
301
|
+
}
|
|
302
|
+
return response;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function extractProviderMessageId(response) {
|
|
306
|
+
const match = response.message.match(/\bid=([^\s]+)/i);
|
|
307
|
+
return match ? match[1] : 'not-returned';
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export async function sendViaSmtp({ host, port, secure, username, password, from, to, message }) {
|
|
311
|
+
const socket = await connectSmtp({ host, port, secure });
|
|
312
|
+
const reader = createResponseReader(socket);
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
await sendCommand(socket, reader, null, [220]);
|
|
316
|
+
await sendCommand(socket, reader, 'EHLO opensquad.local', [250]);
|
|
317
|
+
await sendCommand(socket, reader, 'AUTH LOGIN', [334]);
|
|
318
|
+
await sendCommand(socket, reader, Buffer.from(username, 'utf8').toString('base64'), [334]);
|
|
319
|
+
await sendCommand(socket, reader, Buffer.from(password, 'utf8').toString('base64'), [235]);
|
|
320
|
+
await sendCommand(socket, reader, `MAIL FROM:<${from}>`, [250]);
|
|
321
|
+
for (const recipient of to) {
|
|
322
|
+
await sendCommand(socket, reader, `RCPT TO:<${recipient}>`, [250, 251]);
|
|
323
|
+
}
|
|
324
|
+
await sendCommand(socket, reader, 'DATA', [354]);
|
|
325
|
+
socket.write(`${message}\r\n.\r\n`);
|
|
326
|
+
const deliveryResponse = await reader.waitForResponse();
|
|
327
|
+
if (deliveryResponse.code !== 250) {
|
|
328
|
+
throw new Error(`SMTP delivery failed: ${deliveryResponse.message}`);
|
|
329
|
+
}
|
|
330
|
+
await sendCommand(socket, reader, 'QUIT', [221]);
|
|
331
|
+
socket.end();
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
messageId: extractProviderMessageId(deliveryResponse),
|
|
335
|
+
response: deliveryResponse.message,
|
|
336
|
+
};
|
|
337
|
+
} catch (error) {
|
|
338
|
+
socket.destroy();
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function buildEmailSendResult({ status, mode, messageId, from, to, scheduledAt, sentAt, notes }) {
|
|
344
|
+
return [
|
|
345
|
+
'# Email Send Result',
|
|
346
|
+
'',
|
|
347
|
+
`**Status:** ${status}`,
|
|
348
|
+
`**Mode:** ${mode}`,
|
|
349
|
+
'**Provider:** SMTP',
|
|
350
|
+
`**Message ID:** ${messageId}`,
|
|
351
|
+
`**From:** ${from}`,
|
|
352
|
+
`**To/Audience:** ${to.join(', ')}`,
|
|
353
|
+
`**Scheduled at:** ${scheduledAt || 'not scheduled'}`,
|
|
354
|
+
`**Sent at:** ${sentAt}`,
|
|
355
|
+
`**Notes:** ${notes}`,
|
|
356
|
+
'',
|
|
357
|
+
].join('\n');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function writeResult(outputPath, content) {
|
|
361
|
+
const absoluteOutputPath = resolve(outputPath);
|
|
362
|
+
await mkdir(dirname(absoluteOutputPath), { recursive: true });
|
|
363
|
+
await writeFile(absoluteOutputPath, content, 'utf8');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function main() {
|
|
367
|
+
const args = parseArgs(process.argv);
|
|
368
|
+
if (!args.htmlPath || !args.textPath || !args.newsletterPreviewPath || !args.previewPath || !args.outputPath) {
|
|
369
|
+
throw new Error('Missing required arguments. Use --html, --text, --newsletter-preview, --preview, and --output.');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const [html, text, newsletterPreview, sendPreview] = await Promise.all([
|
|
373
|
+
readFile(resolve(args.htmlPath), 'utf8'),
|
|
374
|
+
readFile(resolve(args.textPath), 'utf8'),
|
|
375
|
+
readFile(resolve(args.newsletterPreviewPath), 'utf8'),
|
|
376
|
+
readFile(resolve(args.previewPath), 'utf8'),
|
|
377
|
+
]);
|
|
378
|
+
|
|
379
|
+
const newsletterFields = parseMarkdownFields(newsletterPreview);
|
|
380
|
+
const previewFields = parseMarkdownFields(sendPreview);
|
|
381
|
+
const env = await loadEnvVars();
|
|
382
|
+
const smtpEnvOptions = resolveSmtpEnvOptions(args, previewFields);
|
|
383
|
+
const configuredEnv = resolveConfiguredEnv(env, smtpEnvOptions);
|
|
384
|
+
|
|
385
|
+
const subject = args.subject || newsletterFields.Subject;
|
|
386
|
+
const senderName = stripWrapping(newsletterFields.Brand || previewFields['Sender Name'] || '');
|
|
387
|
+
const fromEmail = extractEmailAddress(stripWrapping(args.from || previewFields.From || configuredEnv.smtpUser || ''));
|
|
388
|
+
const from = formatMailboxAddress(senderName, fromEmail);
|
|
389
|
+
const recipients = parseRecipients(args.to || previewFields.To || '');
|
|
390
|
+
const replyTo = stripWrapping(args.replyTo || '');
|
|
391
|
+
const mode = stripWrapping(args.mode || previewFields.Mode || 'test').toLowerCase();
|
|
392
|
+
const scheduledAt = stripWrapping(args.scheduledAt || previewFields['Scheduled at'] || 'not scheduled');
|
|
393
|
+
const secure = configuredEnv.secure;
|
|
394
|
+
|
|
395
|
+
if (!subject) throw new Error('Missing subject. Provide --subject or ensure newsletter-preview.md includes **Subject:**.');
|
|
396
|
+
if (!fromEmail) throw new Error('Missing sender. Provide --from or ensure send-preview.md includes **From:**.');
|
|
397
|
+
if (recipients.length === 0) throw new Error('Missing recipients. Provide --to or ensure send-preview.md includes **To:**.');
|
|
398
|
+
if (mode === 'scheduled') throw new Error('Scheduled mode is not implemented in the native SMTP sender.');
|
|
399
|
+
if (!configuredEnv.smtpHost || !configuredEnv.smtpPort || !configuredEnv.smtpUser || !configuredEnv.smtpPass) {
|
|
400
|
+
throw new Error(
|
|
401
|
+
`Missing SMTP configuration. Checked ${configuredEnv.smtpHostKey}, ${configuredEnv.smtpPortKey}, ${configuredEnv.smtpUserKey}, ${configuredEnv.smtpPassKey}.`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const sentAt = new Date().toISOString();
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
if (args.dryRun) {
|
|
409
|
+
const result = buildEmailSendResult({
|
|
410
|
+
status: 'sent',
|
|
411
|
+
mode,
|
|
412
|
+
messageId: 'dry-run',
|
|
413
|
+
from,
|
|
414
|
+
to: recipients,
|
|
415
|
+
scheduledAt,
|
|
416
|
+
sentAt,
|
|
417
|
+
notes: `Dry run only. SMTP connection not opened. Expected server ${configuredEnv.smtpHost}:${configuredEnv.smtpPort}.`,
|
|
418
|
+
});
|
|
419
|
+
await writeResult(args.outputPath, result);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const message = buildMimeMessage({
|
|
424
|
+
from,
|
|
425
|
+
to: recipients,
|
|
426
|
+
replyTo,
|
|
427
|
+
subject,
|
|
428
|
+
html,
|
|
429
|
+
text,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const delivery = await sendViaSmtp({
|
|
433
|
+
host: configuredEnv.smtpHost,
|
|
434
|
+
port: Number(configuredEnv.smtpPort),
|
|
435
|
+
secure,
|
|
436
|
+
username: configuredEnv.smtpUser,
|
|
437
|
+
password: configuredEnv.smtpPass,
|
|
438
|
+
from: fromEmail,
|
|
439
|
+
to: recipients,
|
|
440
|
+
message,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const result = buildEmailSendResult({
|
|
444
|
+
status: 'sent',
|
|
445
|
+
mode,
|
|
446
|
+
messageId: delivery.messageId,
|
|
447
|
+
from,
|
|
448
|
+
to: recipients,
|
|
449
|
+
scheduledAt,
|
|
450
|
+
sentAt,
|
|
451
|
+
notes: `Accepted by SMTP server ${configuredEnv.smtpHost}:${configuredEnv.smtpPort}. ${delivery.response}`,
|
|
452
|
+
});
|
|
453
|
+
await writeResult(args.outputPath, result);
|
|
454
|
+
console.log(`SMTP send accepted with message ID ${delivery.messageId}`);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
const errorMessage = formatUnknownError(error);
|
|
457
|
+
const result = buildEmailSendResult({
|
|
458
|
+
status: 'failed',
|
|
459
|
+
mode,
|
|
460
|
+
messageId: 'not-returned',
|
|
461
|
+
from,
|
|
462
|
+
to: recipients,
|
|
463
|
+
scheduledAt,
|
|
464
|
+
sentAt,
|
|
465
|
+
notes: errorMessage,
|
|
466
|
+
});
|
|
467
|
+
await writeResult(args.outputPath, result);
|
|
468
|
+
throw new Error(errorMessage);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const isMain = process.argv[1] === fileURLToPath(import.meta.url);
|
|
473
|
+
if (isMain) {
|
|
474
|
+
main().catch((error) => {
|
|
475
|
+
console.error(`\n❌ ${formatUnknownError(error)}`);
|
|
476
|
+
process.exit(1);
|
|
477
|
+
});
|
|
478
|
+
}
|