@sesender/cli 1.0.0 → 1.0.3
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 +332 -26
- package/bin/ses-cli.mjs +177 -31
- package/package.json +14 -4
- package/src/commands/account.mjs +48 -0
- package/src/commands/campaigns.mjs +139 -23
- package/src/commands/contacts.mjs +96 -18
- package/src/commands/email.mjs +113 -39
- package/src/commands/providers.mjs +50 -0
- package/src/commands/schedule.mjs +52 -0
- package/src/commands/seedcheck.mjs +97 -0
- package/src/commands/sms.mjs +94 -32
- package/src/commands/templates.mjs +57 -0
- package/src/commands/tracking.mjs +61 -0
- package/src/commands/validate.mjs +112 -16
- package/src/commands/webhooks.mjs +103 -0
package/src/commands/sms.mjs
CHANGED
|
@@ -1,66 +1,123 @@
|
|
|
1
1
|
import { readFileSync } from "fs";
|
|
2
2
|
import { apiRequest } from "../api.mjs";
|
|
3
|
-
import { formatTable, outputResult, success } from "../output.mjs";
|
|
3
|
+
import { formatTable, outputResult, success, info } from "../output.mjs";
|
|
4
4
|
|
|
5
5
|
export async function smsSend(args, jsonMode) {
|
|
6
6
|
const to = args["--to"];
|
|
7
|
-
const body = args["--body"];
|
|
8
|
-
const from = args["--from"];
|
|
7
|
+
const body = args["--body"] || args["--message"];
|
|
8
|
+
const from = args["--from"] || args["--sender"];
|
|
9
|
+
const provider = args["--provider"];
|
|
10
|
+
const route = args["--route"];
|
|
11
|
+
const schedule = args["--schedule"];
|
|
9
12
|
|
|
10
13
|
if (!to || !body) {
|
|
11
|
-
console.error(
|
|
14
|
+
console.error(`
|
|
15
|
+
Usage: ses-cli sms send --to PHONE --body MESSAGE [options]
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--to Recipient phone number (E.164 format, e.g., +1234567890)
|
|
19
|
+
--body SMS message text (aliases: --message)
|
|
20
|
+
--from Sender ID or phone number (aliases: --sender)
|
|
21
|
+
--provider Provider ID to use (see: ses-cli providers list)
|
|
22
|
+
--route Route name for the provider (e.g., DEFAULT, PREMIUM)
|
|
23
|
+
--schedule Schedule send time (ISO 8601, e.g., 2026-06-15T09:00:00Z)
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
ses-cli sms send --to +1234567890 --body "Hello!"
|
|
27
|
+
ses-cli sms send --to +1234567890 --body "VIP offer" --provider 5 --route PREMIUM
|
|
28
|
+
ses-cli sms send --to +1234567890 --body "Reminder" --schedule "2026-06-15T09:00:00Z"
|
|
29
|
+
`);
|
|
12
30
|
process.exit(1);
|
|
13
31
|
}
|
|
14
32
|
|
|
15
|
-
const payload = { to, body };
|
|
16
|
-
if (from) payload.
|
|
33
|
+
const payload = { to, message: body };
|
|
34
|
+
if (from) payload.senderId = from;
|
|
35
|
+
if (provider) payload.providerId = parseInt(provider);
|
|
36
|
+
if (route) payload.routeName = route;
|
|
37
|
+
if (schedule) payload.scheduledAt = schedule;
|
|
17
38
|
|
|
18
39
|
const result = await apiRequest("POST", "/sms/send", payload);
|
|
19
40
|
|
|
20
41
|
if (jsonMode) {
|
|
21
42
|
outputResult(result, true);
|
|
22
43
|
} else {
|
|
23
|
-
success(`SMS sent to ${to}`);
|
|
24
|
-
if (result.messageId) console.log(` Message ID: ${result.messageId}`);
|
|
25
|
-
if (result.
|
|
44
|
+
success(`SMS ${schedule ? "scheduled" : "sent"} to ${to}`);
|
|
45
|
+
if (result.data?.messageId) console.log(` Message ID: ${result.data.messageId}`);
|
|
46
|
+
if (result.data?.provider) console.log(` Provider: ${result.data.provider}`);
|
|
47
|
+
if (schedule) console.log(` Scheduled: ${schedule}`);
|
|
26
48
|
}
|
|
27
49
|
}
|
|
28
50
|
|
|
29
51
|
export async function smsBulk(args, jsonMode) {
|
|
30
52
|
const file = args["--file"];
|
|
31
|
-
const body = args["--body"];
|
|
32
|
-
const from = args["--from"];
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
53
|
+
const body = args["--body"] || args["--message"];
|
|
54
|
+
const from = args["--from"] || args["--sender"];
|
|
55
|
+
const provider = args["--provider"];
|
|
56
|
+
const route = args["--route"];
|
|
57
|
+
const schedule = args["--schedule"];
|
|
58
|
+
const delay = parseInt(args["--delay"] || "0");
|
|
59
|
+
|
|
60
|
+
if (!file || !body) {
|
|
61
|
+
console.error(`
|
|
62
|
+
Usage: ses-cli sms bulk --file FILE --body MESSAGE [options]
|
|
63
|
+
|
|
64
|
+
Options:
|
|
65
|
+
--file CSV file with phone numbers (column: phone or first column)
|
|
66
|
+
--body SMS message text. Use [[ColumnName]] for personalization
|
|
67
|
+
--from Sender ID or phone number
|
|
68
|
+
--provider Provider ID to use
|
|
69
|
+
--route Route name (e.g., DEFAULT, PREMIUM)
|
|
70
|
+
--schedule Schedule send time (ISO 8601)
|
|
71
|
+
--delay Delay between messages in ms (default: 0)
|
|
72
|
+
|
|
73
|
+
Personalization:
|
|
74
|
+
Use [[First]], [[Last]], [[Custom1]], etc. in your message body.
|
|
75
|
+
Column names are matched from your CSV headers.
|
|
76
|
+
|
|
77
|
+
Examples:
|
|
78
|
+
ses-cli sms bulk --file contacts.csv --body "Hi [[First]], check out our sale!"
|
|
79
|
+
ses-cli sms bulk --file vip.csv --body "VIP: 50% off" --provider 3 --route PREMIUM
|
|
80
|
+
`);
|
|
37
81
|
process.exit(1);
|
|
38
82
|
}
|
|
39
83
|
|
|
40
84
|
const csvContent = readFileSync(file, "utf-8");
|
|
41
85
|
const lines = csvContent.trim().split("\n");
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
.filter(Boolean);
|
|
86
|
+
const headers = lines[0].toLowerCase().split(",").map(h => h.trim().replace(/"/g, ""));
|
|
87
|
+
const phoneIdx = headers.indexOf("phone") !== -1 ? headers.indexOf("phone") : 0;
|
|
88
|
+
const dataLines = lines.slice(1);
|
|
46
89
|
|
|
47
|
-
if (
|
|
48
|
-
console.error("Error: No
|
|
90
|
+
if (dataLines.length === 0) {
|
|
91
|
+
console.error("Error: No data rows found in CSV file");
|
|
49
92
|
process.exit(1);
|
|
50
93
|
}
|
|
51
94
|
|
|
52
|
-
|
|
95
|
+
info(`Sending to ${dataLines.length} recipients...`);
|
|
53
96
|
|
|
54
97
|
let sent = 0;
|
|
55
98
|
let failed = 0;
|
|
56
99
|
const errors = [];
|
|
57
100
|
|
|
58
|
-
for (const
|
|
101
|
+
for (const line of dataLines) {
|
|
102
|
+
const cols = line.split(",").map(c => c.trim().replace(/"/g, ""));
|
|
103
|
+
const phone = cols[phoneIdx];
|
|
104
|
+
if (!phone) continue;
|
|
105
|
+
|
|
106
|
+
// Replace personalization tokens
|
|
107
|
+
let personalizedBody = body;
|
|
108
|
+
headers.forEach((header, idx) => {
|
|
109
|
+
const regex = new RegExp(`\\[\\[${header}\\]\\]`, "gi");
|
|
110
|
+
personalizedBody = personalizedBody.replace(regex, cols[idx] || "");
|
|
111
|
+
});
|
|
112
|
+
|
|
59
113
|
try {
|
|
60
|
-
const payload = { to: phone,
|
|
61
|
-
if (from) payload.
|
|
114
|
+
const payload = { to: phone, message: personalizedBody };
|
|
115
|
+
if (from) payload.senderId = from;
|
|
116
|
+
if (provider) payload.providerId = parseInt(provider);
|
|
117
|
+
if (route) payload.routeName = route;
|
|
62
118
|
await apiRequest("POST", "/sms/send", payload);
|
|
63
119
|
sent++;
|
|
120
|
+
if (delay > 0) await new Promise(r => setTimeout(r, delay));
|
|
64
121
|
} catch {
|
|
65
122
|
failed++;
|
|
66
123
|
errors.push(phone);
|
|
@@ -68,11 +125,11 @@ export async function smsBulk(args, jsonMode) {
|
|
|
68
125
|
}
|
|
69
126
|
|
|
70
127
|
if (jsonMode) {
|
|
71
|
-
outputResult({ sent, failed, total:
|
|
128
|
+
outputResult({ sent, failed, total: dataLines.length, errors }, true);
|
|
72
129
|
} else {
|
|
73
|
-
success(`Bulk SMS complete: ${sent} sent, ${failed} failed out of ${
|
|
130
|
+
success(`Bulk SMS complete: ${sent} sent, ${failed} failed out of ${dataLines.length}`);
|
|
74
131
|
if (errors.length > 0) {
|
|
75
|
-
console.log(` Failed
|
|
132
|
+
console.log(` Failed: ${errors.slice(0, 5).join(", ")}${errors.length > 5 ? ` +${errors.length - 5} more` : ""}`);
|
|
76
133
|
}
|
|
77
134
|
}
|
|
78
135
|
}
|
|
@@ -80,24 +137,29 @@ export async function smsBulk(args, jsonMode) {
|
|
|
80
137
|
export async function smsHistory(args, jsonMode) {
|
|
81
138
|
const limit = args["--limit"] || "20";
|
|
82
139
|
const page = args["--page"] || "1";
|
|
140
|
+
const status = args["--status"];
|
|
141
|
+
|
|
142
|
+
const params = { limit, page };
|
|
143
|
+
if (status) params.status = status;
|
|
83
144
|
|
|
84
|
-
const result = await apiRequest("GET", "/sms/
|
|
145
|
+
const result = await apiRequest("GET", "/sms/messages", null, params);
|
|
85
146
|
|
|
86
147
|
if (jsonMode) {
|
|
87
148
|
outputResult(result, true);
|
|
88
149
|
} else {
|
|
89
|
-
const messages = result.messages || result.data || [];
|
|
150
|
+
const messages = result.data?.messages || result.messages || result.data || [];
|
|
90
151
|
if (messages.length === 0) {
|
|
91
152
|
console.log("No SMS messages found.");
|
|
92
153
|
return;
|
|
93
154
|
}
|
|
94
155
|
formatTable(
|
|
95
|
-
["To", "Status", "Date", "Body"],
|
|
156
|
+
["To", "Status", "Provider", "Date", "Body"],
|
|
96
157
|
messages.map((m) => [
|
|
97
158
|
m.to || m.recipient,
|
|
98
159
|
m.status || "sent",
|
|
160
|
+
m.provider || "-",
|
|
99
161
|
new Date(m.createdAt || m.sentAt).toLocaleString(),
|
|
100
|
-
(m.body || m.message || "").slice(0,
|
|
162
|
+
(m.body || m.message || "").slice(0, 30),
|
|
101
163
|
])
|
|
102
164
|
);
|
|
103
165
|
console.log(`\nShowing ${messages.length} messages (page ${page})`);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { apiRequest } from "../api.mjs";
|
|
2
|
+
import { formatTable, outputResult, success } from "../output.mjs";
|
|
3
|
+
|
|
4
|
+
export async function templatesList(args, jsonMode) {
|
|
5
|
+
const type = args["--type"]; // sms or email
|
|
6
|
+
|
|
7
|
+
const params = {};
|
|
8
|
+
if (type) params.type = type;
|
|
9
|
+
|
|
10
|
+
const result = await apiRequest("GET", "/templates", null, params);
|
|
11
|
+
|
|
12
|
+
if (jsonMode) {
|
|
13
|
+
outputResult(result, true);
|
|
14
|
+
} else {
|
|
15
|
+
const templates = result.data?.templates || result.templates || result.data || [];
|
|
16
|
+
if (templates.length === 0) {
|
|
17
|
+
console.log("No templates found. Create templates in your SESender dashboard.");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
formatTable(
|
|
21
|
+
["ID", "Name", "Type", "Subject/Preview", "Created"],
|
|
22
|
+
templates.map((t) => [
|
|
23
|
+
t.id,
|
|
24
|
+
(t.name || "Untitled").slice(0, 20),
|
|
25
|
+
t.type || "email",
|
|
26
|
+
(t.subject || t.body || "").slice(0, 25),
|
|
27
|
+
t.createdAt ? new Date(t.createdAt).toLocaleDateString() : "-",
|
|
28
|
+
])
|
|
29
|
+
);
|
|
30
|
+
console.log(`\n${templates.length} template(s) found`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function templatesGet(args, jsonMode) {
|
|
35
|
+
const id = args["--id"];
|
|
36
|
+
|
|
37
|
+
if (!id) {
|
|
38
|
+
console.error("Usage: ses-cli templates get --id TEMPLATE_ID");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = await apiRequest("GET", `/templates/${id}`);
|
|
43
|
+
|
|
44
|
+
if (jsonMode) {
|
|
45
|
+
outputResult(result, true);
|
|
46
|
+
} else {
|
|
47
|
+
const t = result.data || result;
|
|
48
|
+
console.log(`Template: ${t.name || "Untitled"}`);
|
|
49
|
+
console.log(` ID: ${t.id}`);
|
|
50
|
+
console.log(` Type: ${t.type || "email"}`);
|
|
51
|
+
if (t.subject) console.log(` Subject: ${t.subject}`);
|
|
52
|
+
if (t.body) {
|
|
53
|
+
console.log(` Body:`);
|
|
54
|
+
console.log(` ${t.body.slice(0, 200)}${t.body.length > 200 ? "..." : ""}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { apiRequest } from "../api.mjs";
|
|
2
|
+
import { formatTable, outputResult, success } from "../output.mjs";
|
|
3
|
+
|
|
4
|
+
export async function trackingDomainsList(args, jsonMode) {
|
|
5
|
+
const result = await apiRequest("GET", "/tracking-domains");
|
|
6
|
+
|
|
7
|
+
if (jsonMode) {
|
|
8
|
+
outputResult(result, true);
|
|
9
|
+
} else {
|
|
10
|
+
const domains = result.data?.domains || result.domains || result.data || [];
|
|
11
|
+
if (domains.length === 0) {
|
|
12
|
+
console.log("No tracking domains configured. Add them in your SESender dashboard.");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
formatTable(
|
|
16
|
+
["ID", "Domain", "Status", "SSL", "Clicks (24h)"],
|
|
17
|
+
domains.map((d) => [
|
|
18
|
+
d.id,
|
|
19
|
+
d.domain || d.name,
|
|
20
|
+
d.status || (d.isVerified ? "Verified" : "Pending"),
|
|
21
|
+
d.sslEnabled ? "Yes" : "No",
|
|
22
|
+
d.clicksToday || d.clicks24h || 0,
|
|
23
|
+
])
|
|
24
|
+
);
|
|
25
|
+
console.log(`\n${domains.length} tracking domain(s)`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function trackingStats(args, jsonMode) {
|
|
30
|
+
const domain = args["--domain"];
|
|
31
|
+
const period = args["--period"] || "7d";
|
|
32
|
+
|
|
33
|
+
const params = { period };
|
|
34
|
+
if (domain) params.domain = domain;
|
|
35
|
+
|
|
36
|
+
const result = await apiRequest("GET", "/tracking-domains/stats", null, params);
|
|
37
|
+
|
|
38
|
+
if (jsonMode) {
|
|
39
|
+
outputResult(result, true);
|
|
40
|
+
} else {
|
|
41
|
+
const data = result.data || result;
|
|
42
|
+
console.log(`\n Tracking Statistics (${period})`);
|
|
43
|
+
console.log(` ─────────────────────────`);
|
|
44
|
+
console.log(` Total Clicks: ${data.totalClicks || 0}`);
|
|
45
|
+
console.log(` Unique Clicks: ${data.uniqueClicks || 0}`);
|
|
46
|
+
console.log(` Total Opens: ${data.totalOpens || 0}`);
|
|
47
|
+
console.log(` Unique Opens: ${data.uniqueOpens || 0}`);
|
|
48
|
+
if (data.topLinks && data.topLinks.length > 0) {
|
|
49
|
+
console.log(`\n Top Links:`);
|
|
50
|
+
formatTable(
|
|
51
|
+
["URL", "Clicks", "Unique"],
|
|
52
|
+
data.topLinks.slice(0, 10).map(l => [
|
|
53
|
+
(l.url || l.originalUrl || "").slice(0, 35),
|
|
54
|
+
l.clicks || 0,
|
|
55
|
+
l.uniqueClicks || 0,
|
|
56
|
+
])
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
console.log("");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -1,12 +1,25 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
1
2
|
import { apiRequest } from "../api.mjs";
|
|
2
|
-
import { outputResult, success } from "../output.mjs";
|
|
3
|
+
import { formatTable, outputResult, success, info } from "../output.mjs";
|
|
3
4
|
|
|
4
5
|
export async function validatePhone(args, jsonMode) {
|
|
5
6
|
const phone = args["--phone"] || args._positional?.[0];
|
|
6
7
|
|
|
7
8
|
if (!phone) {
|
|
8
|
-
console.error(
|
|
9
|
-
|
|
9
|
+
console.error(`
|
|
10
|
+
Usage: ses-cli validate phone PHONE_NUMBER
|
|
11
|
+
ses-cli validate phone --phone PHONE_NUMBER
|
|
12
|
+
|
|
13
|
+
Checks:
|
|
14
|
+
- Format validity
|
|
15
|
+
- Country detection
|
|
16
|
+
- Carrier lookup
|
|
17
|
+
- Line type (mobile, landline, voip)
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
ses-cli validate phone +1234567890
|
|
21
|
+
ses-cli validate phone --phone "+44 7911 123456"
|
|
22
|
+
`);
|
|
10
23
|
process.exit(1);
|
|
11
24
|
}
|
|
12
25
|
|
|
@@ -15,12 +28,15 @@ export async function validatePhone(args, jsonMode) {
|
|
|
15
28
|
if (jsonMode) {
|
|
16
29
|
outputResult(result, true);
|
|
17
30
|
} else {
|
|
18
|
-
|
|
19
|
-
console.log(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
if (
|
|
31
|
+
const data = result.data || result;
|
|
32
|
+
console.log(`\n Phone Validation: ${phone}`);
|
|
33
|
+
console.log(` ─────────────────────────`);
|
|
34
|
+
console.log(` Valid: ${data.valid ? "Yes" : "No"}`);
|
|
35
|
+
if (data.country || data.countryCode) console.log(` Country: ${data.country || data.countryCode}`);
|
|
36
|
+
if (data.carrier) console.log(` Carrier: ${data.carrier}`);
|
|
37
|
+
if (data.type || data.lineType) console.log(` Type: ${data.type || data.lineType}`);
|
|
38
|
+
if (data.formatted) console.log(` Format: ${data.formatted}`);
|
|
39
|
+
console.log("");
|
|
24
40
|
}
|
|
25
41
|
}
|
|
26
42
|
|
|
@@ -28,8 +44,21 @@ export async function validateEmail(args, jsonMode) {
|
|
|
28
44
|
const email = args["--email"] || args._positional?.[0];
|
|
29
45
|
|
|
30
46
|
if (!email) {
|
|
31
|
-
console.error(
|
|
32
|
-
|
|
47
|
+
console.error(`
|
|
48
|
+
Usage: ses-cli validate email EMAIL_ADDRESS
|
|
49
|
+
ses-cli validate email --email EMAIL_ADDRESS
|
|
50
|
+
|
|
51
|
+
Checks:
|
|
52
|
+
- Syntax validity
|
|
53
|
+
- MX record existence
|
|
54
|
+
- Disposable email detection
|
|
55
|
+
- Role-based address detection
|
|
56
|
+
- Deliverability score
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
ses-cli validate email user@example.com
|
|
60
|
+
ses-cli validate email --email "test@gmail.com"
|
|
61
|
+
`);
|
|
33
62
|
process.exit(1);
|
|
34
63
|
}
|
|
35
64
|
|
|
@@ -38,10 +67,77 @@ export async function validateEmail(args, jsonMode) {
|
|
|
38
67
|
if (jsonMode) {
|
|
39
68
|
outputResult(result, true);
|
|
40
69
|
} else {
|
|
41
|
-
|
|
42
|
-
console.log(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
70
|
+
const data = result.data || result;
|
|
71
|
+
console.log(`\n Email Validation: ${email}`);
|
|
72
|
+
console.log(` ─────────────────────────`);
|
|
73
|
+
console.log(` Valid: ${data.valid ? "Yes" : "No"}`);
|
|
74
|
+
if (data.mx !== undefined || data.mxValid !== undefined) console.log(` MX Record: ${(data.mx || data.mxValid) ? "Yes" : "No"}`);
|
|
75
|
+
if (data.disposable !== undefined) console.log(` Disposable: ${data.disposable ? "Yes" : "No"}`);
|
|
76
|
+
if (data.roleBased !== undefined) console.log(` Role-based: ${data.roleBased ? "Yes" : "No"}`);
|
|
77
|
+
if (data.freeProvider !== undefined) console.log(` Free: ${data.freeProvider ? "Yes" : "No"}`);
|
|
78
|
+
if (data.score !== undefined) console.log(` Score: ${data.score}/100`);
|
|
79
|
+
if (data.reason) console.log(` Reason: ${data.reason}`);
|
|
80
|
+
if (data.suggestion) console.log(` Suggestion: ${data.suggestion}`);
|
|
81
|
+
console.log("");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function validateBulk(args, jsonMode) {
|
|
86
|
+
const file = args["--file"];
|
|
87
|
+
const type = args["--type"] || "email";
|
|
88
|
+
|
|
89
|
+
if (!file) {
|
|
90
|
+
console.error(`
|
|
91
|
+
Usage: ses-cli validate bulk --file FILE [--type email|phone]
|
|
92
|
+
|
|
93
|
+
Options:
|
|
94
|
+
--file CSV file with emails or phone numbers (one per line or first column)
|
|
95
|
+
--type Type of validation: "email" (default) or "phone"
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
ses-cli validate bulk --file emails.csv --type email
|
|
99
|
+
ses-cli validate bulk --file phones.csv --type phone
|
|
100
|
+
`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const csvContent = readFileSync(file, "utf-8");
|
|
105
|
+
const lines = csvContent.trim().split("\n");
|
|
106
|
+
const hasHeader = lines[0].toLowerCase().includes(type) || lines[0].toLowerCase().includes("@") === false;
|
|
107
|
+
const items = (hasHeader ? lines.slice(1) : lines)
|
|
108
|
+
.map(l => l.trim().split(",")[0].trim().replace(/"/g, ""))
|
|
109
|
+
.filter(Boolean);
|
|
110
|
+
|
|
111
|
+
if (items.length === 0) {
|
|
112
|
+
console.error("Error: No items found in file");
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (items.length > 500) {
|
|
117
|
+
console.error("Error: Maximum 500 items per batch. Split your file.");
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
info(`Validating ${items.length} ${type}(s)...`);
|
|
122
|
+
|
|
123
|
+
const result = await apiRequest("POST", `/validate/bulk`, { items, type });
|
|
124
|
+
|
|
125
|
+
if (jsonMode) {
|
|
126
|
+
outputResult(result, true);
|
|
127
|
+
} else {
|
|
128
|
+
const data = result.data || result;
|
|
129
|
+
const valid = data.valid || 0;
|
|
130
|
+
const invalid = data.invalid || 0;
|
|
131
|
+
const invalidItems = data.invalidItems || [];
|
|
132
|
+
|
|
133
|
+
success(`Validation complete: ${valid} valid, ${invalid} invalid out of ${items.length}`);
|
|
134
|
+
if (invalidItems.length > 0 && invalidItems.length <= 10) {
|
|
135
|
+
console.log(`\n Invalid ${type}s:`);
|
|
136
|
+
invalidItems.forEach(i => console.log(` - ${i}`));
|
|
137
|
+
} else if (invalidItems.length > 10) {
|
|
138
|
+
console.log(`\n First 10 invalid ${type}s:`);
|
|
139
|
+
invalidItems.slice(0, 10).forEach(i => console.log(` - ${i}`));
|
|
140
|
+
console.log(` ... and ${invalidItems.length - 10} more`);
|
|
141
|
+
}
|
|
46
142
|
}
|
|
47
143
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { apiRequest } from "../api.mjs";
|
|
2
|
+
import { formatTable, outputResult, success } from "../output.mjs";
|
|
3
|
+
|
|
4
|
+
export async function webhooksList(args, jsonMode) {
|
|
5
|
+
const result = await apiRequest("GET", "/webhooks");
|
|
6
|
+
|
|
7
|
+
if (jsonMode) {
|
|
8
|
+
outputResult(result, true);
|
|
9
|
+
} else {
|
|
10
|
+
const webhooks = result.data?.webhooks || result.webhooks || result.data || [];
|
|
11
|
+
if (webhooks.length === 0) {
|
|
12
|
+
console.log("No webhooks configured.");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
formatTable(
|
|
16
|
+
["ID", "URL", "Events", "Status"],
|
|
17
|
+
webhooks.map((w) => [
|
|
18
|
+
w.id,
|
|
19
|
+
(w.url || "").slice(0, 40),
|
|
20
|
+
(w.events || []).join(", ").slice(0, 30),
|
|
21
|
+
w.isActive ? "Active" : "Inactive",
|
|
22
|
+
])
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function webhooksCreate(args, jsonMode) {
|
|
28
|
+
const url = args["--url"];
|
|
29
|
+
const events = args["--events"];
|
|
30
|
+
|
|
31
|
+
if (!url || !events) {
|
|
32
|
+
console.error(`
|
|
33
|
+
Usage: ses-cli webhooks create --url URL --events EVENTS
|
|
34
|
+
|
|
35
|
+
Options:
|
|
36
|
+
--url Webhook endpoint URL (must be HTTPS)
|
|
37
|
+
--events Comma-separated list of events to subscribe to
|
|
38
|
+
|
|
39
|
+
Available Events:
|
|
40
|
+
email.sent, email.delivered, email.bounced, email.complained
|
|
41
|
+
sms.sent, sms.delivered, sms.failed
|
|
42
|
+
link.clicked, email.opened
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
ses-cli webhooks create --url https://myapp.com/webhook --events "email.delivered,email.bounced"
|
|
46
|
+
ses-cli webhooks create --url https://myapp.com/sms-hook --events "sms.sent,sms.delivered,sms.failed"
|
|
47
|
+
`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const payload = {
|
|
52
|
+
url,
|
|
53
|
+
events: events.split(",").map(e => e.trim()),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const result = await apiRequest("POST", "/webhooks", payload);
|
|
57
|
+
|
|
58
|
+
if (jsonMode) {
|
|
59
|
+
outputResult(result, true);
|
|
60
|
+
} else {
|
|
61
|
+
success(`Webhook created: ${url}`);
|
|
62
|
+
if (result.data?.id) console.log(` ID: ${result.data.id}`);
|
|
63
|
+
if (result.data?.secret) console.log(` Secret: ${result.data.secret}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function webhooksDelete(args, jsonMode) {
|
|
68
|
+
const id = args["--id"];
|
|
69
|
+
|
|
70
|
+
if (!id) {
|
|
71
|
+
console.error("Usage: ses-cli webhooks delete --id WEBHOOK_ID");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const result = await apiRequest("DELETE", `/webhooks/${id}`);
|
|
76
|
+
|
|
77
|
+
if (jsonMode) {
|
|
78
|
+
outputResult(result, true);
|
|
79
|
+
} else {
|
|
80
|
+
success(`Webhook ${id} deleted`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function webhooksTest(args, jsonMode) {
|
|
85
|
+
const id = args["--id"];
|
|
86
|
+
|
|
87
|
+
if (!id) {
|
|
88
|
+
console.error("Usage: ses-cli webhooks test --id WEBHOOK_ID");
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const result = await apiRequest("POST", `/webhooks/${id}/test`);
|
|
93
|
+
|
|
94
|
+
if (jsonMode) {
|
|
95
|
+
outputResult(result, true);
|
|
96
|
+
} else {
|
|
97
|
+
if (result.data?.success || result.success) {
|
|
98
|
+
success(`Test webhook sent to webhook ${id}`);
|
|
99
|
+
} else {
|
|
100
|
+
console.error(`Webhook test failed: ${result.data?.error || "Unknown error"}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|