@stacksjs/buddy 0.70.88 → 0.70.90
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/dist/cli.d.ts +1 -0
- package/dist/cli.js +165 -0
- package/dist/commands/about.d.ts +2 -0
- package/dist/commands/about.js +35 -0
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +33 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +68 -0
- package/dist/commands/build.d.ts +2 -0
- package/dist/commands/build.js +153 -0
- package/dist/commands/cd.d.ts +20 -0
- package/dist/commands/cd.js +39 -0
- package/dist/commands/changelog.d.ts +2 -0
- package/dist/commands/changelog.js +32 -0
- package/dist/commands/clean.d.ts +2 -0
- package/dist/commands/clean.js +37 -0
- package/dist/commands/cloud.d.ts +2 -0
- package/dist/commands/cloud.js +503 -0
- package/dist/commands/commit.d.ts +2 -0
- package/dist/commands/commit.js +16 -0
- package/dist/commands/completion.d.ts +2 -0
- package/dist/commands/completion.js +143 -0
- package/dist/commands/config-migrate.d.ts +26 -0
- package/dist/commands/config-migrate.js +94 -0
- package/dist/commands/configure.d.ts +2 -0
- package/dist/commands/configure.js +48 -0
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.js +141 -0
- package/dist/commands/deploy.d.ts +107 -0
- package/dist/commands/deploy.js +1439 -0
- package/dist/commands/dev.d.ts +10 -0
- package/dist/commands/dev.js +907 -0
- package/dist/commands/dns.d.ts +2 -0
- package/dist/commands/dns.js +102 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +285 -0
- package/dist/commands/domains.d.ts +2 -0
- package/dist/commands/domains.js +159 -0
- package/dist/commands/email.d.ts +2 -0
- package/dist/commands/email.js +524 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/commands/env.js +246 -0
- package/dist/commands/extension.d.ts +8 -0
- package/dist/commands/extension.js +78 -0
- package/dist/commands/features.d.ts +180 -0
- package/dist/commands/features.js +422 -0
- package/dist/commands/fresh.d.ts +2 -0
- package/dist/commands/fresh.js +39 -0
- package/dist/commands/generate.d.ts +2 -0
- package/dist/commands/generate.js +104 -0
- package/dist/commands/http.d.ts +2 -0
- package/dist/commands/http.js +33 -0
- package/dist/commands/index.d.ts +53 -0
- package/dist/commands/index.js +53 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +22 -0
- package/dist/commands/key.d.ts +2 -0
- package/dist/commands/key.js +23 -0
- package/dist/commands/lint.d.ts +2 -0
- package/dist/commands/lint.js +46 -0
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/list.js +108 -0
- package/dist/commands/mail.d.ts +2 -0
- package/dist/commands/mail.js +693 -0
- package/dist/commands/maintenance.d.ts +2 -0
- package/dist/commands/maintenance.js +141 -0
- package/dist/commands/make.d.ts +2 -0
- package/dist/commands/make.js +375 -0
- package/dist/commands/migrate-project.d.ts +2 -0
- package/dist/commands/migrate-project.js +49 -0
- package/dist/commands/migrate.d.ts +4 -0
- package/dist/commands/migrate.js +455 -0
- package/dist/commands/outdated.d.ts +2 -0
- package/dist/commands/outdated.js +19 -0
- package/dist/commands/package.d.ts +2 -0
- package/dist/commands/package.js +17 -0
- package/dist/commands/phone.d.ts +2 -0
- package/dist/commands/phone.js +188 -0
- package/dist/commands/ports.d.ts +2 -0
- package/dist/commands/ports.js +118 -0
- package/dist/commands/prepublish.d.ts +2 -0
- package/dist/commands/prepublish.js +17 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +29 -0
- package/dist/commands/publish.d.ts +2 -0
- package/dist/commands/publish.js +172 -0
- package/dist/commands/queue.d.ts +2 -0
- package/dist/commands/queue.js +248 -0
- package/dist/commands/release.d.ts +2 -0
- package/dist/commands/release.js +30 -0
- package/dist/commands/route.d.ts +2 -0
- package/dist/commands/route.js +21 -0
- package/dist/commands/saas.d.ts +2 -0
- package/dist/commands/saas.js +25 -0
- package/dist/commands/schedule.d.ts +2 -0
- package/dist/commands/schedule.js +61 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +84 -0
- package/dist/commands/seed.d.ts +2 -0
- package/dist/commands/seed.js +71 -0
- package/dist/commands/serve.d.ts +25 -0
- package/dist/commands/serve.js +182 -0
- package/dist/commands/setup.d.ts +12 -0
- package/dist/commands/setup.js +215 -0
- package/dist/commands/share.d.ts +2 -0
- package/dist/commands/share.js +214 -0
- package/dist/commands/sms.d.ts +2 -0
- package/dist/commands/sms.js +328 -0
- package/dist/commands/stacks.d.ts +2 -0
- package/dist/commands/stacks.js +69 -0
- package/dist/commands/telemetry.d.ts +2 -0
- package/dist/commands/telemetry.js +74 -0
- package/dist/commands/test.d.ts +2 -0
- package/dist/commands/test.js +130 -0
- package/dist/commands/tinker.d.ts +2 -0
- package/dist/commands/tinker.js +37 -0
- package/dist/commands/types.d.ts +2 -0
- package/dist/commands/types.js +18 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +98 -0
- package/dist/commands/version.d.ts +2 -0
- package/dist/commands/version.js +16 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.js +223 -0
- package/dist/custom-cli.d.ts +1 -0
- package/dist/custom-cli.js +23 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lazy-commands.d.ts +61 -0
- package/dist/lazy-commands.js +188 -0
- package/dist/migrators/index.d.ts +9 -0
- package/dist/migrators/index.js +62 -0
- package/dist/migrators/laravel/index.d.ts +2 -0
- package/dist/migrators/laravel/index.js +148 -0
- package/dist/migrators/laravel/migrations.d.ts +52 -0
- package/dist/migrators/laravel/migrations.js +232 -0
- package/dist/migrators/laravel/models.d.ts +34 -0
- package/dist/migrators/laravel/models.js +134 -0
- package/dist/migrators/rails/index.d.ts +2 -0
- package/dist/migrators/rails/index.js +11 -0
- package/dist/migrators/types.d.ts +44 -0
- package/dist/migrators/types.js +0 -0
- package/package.json +2 -2
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { email as emailConfig } from "@stacksjs/config";
|
|
4
|
+
import { ExitCode } from "@stacksjs/types";
|
|
5
|
+
import { onUnknownSubcommand } from "@stacksjs/cli";
|
|
6
|
+
import { getErrorMessage } from "@stacksjs/utils";
|
|
7
|
+
const TIMEOUT_MS = 30000;
|
|
8
|
+
async function withTimeout(promise, ms = TIMEOUT_MS) {
|
|
9
|
+
let timeoutId;
|
|
10
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
11
|
+
timeoutId = setTimeout(() => reject(Error(`Operation timed out after ${ms}ms`)), ms);
|
|
12
|
+
});
|
|
13
|
+
try {
|
|
14
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
15
|
+
} finally {
|
|
16
|
+
clearTimeout(timeoutId);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
let _awsCredsLoaded = !1;
|
|
20
|
+
async function validateS3Bucket(bucket, region) {
|
|
21
|
+
try {
|
|
22
|
+
const { S3Client } = await import("@stacksjs/ts-cloud"), s3 = new S3Client(region), list = s3.listObjects;
|
|
23
|
+
if (typeof list !== "function")
|
|
24
|
+
return { ok: !0 };
|
|
25
|
+
await withTimeout(list.call(s3, { bucket, maxKeys: 1 }), 5000);
|
|
26
|
+
return { ok: !0 };
|
|
27
|
+
} catch (err) {
|
|
28
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
29
|
+
if (/NoSuchBucket/i.test(message))
|
|
30
|
+
return {
|
|
31
|
+
ok: !1,
|
|
32
|
+
reason: `Bucket '${bucket}' does not exist in region '${region}'.`,
|
|
33
|
+
hint: "Run `buddy deploy` to create email infrastructure, or pass --bucket <name> if you know the correct bucket."
|
|
34
|
+
};
|
|
35
|
+
if (/AccessDenied|Forbidden/i.test(message))
|
|
36
|
+
return {
|
|
37
|
+
ok: !1,
|
|
38
|
+
reason: `Access denied to bucket '${bucket}'.`,
|
|
39
|
+
hint: "Check that AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (or your IAM role) have s3:ListBucket permission for this bucket."
|
|
40
|
+
};
|
|
41
|
+
if (/region/i.test(message) || /PermanentRedirect/i.test(message))
|
|
42
|
+
return {
|
|
43
|
+
ok: !1,
|
|
44
|
+
reason: `Bucket '${bucket}' is not in region '${region}'.`,
|
|
45
|
+
hint: "Set AWS_REGION to the bucket's actual region, or pass the right bucket for this region."
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
ok: !1,
|
|
49
|
+
reason: message,
|
|
50
|
+
hint: "Check AWS connectivity and credentials."
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function loadAwsCredentials() {
|
|
55
|
+
if (_awsCredsLoaded)
|
|
56
|
+
return;
|
|
57
|
+
_awsCredsLoaded = !0;
|
|
58
|
+
const envPath = ".env.production";
|
|
59
|
+
if (!existsSync(envPath))
|
|
60
|
+
return;
|
|
61
|
+
const { parse } = await import("@stacksjs/env"), content = readFileSync(envPath, "utf-8"), { parsed } = parse(content);
|
|
62
|
+
for (const [key, value] of Object.entries(parsed))
|
|
63
|
+
if (process.env[key] === void 0)
|
|
64
|
+
process.env[key] = value;
|
|
65
|
+
}
|
|
66
|
+
const descriptions = {
|
|
67
|
+
email: "Email server management commands",
|
|
68
|
+
verify: "Check domain verification status",
|
|
69
|
+
test: "Send a test email",
|
|
70
|
+
list: "List configured mailboxes",
|
|
71
|
+
logs: "View email processing logs",
|
|
72
|
+
status: "Show email server status",
|
|
73
|
+
inbox: "View inbox emails from S3",
|
|
74
|
+
reprocess: "Reprocess raw emails from S3 into mailbox structure"
|
|
75
|
+
};
|
|
76
|
+
export function email(buddy) {
|
|
77
|
+
buddy.command("email", descriptions.email).alias("mail").action(async () => {
|
|
78
|
+
console.log(`
|
|
79
|
+
\uD83D\uDCE7 Email Server Commands
|
|
80
|
+
`);
|
|
81
|
+
console.log(" buddy email:verify - Check domain verification status");
|
|
82
|
+
console.log(" buddy email:test - Send a test email");
|
|
83
|
+
console.log(" buddy email:list - List configured mailboxes");
|
|
84
|
+
console.log(" buddy email:inbox - View inbox emails from S3");
|
|
85
|
+
console.log(" buddy email:reprocess - Reprocess raw emails into mailbox structure");
|
|
86
|
+
console.log(" buddy email:logs - View email processing logs");
|
|
87
|
+
console.log(" buddy email:status - Show email server status");
|
|
88
|
+
console.log("");
|
|
89
|
+
});
|
|
90
|
+
buddy.command("email:verify", descriptions.verify).action(async () => {
|
|
91
|
+
console.log(`
|
|
92
|
+
\uD83D\uDCE7 Checking Email Domain Verification...
|
|
93
|
+
`);
|
|
94
|
+
await loadAwsCredentials();
|
|
95
|
+
try {
|
|
96
|
+
const { SESClient } = await import("@stacksjs/ts-cloud"), ses = new SESClient(process.env.AWS_REGION || "us-east-1"), emailDomain = (emailConfig?.from?.address?.includes("@") ? emailConfig.from.address.split("@")[1] : void 0) || "stacksjs.com";
|
|
97
|
+
console.log(`Domain: ${emailDomain}`);
|
|
98
|
+
const identity = await withTimeout(ses.getEmailIdentity(emailDomain));
|
|
99
|
+
if (identity) {
|
|
100
|
+
console.log(`
|
|
101
|
+
\u2705 Domain Status: ${identity.VerificationStatus || "Unknown"}`);
|
|
102
|
+
if (identity.DkimAttributes) {
|
|
103
|
+
console.log(`
|
|
104
|
+
DKIM Status: ${identity.DkimAttributes.Status || "Unknown"}`);
|
|
105
|
+
if (identity.DkimAttributes.Tokens) {
|
|
106
|
+
console.log(`
|
|
107
|
+
DKIM Records needed:`);
|
|
108
|
+
for (const token of identity.DkimAttributes.Tokens) {
|
|
109
|
+
console.log(` CNAME: ${token}._domainkey.${emailDomain}`);
|
|
110
|
+
console.log(` Value: ${token}.dkim.amazonses.com`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} else
|
|
115
|
+
console.log("\n\u26A0\uFE0F Domain not found in SES. Run `buddy deploy` to set up email.");
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error(`
|
|
118
|
+
\u274C Error checking verification:`, getErrorMessage(error));
|
|
119
|
+
}
|
|
120
|
+
process.exit(0);
|
|
121
|
+
});
|
|
122
|
+
buddy.command("email:test [recipient]", descriptions.test).action(async (recipient) => {
|
|
123
|
+
const to = recipient || emailConfig?.from?.address || "test@example.com", { config } = await import("@stacksjs/config"), driver = config?.email?.default || "ses";
|
|
124
|
+
console.log(`
|
|
125
|
+
\uD83D\uDCE7 Sending Test Email to ${to} via the configured mailer ('${driver}')...
|
|
126
|
+
`);
|
|
127
|
+
try {
|
|
128
|
+
const { mail } = await import("@stacksjs/email"), from = {
|
|
129
|
+
name: emailConfig?.from?.name || "Stacks",
|
|
130
|
+
address: emailConfig?.from?.address || `hello@${emailConfig?.domain || "stacksjs.com"}`
|
|
131
|
+
}, result = await mail.send({
|
|
132
|
+
to: [to],
|
|
133
|
+
from,
|
|
134
|
+
subject: "Test Email from Stacks",
|
|
135
|
+
text: "This is a test email sent from your Stacks application. If you received this, your mail setup is working.",
|
|
136
|
+
html: `<!doctype html><html><body style="font-family: sans-serif; padding: 20px;">
|
|
137
|
+
<h1>\uD83D\uDE80 Test Email from Stacks</h1>
|
|
138
|
+
<p>This is a test email sent from your Stacks application.</p>
|
|
139
|
+
<p>If you received this, your mail setup is working correctly!</p>
|
|
140
|
+
<hr><p style="color:#666;font-size:12px;">Sent from ${from.address} via the '${driver}' mailer.</p>
|
|
141
|
+
</body></html>`
|
|
142
|
+
});
|
|
143
|
+
if (result?.success === !1)
|
|
144
|
+
throw Error(result?.message || "send returned success=false");
|
|
145
|
+
console.log(`\u2705 Test email sent via '${driver}'!`);
|
|
146
|
+
if (result?.messageId)
|
|
147
|
+
console.log(` Message ID: ${result.messageId}`);
|
|
148
|
+
if (driver === "log" || driver === "capture")
|
|
149
|
+
console.log(`
|
|
150
|
+
\uD83D\uDCA1 The '${driver}' mailer does not deliver mail \u2014 set MAIL_MAILER=smtp (or ses) to send for real.`);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error(`
|
|
153
|
+
\u274C Error sending test email via '${driver}':`, getErrorMessage(error));
|
|
154
|
+
if (driver === "smtp")
|
|
155
|
+
console.log(`
|
|
156
|
+
\uD83D\uDCA1 Check MAIL_HOST/MAIL_PORT/MAIL_USERNAME/MAIL_PASSWORD/MAIL_ENCRYPTION in your .env.`);
|
|
157
|
+
else if (getErrorMessage(error).includes("not verified"))
|
|
158
|
+
console.log("\n\uD83D\uDCA1 Tip: make sure your domain is verified in SES (`buddy email:verify`).");
|
|
159
|
+
}
|
|
160
|
+
process.exit(0);
|
|
161
|
+
});
|
|
162
|
+
buddy.command("email:list", descriptions.list).action(async () => {
|
|
163
|
+
console.log(`
|
|
164
|
+
\uD83D\uDCE7 Configured Mailboxes
|
|
165
|
+
`);
|
|
166
|
+
const emailDomain = (emailConfig?.from?.address?.includes("@") ? emailConfig.from.address.split("@")[1] : void 0) || "stacksjs.com", mailboxes = emailConfig?.mailboxes || [];
|
|
167
|
+
console.log(`Domain: ${emailDomain}`);
|
|
168
|
+
console.log(`Default From: ${emailConfig?.from?.name} <${emailConfig?.from?.address}>`);
|
|
169
|
+
console.log("");
|
|
170
|
+
if (mailboxes.length === 0) {
|
|
171
|
+
console.log("No mailboxes configured.");
|
|
172
|
+
console.log(`
|
|
173
|
+
\uD83D\uDCA1 Add mailboxes in config/email.ts:`);
|
|
174
|
+
console.log(' mailboxes: ["user@domain.com", "another@domain.com"]');
|
|
175
|
+
} else {
|
|
176
|
+
console.log("Mailboxes:");
|
|
177
|
+
for (const mailbox of mailboxes)
|
|
178
|
+
console.log(` \uD83D\uDCEC ${mailbox}`);
|
|
179
|
+
}
|
|
180
|
+
console.log("");
|
|
181
|
+
process.exit(0);
|
|
182
|
+
});
|
|
183
|
+
buddy.command("email:logs", descriptions.logs).option("-n, --lines <count>", "Number of log lines to show", { default: "20" }).action(async (options) => {
|
|
184
|
+
console.log(`
|
|
185
|
+
\uD83D\uDCE7 Email Processing Logs
|
|
186
|
+
`);
|
|
187
|
+
await loadAwsCredentials();
|
|
188
|
+
try {
|
|
189
|
+
const { CloudWatchLogsClient } = await import("@stacksjs/ts-cloud"), logs = new CloudWatchLogsClient(process.env.AWS_REGION || "us-east-1"), logGroupName = `/aws/lambda/${(process.env.APP_NAME || "stacks").toLowerCase().replace(/[^a-z0-9-]/g, "-")}-inbound-email`;
|
|
190
|
+
console.log(`Log Group: ${logGroupName}`);
|
|
191
|
+
console.log(`Showing last ${options.lines} events...
|
|
192
|
+
`);
|
|
193
|
+
const streams = await withTimeout(logs.describeLogStreams({
|
|
194
|
+
logGroupName,
|
|
195
|
+
orderBy: "LastEventTime",
|
|
196
|
+
descending: !0,
|
|
197
|
+
limit: 1
|
|
198
|
+
}));
|
|
199
|
+
if (!streams.logStreams || streams.logStreams.length === 0) {
|
|
200
|
+
console.log("No log streams found.");
|
|
201
|
+
console.log(`
|
|
202
|
+
\uD83D\uDCA1 Logs will appear after emails are processed.`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const logStreamName = streams.logStreams[0].logStreamName;
|
|
206
|
+
if (!logStreamName) {
|
|
207
|
+
console.log("No log stream name found.");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const events = await withTimeout(logs.getLogEvents({
|
|
211
|
+
logGroupName,
|
|
212
|
+
logStreamName,
|
|
213
|
+
limit: parseInt(options.lines || "20", 10)
|
|
214
|
+
}));
|
|
215
|
+
if (events.events && events.events.length > 0)
|
|
216
|
+
for (const event of events.events) {
|
|
217
|
+
const time = new Date(event.timestamp || 0).toISOString();
|
|
218
|
+
console.log(`[${time}] ${event.message}`);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
console.log("No log events found.");
|
|
222
|
+
console.log(`
|
|
223
|
+
\uD83D\uDCA1 Logs will appear after emails are processed.`);
|
|
224
|
+
}
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (getErrorMessage(error).includes("ResourceNotFoundException") || getErrorMessage(error).includes("timed out"))
|
|
227
|
+
console.log("Log group not found or not accessible. No emails have been processed yet.");
|
|
228
|
+
else
|
|
229
|
+
console.error("Error fetching logs:", getErrorMessage(error));
|
|
230
|
+
}
|
|
231
|
+
process.exit(0);
|
|
232
|
+
});
|
|
233
|
+
buddy.command("email:status", descriptions.status).action(async () => {
|
|
234
|
+
console.log(`
|
|
235
|
+
\uD83D\uDCE7 Email Server Status
|
|
236
|
+
`);
|
|
237
|
+
await loadAwsCredentials();
|
|
238
|
+
try {
|
|
239
|
+
const { AWSCloudFormationClient } = await import("@stacksjs/ts-cloud"), cf = new AWSCloudFormationClient(process.env.AWS_REGION || "us-east-1"), stackName = `${(process.env.APP_NAME || "stacks").toLowerCase().replace(/[^a-z0-9-]/g, "-")}-cloud`, emailResources = (await withTimeout(cf.listStackResources(stackName))).StackResourceSummaries?.filter((r) => r.LogicalResourceId.includes("Email") || r.LogicalResourceId.includes("Inbound") || r.LogicalResourceId.includes("Outbound")) || [];
|
|
240
|
+
if (emailResources.length === 0) {
|
|
241
|
+
console.log("\u274C Email server not deployed.");
|
|
242
|
+
console.log("\n\uD83D\uDCA1 Run `buddy deploy` to set up email infrastructure.");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
console.log(`\u2705 Email Server Deployed
|
|
246
|
+
`);
|
|
247
|
+
console.log("Resources:");
|
|
248
|
+
for (const resource of emailResources) {
|
|
249
|
+
const status = resource.ResourceStatus === "CREATE_COMPLETE" || resource.ResourceStatus === "UPDATE_COMPLETE" ? "\u2705" : "\u23F3";
|
|
250
|
+
console.log(` ${status} ${resource.LogicalResourceId}`);
|
|
251
|
+
}
|
|
252
|
+
const emailOutputs = ((await withTimeout(cf.describeStacks({ stackName }))).Stacks?.[0]?.Outputs || []).filter((o) => o.OutputKey?.includes("Email"));
|
|
253
|
+
if (emailOutputs.length > 0) {
|
|
254
|
+
console.log(`
|
|
255
|
+
Configuration:`);
|
|
256
|
+
for (const output of emailOutputs)
|
|
257
|
+
console.log(` ${output.OutputKey}: ${output.OutputValue}`);
|
|
258
|
+
}
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error("Error checking status:", getErrorMessage(error));
|
|
261
|
+
}
|
|
262
|
+
process.exit(0);
|
|
263
|
+
});
|
|
264
|
+
buddy.command("email:inbox [mailbox]", descriptions.inbox).option("-n, --limit <count>", "Number of emails to show", { default: "20" }).option("--raw <id>", "Show raw email content for a specific message ID").option("--bucket <name>", "S3 bucket name override").action(async (mailbox, options) => {
|
|
265
|
+
await loadAwsCredentials();
|
|
266
|
+
const region = process.env.AWS_REGION || "us-east-1", appName = (process.env.APP_NAME || "stacks").toLowerCase().replace(/[^a-z0-9-]/g, "-"), emailDomain = (emailConfig?.from?.address?.includes("@") ? emailConfig.from.address.split("@")[1] : void 0) || "stacksjs.com", bucketName = options?.bucket || `${appName}-production-email`, probe = await validateS3Bucket(bucketName, region);
|
|
267
|
+
if (!probe.ok) {
|
|
268
|
+
console.error(`
|
|
269
|
+
\u274C ${probe.reason}
|
|
270
|
+
\uD83D\uDCA1 ${probe.hint}
|
|
271
|
+
`);
|
|
272
|
+
process.exit(ExitCode.FatalError);
|
|
273
|
+
}
|
|
274
|
+
if (options?.raw) {
|
|
275
|
+
console.log(`
|
|
276
|
+
\uD83D\uDCE7 Fetching raw email: ${options.raw}
|
|
277
|
+
`);
|
|
278
|
+
try {
|
|
279
|
+
const { S3Client } = await import("@stacksjs/ts-cloud"), s3 = new S3Client(region);
|
|
280
|
+
let rawContent = null;
|
|
281
|
+
try {
|
|
282
|
+
rawContent = await withTimeout(s3.getObject(bucketName, `inbox/${options.raw}`));
|
|
283
|
+
} catch (e) {
|
|
284
|
+
if (mailbox) {
|
|
285
|
+
const [localPart, domain] = mailbox.includes("@") ? mailbox.split("@") : [mailbox, emailDomain], matchingKey = (await withTimeout(s3.listObjects({
|
|
286
|
+
bucket: bucketName,
|
|
287
|
+
prefix: `mailboxes/${domain}/${localPart}/`,
|
|
288
|
+
maxKeys: 1000
|
|
289
|
+
}))).Contents?.find((obj) => obj.Key.includes(options.raw) && obj.Key.endsWith("/raw.eml"));
|
|
290
|
+
if (matchingKey)
|
|
291
|
+
rawContent = await withTimeout(s3.getObject(bucketName, matchingKey.Key));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (rawContent)
|
|
295
|
+
console.log(rawContent);
|
|
296
|
+
else
|
|
297
|
+
console.log("Email not found.");
|
|
298
|
+
} catch (error) {
|
|
299
|
+
console.error("Error fetching email:", getErrorMessage(error));
|
|
300
|
+
}
|
|
301
|
+
process.exit(0);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const resolvedMailbox = mailbox || `chris@${emailDomain}`, [localPart, domain] = resolvedMailbox.includes("@") ? resolvedMailbox.split("@") : [resolvedMailbox, emailDomain];
|
|
305
|
+
console.log(`
|
|
306
|
+
\uD83D\uDCE7 Inbox for ${localPart}@${domain}
|
|
307
|
+
`);
|
|
308
|
+
console.log(` Bucket: ${bucketName}`);
|
|
309
|
+
console.log("");
|
|
310
|
+
try {
|
|
311
|
+
const { S3Client } = await import("@stacksjs/ts-cloud"), s3 = new S3Client(region), limit = parseInt(options?.limit || "20", 10), inboxKey = `mailboxes/${domain}/${localPart}/inbox.json`;
|
|
312
|
+
let inboxEmails = [], source = "mailboxes";
|
|
313
|
+
try {
|
|
314
|
+
const inboxData = await withTimeout(s3.getObject(bucketName, inboxKey));
|
|
315
|
+
if (inboxData)
|
|
316
|
+
try {
|
|
317
|
+
inboxEmails = JSON.parse(inboxData);
|
|
318
|
+
source = "mailboxes";
|
|
319
|
+
} catch {
|
|
320
|
+
inboxEmails = [];
|
|
321
|
+
}
|
|
322
|
+
} catch {}
|
|
323
|
+
if (inboxEmails.length === 0) {
|
|
324
|
+
console.log(` No processed inbox found. Scanning raw emails from inbox/ ...
|
|
325
|
+
`);
|
|
326
|
+
source = "raw";
|
|
327
|
+
try {
|
|
328
|
+
const objects = (await withTimeout(s3.listObjects({
|
|
329
|
+
bucket: bucketName,
|
|
330
|
+
prefix: "inbox/",
|
|
331
|
+
maxKeys: limit
|
|
332
|
+
}))).Contents || [];
|
|
333
|
+
if (objects.length === 0) {
|
|
334
|
+
const objects2 = (await withTimeout(s3.listObjects({
|
|
335
|
+
bucket: bucketName,
|
|
336
|
+
prefix: "incoming/",
|
|
337
|
+
maxKeys: limit
|
|
338
|
+
}))).Contents || [];
|
|
339
|
+
objects.push(...objects2);
|
|
340
|
+
}
|
|
341
|
+
for (const obj of objects.slice(0, limit))
|
|
342
|
+
try {
|
|
343
|
+
const rawEmail = await withTimeout(s3.getObject(bucketName, obj.Key));
|
|
344
|
+
if (!rawEmail)
|
|
345
|
+
continue;
|
|
346
|
+
const headers = parseRawEmailHeaders(rawEmail);
|
|
347
|
+
inboxEmails.push({
|
|
348
|
+
messageId: obj.Key.split("/").pop() || obj.Key,
|
|
349
|
+
from: headers.from || "unknown",
|
|
350
|
+
subject: headers.subject || "No Subject",
|
|
351
|
+
date: headers.date || obj.LastModified || "unknown",
|
|
352
|
+
path: obj.Key
|
|
353
|
+
});
|
|
354
|
+
} catch (parseErr) {
|
|
355
|
+
inboxEmails.push({
|
|
356
|
+
messageId: obj.Key.split("/").pop() || obj.Key,
|
|
357
|
+
from: "unknown",
|
|
358
|
+
subject: "(could not parse)",
|
|
359
|
+
date: obj.LastModified || "unknown",
|
|
360
|
+
path: obj.Key
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
} catch (listErr) {
|
|
364
|
+
console.error("Error listing emails:", listErr.message);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (inboxEmails.length === 0) {
|
|
368
|
+
console.log(" No emails found.");
|
|
369
|
+
console.log(`
|
|
370
|
+
Waiting for emails to arrive at inbox/ in the S3 bucket.`);
|
|
371
|
+
console.log(" Send an email to an @" + domain + " address to test.");
|
|
372
|
+
process.exit(0);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
console.log(` Source: ${source} (${inboxEmails.length} emails)
|
|
376
|
+
`);
|
|
377
|
+
console.log(" %-4s %-20s %-30s %s", "#", "Date", "From", "Subject");
|
|
378
|
+
console.log(" " + "-".repeat(90));
|
|
379
|
+
for (let i = 0;i < Math.min(inboxEmails.length, limit); i++) {
|
|
380
|
+
const e = inboxEmails[i], date = e.date ? new Date(e.date).toLocaleDateString("en-US", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : "?", from = (e.from || "").substring(0, 20).padEnd(20), subject = (e.subject || "").substring(0, 40), readMark = e.read === !1 ? "*" : " ";
|
|
381
|
+
console.log(` ${readMark}${String(i + 1).padStart(3)} ${date.padEnd(20)} ${from} ${subject}`);
|
|
382
|
+
}
|
|
383
|
+
console.log("");
|
|
384
|
+
console.log(" * = unread");
|
|
385
|
+
console.log(`
|
|
386
|
+
View raw email: buddy email:inbox ${resolvedMailbox} --raw <messageId>`);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
console.error("Error reading inbox:", getErrorMessage(error));
|
|
389
|
+
}
|
|
390
|
+
process.exit(0);
|
|
391
|
+
});
|
|
392
|
+
buddy.command("email:reprocess", descriptions.reprocess).option("--bucket <name>", "S3 bucket name override").option("--prefix <prefix>", "S3 prefix to scan", { default: "inbox/" }).option("--domain <domain>", "Email domain", { default: "stacksjs.com" }).action(async (options) => {
|
|
393
|
+
await loadAwsCredentials();
|
|
394
|
+
const region = process.env.AWS_REGION || "us-east-1", appName = (process.env.APP_NAME || "stacks").toLowerCase().replace(/[^a-z0-9-]/g, "-"), bucketName = options?.bucket || `${appName}-production-email`, prefix = options?.prefix || "inbox/", domain = options?.domain || "stacksjs.com";
|
|
395
|
+
console.log(`
|
|
396
|
+
\uD83D\uDCE7 Reprocessing emails from s3://${bucketName}/${prefix}
|
|
397
|
+
`);
|
|
398
|
+
try {
|
|
399
|
+
const { S3Client } = await import("@stacksjs/ts-cloud"), s3 = new S3Client(region), objects = (await withTimeout(s3.listObjects({
|
|
400
|
+
bucket: bucketName,
|
|
401
|
+
prefix,
|
|
402
|
+
maxKeys: 1000
|
|
403
|
+
}), 60000)).Contents || [];
|
|
404
|
+
console.log(` Found ${objects.length} raw emails to process
|
|
405
|
+
`);
|
|
406
|
+
const inboxes = {};
|
|
407
|
+
let processed = 0;
|
|
408
|
+
for (const obj of objects) {
|
|
409
|
+
if (!obj.Key || obj.Key.endsWith("/"))
|
|
410
|
+
continue;
|
|
411
|
+
try {
|
|
412
|
+
const rawEmail = await withTimeout(s3.getObject(bucketName, obj.Key), 15000);
|
|
413
|
+
if (!rawEmail)
|
|
414
|
+
continue;
|
|
415
|
+
const headers = parseRawEmailHeaders(rawEmail), messageId = obj.Key.split("/").pop() || obj.Key, from = extractEmailAddress(headers.from || ""), fromName = extractEmailName(headers.from || ""), toList = (headers.to || "").split(",").map((s) => s.trim()).filter(Boolean), subject = headers.subject || "No Subject", date = headers.date || (obj.LastModified ? new Date(obj.LastModified).toISOString() : new Date().toISOString());
|
|
416
|
+
let preview = "";
|
|
417
|
+
const bodyStart = rawEmail.indexOf(`\r
|
|
418
|
+
\r
|
|
419
|
+
`);
|
|
420
|
+
if (bodyStart > 0)
|
|
421
|
+
preview = rawEmail.substring(bodyStart + 4, bodyStart + 500).replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim().substring(0, 200);
|
|
422
|
+
const recipients = toList.length > 0 ? toList : [`unknown@${domain}`];
|
|
423
|
+
for (const recipient of recipients) {
|
|
424
|
+
const recipientEmail = extractEmailAddress(recipient);
|
|
425
|
+
if (!recipientEmail.endsWith(`@${domain}`))
|
|
426
|
+
continue;
|
|
427
|
+
const [localPart] = recipientEmail.split("@"), d = new Date(date), year = d.getFullYear(), month = String(d.getMonth() + 1).padStart(2, "0"), day = String(d.getDate()).padStart(2, "0"), emailPath = `mailboxes/${domain}/${localPart}/${year}/${month}/${day}/${messageId}`;
|
|
428
|
+
await s3.putObject({
|
|
429
|
+
bucket: bucketName,
|
|
430
|
+
key: `${emailPath}/raw.eml`,
|
|
431
|
+
body: rawEmail,
|
|
432
|
+
contentType: "message/rfc822"
|
|
433
|
+
});
|
|
434
|
+
const metadata = { messageId, from, fromName, to: recipientEmail, subject, date, preview, hasAttachments: !1 };
|
|
435
|
+
await s3.putObject({
|
|
436
|
+
bucket: bucketName,
|
|
437
|
+
key: `${emailPath}/metadata.json`,
|
|
438
|
+
body: JSON.stringify(metadata, null, 2),
|
|
439
|
+
contentType: "application/json"
|
|
440
|
+
});
|
|
441
|
+
const inboxKey = `${domain}/${localPart}`;
|
|
442
|
+
if (!inboxes[inboxKey])
|
|
443
|
+
inboxes[inboxKey] = [];
|
|
444
|
+
inboxes[inboxKey].push({
|
|
445
|
+
messageId,
|
|
446
|
+
from,
|
|
447
|
+
fromName,
|
|
448
|
+
to: recipientEmail,
|
|
449
|
+
subject,
|
|
450
|
+
date,
|
|
451
|
+
read: !1,
|
|
452
|
+
preview,
|
|
453
|
+
hasAttachments: !1,
|
|
454
|
+
path: emailPath
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
processed++;
|
|
458
|
+
if (processed % 5 === 0)
|
|
459
|
+
console.log(` Processed ${processed}/${objects.length} emails...`);
|
|
460
|
+
} catch (emailErr) {
|
|
461
|
+
console.log(` Skipping ${obj.Key}: ${emailErr.message}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
for (const [key, emails] of Object.entries(inboxes)) {
|
|
465
|
+
const [d, localPart] = key.split("/"), inboxJsonKey = `mailboxes/${d}/${localPart}/inbox.json`;
|
|
466
|
+
emails.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
467
|
+
let existing = [];
|
|
468
|
+
try {
|
|
469
|
+
const existingData = await s3.getObject(bucketName, inboxJsonKey);
|
|
470
|
+
if (existingData)
|
|
471
|
+
try {
|
|
472
|
+
existing = JSON.parse(existingData);
|
|
473
|
+
} catch {
|
|
474
|
+
existing = [];
|
|
475
|
+
}
|
|
476
|
+
} catch {}
|
|
477
|
+
const existingIds = new Set(existing.map((e) => e.messageId)), newEmails = emails.filter((e) => !existingIds.has(e.messageId)), merged = [...newEmails, ...existing].slice(0, 1000);
|
|
478
|
+
await s3.putObject({
|
|
479
|
+
bucket: bucketName,
|
|
480
|
+
key: inboxJsonKey,
|
|
481
|
+
body: JSON.stringify(merged, null, 2),
|
|
482
|
+
contentType: "application/json"
|
|
483
|
+
});
|
|
484
|
+
console.log(` Updated inbox for ${localPart}@${d}: ${newEmails.length} new emails (${merged.length} total)`);
|
|
485
|
+
}
|
|
486
|
+
console.log(`
|
|
487
|
+
Done! Processed ${processed} emails.`);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
console.error("Error reprocessing:", getErrorMessage(error));
|
|
490
|
+
}
|
|
491
|
+
process.exit(0);
|
|
492
|
+
});
|
|
493
|
+
onUnknownSubcommand(buddy, "email");
|
|
494
|
+
}
|
|
495
|
+
function parseRawEmailHeaders(rawEmail) {
|
|
496
|
+
const headers = {}, lines = rawEmail.split(/\r?\n/);
|
|
497
|
+
let currentKey = "", currentValue = "";
|
|
498
|
+
for (const line of lines) {
|
|
499
|
+
if (line === "")
|
|
500
|
+
break;
|
|
501
|
+
if (/^\s/.test(line) && currentKey) {
|
|
502
|
+
currentValue += " " + line.trim();
|
|
503
|
+
headers[currentKey] = currentValue;
|
|
504
|
+
} else {
|
|
505
|
+
const match = line.match(/^([^:]+):\s*(.*)$/);
|
|
506
|
+
if (match) {
|
|
507
|
+
currentKey = match[1].toLowerCase();
|
|
508
|
+
currentValue = match[2] ?? "";
|
|
509
|
+
headers[currentKey] = currentValue;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return headers;
|
|
514
|
+
}
|
|
515
|
+
function extractEmailAddress(str) {
|
|
516
|
+
if (!str)
|
|
517
|
+
return "";
|
|
518
|
+
return (str.match(/<([^>]+)>/)?.[1] ?? str).toLowerCase().trim();
|
|
519
|
+
}
|
|
520
|
+
function extractEmailName(str) {
|
|
521
|
+
if (!str)
|
|
522
|
+
return "";
|
|
523
|
+
return str.match(/^"?([^"<]+)"?\s*</)?.[1]?.trim() ?? "";
|
|
524
|
+
}
|