@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
|
@@ -1,32 +1,36 @@
|
|
|
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 contactsList(args, jsonMode) {
|
|
6
6
|
const limit = args["--limit"] || "20";
|
|
7
7
|
const page = args["--page"] || "1";
|
|
8
8
|
const group = args["--group"];
|
|
9
|
+
const search = args["--search"];
|
|
9
10
|
|
|
10
11
|
const params = { limit, page };
|
|
11
12
|
if (group) params.group = group;
|
|
13
|
+
if (search) params.search = search;
|
|
12
14
|
|
|
13
15
|
const result = await apiRequest("GET", "/contacts", null, params);
|
|
14
16
|
|
|
15
17
|
if (jsonMode) {
|
|
16
18
|
outputResult(result, true);
|
|
17
19
|
} else {
|
|
18
|
-
const contacts = result.contacts || result.data || [];
|
|
20
|
+
const contacts = result.data?.contacts || result.contacts || result.data || [];
|
|
19
21
|
if (contacts.length === 0) {
|
|
20
22
|
console.log("No contacts found.");
|
|
21
23
|
return;
|
|
22
24
|
}
|
|
23
25
|
formatTable(
|
|
24
|
-
["Name", "Phone", "
|
|
26
|
+
["ID", "Name", "Email", "Phone", "Group", "Created"],
|
|
25
27
|
contacts.map((c) => [
|
|
26
|
-
c.
|
|
28
|
+
c.id,
|
|
29
|
+
(c.name || c.firstName || "-").slice(0, 15),
|
|
30
|
+
(c.email || "-").slice(0, 25),
|
|
27
31
|
c.phone || "-",
|
|
28
|
-
c.
|
|
29
|
-
|
|
32
|
+
(c.groups || []).join(", ").slice(0, 12) || c.group || "-",
|
|
33
|
+
c.createdAt ? new Date(c.createdAt).toLocaleDateString() : "-",
|
|
30
34
|
])
|
|
31
35
|
);
|
|
32
36
|
console.log(`\nShowing ${contacts.length} contacts (page ${page})`);
|
|
@@ -37,10 +41,25 @@ export async function contactsList(args, jsonMode) {
|
|
|
37
41
|
export async function contactsImport(args, jsonMode) {
|
|
38
42
|
const file = args["--file"];
|
|
39
43
|
const group = args["--group"];
|
|
44
|
+
const skipDuplicates = args["--skip-duplicates"] !== undefined;
|
|
40
45
|
|
|
41
46
|
if (!file) {
|
|
42
|
-
console.error(
|
|
43
|
-
|
|
47
|
+
console.error(`
|
|
48
|
+
Usage: ses-cli contacts import --file FILE [options]
|
|
49
|
+
|
|
50
|
+
Options:
|
|
51
|
+
--file CSV file to import (required)
|
|
52
|
+
--group Assign contacts to this group
|
|
53
|
+
--skip-duplicates Skip duplicate contacts instead of updating
|
|
54
|
+
|
|
55
|
+
CSV Format:
|
|
56
|
+
Headers should include: email, phone, firstName, lastName, custom1, custom2, etc.
|
|
57
|
+
Minimum: email OR phone column required.
|
|
58
|
+
|
|
59
|
+
Examples:
|
|
60
|
+
ses-cli contacts import --file contacts.csv
|
|
61
|
+
ses-cli contacts import --file leads.csv --group "Newsletter" --skip-duplicates
|
|
62
|
+
`);
|
|
44
63
|
process.exit(1);
|
|
45
64
|
}
|
|
46
65
|
|
|
@@ -52,28 +71,34 @@ export async function contactsImport(args, jsonMode) {
|
|
|
52
71
|
process.exit(1);
|
|
53
72
|
}
|
|
54
73
|
|
|
55
|
-
const headers = lines[0].split(",").map((h) => h.trim().toLowerCase());
|
|
74
|
+
const headers = lines[0].split(",").map((h) => h.trim().toLowerCase().replace(/"/g, ""));
|
|
56
75
|
const contacts = lines.slice(1).map((line) => {
|
|
57
|
-
const values = line.split(",").map((v) => v.trim());
|
|
76
|
+
const values = line.split(",").map((v) => v.trim().replace(/"/g, ""));
|
|
58
77
|
const contact = {};
|
|
59
78
|
headers.forEach((h, i) => {
|
|
60
79
|
if (values[i]) contact[h] = values[i];
|
|
61
80
|
});
|
|
62
81
|
return contact;
|
|
63
|
-
});
|
|
82
|
+
}).filter(c => c.email || c.phone);
|
|
83
|
+
|
|
84
|
+
info(`Importing ${contacts.length} contacts...`);
|
|
64
85
|
|
|
65
86
|
const payload = { contacts };
|
|
66
87
|
if (group) payload.group = group;
|
|
88
|
+
if (skipDuplicates) payload.skipDuplicates = true;
|
|
67
89
|
|
|
68
90
|
const result = await apiRequest("POST", "/contacts/bulk", payload);
|
|
69
91
|
|
|
70
92
|
if (jsonMode) {
|
|
71
93
|
outputResult(result, true);
|
|
72
94
|
} else {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (
|
|
95
|
+
const data = result.data || result;
|
|
96
|
+
success(`Import complete`);
|
|
97
|
+
console.log(` Imported: ${data.imported || data.created || contacts.length}`);
|
|
98
|
+
if (data.updated) console.log(` Updated: ${data.updated}`);
|
|
99
|
+
if (data.duplicates || data.skipped) console.log(` Skipped: ${data.duplicates || data.skipped}`);
|
|
100
|
+
if (data.errors) console.log(` Errors: ${data.errors}`);
|
|
101
|
+
if (group) console.log(` Group: ${group}`);
|
|
77
102
|
}
|
|
78
103
|
}
|
|
79
104
|
|
|
@@ -83,14 +108,67 @@ export async function contactsGroups(args, jsonMode) {
|
|
|
83
108
|
if (jsonMode) {
|
|
84
109
|
outputResult(result, true);
|
|
85
110
|
} else {
|
|
86
|
-
const groups = result.groups || result.data || [];
|
|
111
|
+
const groups = result.data?.groups || result.groups || result.data || [];
|
|
87
112
|
if (groups.length === 0) {
|
|
88
113
|
console.log("No contact groups found.");
|
|
89
114
|
return;
|
|
90
115
|
}
|
|
91
116
|
formatTable(
|
|
92
|
-
["ID", "Name", "Contacts"],
|
|
93
|
-
groups.map((g) => [
|
|
117
|
+
["ID", "Name", "Contacts", "Created"],
|
|
118
|
+
groups.map((g) => [
|
|
119
|
+
g.id,
|
|
120
|
+
g.name || "Untitled",
|
|
121
|
+
g.contactCount || g.count || 0,
|
|
122
|
+
g.createdAt ? new Date(g.createdAt).toLocaleDateString() : "-",
|
|
123
|
+
])
|
|
94
124
|
);
|
|
125
|
+
console.log(`\n${groups.length} group(s) found`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function contactsExport(args, jsonMode) {
|
|
130
|
+
const group = args["--group"];
|
|
131
|
+
const format = args["--format"] || "csv";
|
|
132
|
+
|
|
133
|
+
const params = { format };
|
|
134
|
+
if (group) params.group = group;
|
|
135
|
+
|
|
136
|
+
const result = await apiRequest("GET", "/contacts/export", null, params);
|
|
137
|
+
|
|
138
|
+
if (jsonMode) {
|
|
139
|
+
outputResult(result, true);
|
|
140
|
+
} else {
|
|
141
|
+
const data = result.data || result;
|
|
142
|
+
if (data.downloadUrl) {
|
|
143
|
+
success(`Export ready: ${data.downloadUrl}`);
|
|
144
|
+
} else if (data.csv || typeof data === "string") {
|
|
145
|
+
console.log(data.csv || data);
|
|
146
|
+
} else {
|
|
147
|
+
success("Export initiated. Check your dashboard for the download link.");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function contactsDelete(args, jsonMode) {
|
|
153
|
+
const id = args["--id"];
|
|
154
|
+
const email = args["--email"];
|
|
155
|
+
|
|
156
|
+
if (!id && !email) {
|
|
157
|
+
console.error(`
|
|
158
|
+
Usage: ses-cli contacts delete --id CONTACT_ID
|
|
159
|
+
ses-cli contacts delete --email EMAIL
|
|
160
|
+
|
|
161
|
+
Permanently deletes a contact. This cannot be undone.
|
|
162
|
+
`);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const path = id ? `/contacts/${id}` : `/contacts/by-email/${encodeURIComponent(email)}`;
|
|
167
|
+
const result = await apiRequest("DELETE", path);
|
|
168
|
+
|
|
169
|
+
if (jsonMode) {
|
|
170
|
+
outputResult(result, true);
|
|
171
|
+
} else {
|
|
172
|
+
success(`Contact ${id || email} deleted`);
|
|
95
173
|
}
|
|
96
174
|
}
|
package/src/commands/email.mjs
CHANGED
|
@@ -1,35 +1,64 @@
|
|
|
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 emailSend(args, jsonMode) {
|
|
6
6
|
const to = args["--to"];
|
|
7
7
|
const subject = args["--subject"];
|
|
8
8
|
const body = args["--body"];
|
|
9
|
-
const
|
|
9
|
+
const htmlFile = args["--html-file"] || args["--html"];
|
|
10
10
|
const from = args["--from"];
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const fromName = args["--from-name"];
|
|
12
|
+
const provider = args["--provider"];
|
|
13
|
+
const schedule = args["--schedule"];
|
|
14
|
+
const templateId = args["--template"];
|
|
15
|
+
|
|
16
|
+
if (!to || (!subject && !templateId)) {
|
|
17
|
+
console.error(`
|
|
18
|
+
Usage: ses-cli email send --to EMAIL --subject SUBJECT [options]
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--to Recipient email address
|
|
22
|
+
--subject Email subject line
|
|
23
|
+
--body Plain text body
|
|
24
|
+
--html-file Path to HTML file for email body
|
|
25
|
+
--from Sender email address
|
|
26
|
+
--from-name Sender display name
|
|
27
|
+
--provider Provider ID to use (see: ses-cli providers list)
|
|
28
|
+
--template Template ID to use (see: ses-cli templates list)
|
|
29
|
+
--schedule Schedule send time (ISO 8601)
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
ses-cli email send --to user@example.com --subject "Hello" --body "Hi there!"
|
|
33
|
+
ses-cli email send --to user@example.com --subject "Newsletter" --html-file ./email.html
|
|
34
|
+
ses-cli email send --to user@example.com --subject "Promo" --template 12 --provider 3
|
|
35
|
+
`);
|
|
14
36
|
process.exit(1);
|
|
15
37
|
}
|
|
16
38
|
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
if (html) {
|
|
21
|
-
payload.html = readFileSync(html, "utf-8");
|
|
22
|
-
} else {
|
|
23
|
-
payload.body = body;
|
|
39
|
+
let htmlBody = body || "";
|
|
40
|
+
if (htmlFile) {
|
|
41
|
+
htmlBody = readFileSync(htmlFile, "utf-8");
|
|
24
42
|
}
|
|
25
43
|
|
|
44
|
+
const payload = { to, subject };
|
|
45
|
+
if (htmlFile) payload.htmlBody = htmlBody;
|
|
46
|
+
else if (body) payload.body = body;
|
|
47
|
+
if (from) payload.fromEmail = from;
|
|
48
|
+
if (fromName) payload.fromName = fromName;
|
|
49
|
+
if (provider) payload.providerId = parseInt(provider);
|
|
50
|
+
if (schedule) payload.scheduledAt = schedule;
|
|
51
|
+
if (templateId) payload.templateId = parseInt(templateId);
|
|
52
|
+
|
|
26
53
|
const result = await apiRequest("POST", "/email/send", payload);
|
|
27
54
|
|
|
28
55
|
if (jsonMode) {
|
|
29
56
|
outputResult(result, true);
|
|
30
57
|
} else {
|
|
31
|
-
success(`Email sent to ${to}`);
|
|
32
|
-
if (result.messageId) console.log(` Message ID: ${result.messageId}`);
|
|
58
|
+
success(`Email ${schedule ? "scheduled" : "sent"} to ${to}`);
|
|
59
|
+
if (result.data?.messageId) console.log(` Message ID: ${result.data.messageId}`);
|
|
60
|
+
if (result.data?.provider) console.log(` Provider: ${result.data.provider}`);
|
|
61
|
+
if (schedule) console.log(` Scheduled: ${schedule}`);
|
|
33
62
|
}
|
|
34
63
|
}
|
|
35
64
|
|
|
@@ -37,42 +66,82 @@ export async function emailBulk(args, jsonMode) {
|
|
|
37
66
|
const file = args["--file"];
|
|
38
67
|
const subject = args["--subject"];
|
|
39
68
|
const body = args["--body"];
|
|
40
|
-
const
|
|
69
|
+
const htmlFile = args["--html-file"] || args["--html"];
|
|
41
70
|
const from = args["--from"];
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
71
|
+
const fromName = args["--from-name"];
|
|
72
|
+
const provider = args["--provider"];
|
|
73
|
+
const delay = parseInt(args["--delay"] || "0");
|
|
74
|
+
|
|
75
|
+
if (!file || !subject) {
|
|
76
|
+
console.error(`
|
|
77
|
+
Usage: ses-cli email bulk --file FILE --subject SUBJECT [options]
|
|
78
|
+
|
|
79
|
+
Options:
|
|
80
|
+
--file CSV file with email addresses (column: email or first column)
|
|
81
|
+
--subject Email subject. Use [[ColumnName]] for personalization
|
|
82
|
+
--body Plain text body. Use [[ColumnName]] for personalization
|
|
83
|
+
--html-file Path to HTML template file. Use [[ColumnName]] for personalization
|
|
84
|
+
--from Sender email address
|
|
85
|
+
--from-name Sender display name
|
|
86
|
+
--provider Provider ID to use
|
|
87
|
+
--delay Delay between sends in ms (default: 0)
|
|
88
|
+
|
|
89
|
+
Personalization:
|
|
90
|
+
Use [[First]], [[Last]], [[Email]], [[Custom1]], etc. in subject/body.
|
|
91
|
+
Column names are matched from your CSV headers.
|
|
92
|
+
|
|
93
|
+
Examples:
|
|
94
|
+
ses-cli email bulk --file contacts.csv --subject "Hi [[First]]!" --html-file ./promo.html
|
|
95
|
+
ses-cli email bulk --file vip.csv --subject "Exclusive" --body "Hey [[First]]" --provider 5
|
|
96
|
+
`);
|
|
46
97
|
process.exit(1);
|
|
47
98
|
}
|
|
48
99
|
|
|
100
|
+
let htmlBody = "";
|
|
101
|
+
if (htmlFile) {
|
|
102
|
+
htmlBody = readFileSync(htmlFile, "utf-8");
|
|
103
|
+
}
|
|
104
|
+
|
|
49
105
|
const csvContent = readFileSync(file, "utf-8");
|
|
50
106
|
const lines = csvContent.trim().split("\n");
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
.filter(Boolean);
|
|
107
|
+
const headers = lines[0].toLowerCase().split(",").map(h => h.trim().replace(/"/g, ""));
|
|
108
|
+
const emailIdx = headers.indexOf("email") !== -1 ? headers.indexOf("email") : 0;
|
|
109
|
+
const dataLines = lines.slice(1);
|
|
55
110
|
|
|
56
|
-
if (
|
|
57
|
-
console.error("Error: No
|
|
111
|
+
if (dataLines.length === 0) {
|
|
112
|
+
console.error("Error: No data rows found in CSV file");
|
|
58
113
|
process.exit(1);
|
|
59
114
|
}
|
|
60
115
|
|
|
61
|
-
|
|
62
|
-
console.log(`Sending to ${emails.length} recipients...`);
|
|
116
|
+
info(`Sending to ${dataLines.length} recipients...`);
|
|
63
117
|
|
|
64
118
|
let sent = 0;
|
|
65
119
|
let failed = 0;
|
|
66
120
|
const errors = [];
|
|
67
121
|
|
|
68
|
-
for (const
|
|
122
|
+
for (const line of dataLines) {
|
|
123
|
+
const cols = line.split(",").map(c => c.trim().replace(/"/g, ""));
|
|
124
|
+
const email = cols[emailIdx];
|
|
125
|
+
if (!email) continue;
|
|
126
|
+
|
|
127
|
+
let personalizedSubject = subject;
|
|
128
|
+
let personalizedBody = htmlBody || body || "";
|
|
129
|
+
headers.forEach((header, idx) => {
|
|
130
|
+
const regex = new RegExp(`\\[\\[${header}\\]\\]`, "gi");
|
|
131
|
+
personalizedSubject = personalizedSubject.replace(regex, cols[idx] || "");
|
|
132
|
+
personalizedBody = personalizedBody.replace(regex, cols[idx] || "");
|
|
133
|
+
});
|
|
134
|
+
|
|
69
135
|
try {
|
|
70
|
-
const payload = { to: email, subject };
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
136
|
+
const payload = { to: email, subject: personalizedSubject };
|
|
137
|
+
if (htmlFile) payload.htmlBody = personalizedBody;
|
|
138
|
+
else payload.body = personalizedBody;
|
|
139
|
+
if (from) payload.fromEmail = from;
|
|
140
|
+
if (fromName) payload.fromName = fromName;
|
|
141
|
+
if (provider) payload.providerId = parseInt(provider);
|
|
74
142
|
await apiRequest("POST", "/email/send", payload);
|
|
75
143
|
sent++;
|
|
144
|
+
if (delay > 0) await new Promise(r => setTimeout(r, delay));
|
|
76
145
|
} catch {
|
|
77
146
|
failed++;
|
|
78
147
|
errors.push(email);
|
|
@@ -80,11 +149,11 @@ export async function emailBulk(args, jsonMode) {
|
|
|
80
149
|
}
|
|
81
150
|
|
|
82
151
|
if (jsonMode) {
|
|
83
|
-
outputResult({ sent, failed, total:
|
|
152
|
+
outputResult({ sent, failed, total: dataLines.length, errors }, true);
|
|
84
153
|
} else {
|
|
85
|
-
success(`Bulk email complete: ${sent} sent, ${failed} failed out of ${
|
|
154
|
+
success(`Bulk email complete: ${sent} sent, ${failed} failed out of ${dataLines.length}`);
|
|
86
155
|
if (errors.length > 0) {
|
|
87
|
-
console.log(` Failed
|
|
156
|
+
console.log(` Failed: ${errors.slice(0, 5).join(", ")}${errors.length > 5 ? ` +${errors.length - 5} more` : ""}`);
|
|
88
157
|
}
|
|
89
158
|
}
|
|
90
159
|
}
|
|
@@ -92,23 +161,28 @@ export async function emailBulk(args, jsonMode) {
|
|
|
92
161
|
export async function emailHistory(args, jsonMode) {
|
|
93
162
|
const limit = args["--limit"] || "20";
|
|
94
163
|
const page = args["--page"] || "1";
|
|
164
|
+
const status = args["--status"];
|
|
165
|
+
|
|
166
|
+
const params = { limit, page };
|
|
167
|
+
if (status) params.status = status;
|
|
95
168
|
|
|
96
|
-
const result = await apiRequest("GET", "/email/
|
|
169
|
+
const result = await apiRequest("GET", "/email/messages", null, params);
|
|
97
170
|
|
|
98
171
|
if (jsonMode) {
|
|
99
172
|
outputResult(result, true);
|
|
100
173
|
} else {
|
|
101
|
-
const messages = result.messages || result.data || [];
|
|
174
|
+
const messages = result.data?.messages || result.messages || result.data || [];
|
|
102
175
|
if (messages.length === 0) {
|
|
103
176
|
console.log("No email messages found.");
|
|
104
177
|
return;
|
|
105
178
|
}
|
|
106
179
|
formatTable(
|
|
107
|
-
["To", "Subject", "Status", "Date"],
|
|
180
|
+
["To", "Subject", "Status", "Provider", "Date"],
|
|
108
181
|
messages.map((m) => [
|
|
109
182
|
m.to || m.recipient,
|
|
110
|
-
(m.subject || "").slice(0,
|
|
183
|
+
(m.subject || "").slice(0, 25),
|
|
111
184
|
m.status || "sent",
|
|
185
|
+
m.provider || "-",
|
|
112
186
|
new Date(m.createdAt || m.sentAt).toLocaleString(),
|
|
113
187
|
])
|
|
114
188
|
);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { apiRequest } from "../api.mjs";
|
|
2
|
+
import { formatTable, outputResult, success } from "../output.mjs";
|
|
3
|
+
|
|
4
|
+
export async function providersList(args, jsonMode) {
|
|
5
|
+
const type = args["--type"]; // sms, email, or all
|
|
6
|
+
|
|
7
|
+
const params = {};
|
|
8
|
+
if (type) params.type = type;
|
|
9
|
+
|
|
10
|
+
const result = await apiRequest("GET", "/providers", null, params);
|
|
11
|
+
|
|
12
|
+
if (jsonMode) {
|
|
13
|
+
outputResult(result, true);
|
|
14
|
+
} else {
|
|
15
|
+
const providers = result.data?.providers || result.providers || result.data || [];
|
|
16
|
+
if (providers.length === 0) {
|
|
17
|
+
console.log("No providers configured. Set up providers in your SESender dashboard.");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
formatTable(
|
|
21
|
+
["ID", "Name", "Type", "Provider", "Status"],
|
|
22
|
+
providers.map((p) => [
|
|
23
|
+
p.id,
|
|
24
|
+
p.name || p.providerType,
|
|
25
|
+
p.type || p.providerType,
|
|
26
|
+
p.providerType,
|
|
27
|
+
p.isActive ? "Active" : "Inactive",
|
|
28
|
+
])
|
|
29
|
+
);
|
|
30
|
+
console.log(`\n${providers.length} provider(s) found`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function providersBalance(args, jsonMode) {
|
|
35
|
+
const result = await apiRequest("GET", "/providers/balance");
|
|
36
|
+
|
|
37
|
+
if (jsonMode) {
|
|
38
|
+
outputResult(result, true);
|
|
39
|
+
} else {
|
|
40
|
+
const balances = result.data || result;
|
|
41
|
+
if (Array.isArray(balances)) {
|
|
42
|
+
formatTable(
|
|
43
|
+
["Provider", "Balance", "Currency"],
|
|
44
|
+
balances.map((b) => [b.provider || b.name, b.balance || "N/A", b.currency || "USD"])
|
|
45
|
+
);
|
|
46
|
+
} else {
|
|
47
|
+
console.log(JSON.stringify(balances, null, 2));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { apiRequest } from "../api.mjs";
|
|
2
|
+
import { formatTable, outputResult, success } from "../output.mjs";
|
|
3
|
+
|
|
4
|
+
export async function scheduleList(args, jsonMode) {
|
|
5
|
+
const status = args["--status"];
|
|
6
|
+
const params = {};
|
|
7
|
+
if (status) params.status = status;
|
|
8
|
+
|
|
9
|
+
const result = await apiRequest("GET", "/scheduled", null, params);
|
|
10
|
+
|
|
11
|
+
if (jsonMode) {
|
|
12
|
+
outputResult(result, true);
|
|
13
|
+
} else {
|
|
14
|
+
const items = result.data?.scheduled || result.scheduled || result.data || [];
|
|
15
|
+
if (items.length === 0) {
|
|
16
|
+
console.log("No scheduled messages found.");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
formatTable(
|
|
20
|
+
["ID", "Type", "To", "Scheduled For", "Status"],
|
|
21
|
+
items.map((s) => [
|
|
22
|
+
s.id,
|
|
23
|
+
s.type || "email",
|
|
24
|
+
(s.to || s.recipient || "").slice(0, 20),
|
|
25
|
+
s.scheduledAt ? new Date(s.scheduledAt).toLocaleString() : "-",
|
|
26
|
+
s.status || "pending",
|
|
27
|
+
])
|
|
28
|
+
);
|
|
29
|
+
console.log(`\n${items.length} scheduled message(s)`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function scheduleCancel(args, jsonMode) {
|
|
34
|
+
const id = args["--id"];
|
|
35
|
+
|
|
36
|
+
if (!id) {
|
|
37
|
+
console.error(`
|
|
38
|
+
Usage: ses-cli schedule cancel --id SCHEDULE_ID
|
|
39
|
+
|
|
40
|
+
Cancels a scheduled message before it is sent.
|
|
41
|
+
`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const result = await apiRequest("POST", `/scheduled/${id}/cancel`);
|
|
46
|
+
|
|
47
|
+
if (jsonMode) {
|
|
48
|
+
outputResult(result, true);
|
|
49
|
+
} else {
|
|
50
|
+
success(`Scheduled message ${id} cancelled`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { apiRequest } from "../api.mjs";
|
|
3
|
+
import { formatTable, outputResult, success, info } from "../output.mjs";
|
|
4
|
+
|
|
5
|
+
export async function seedCheck(args, jsonMode) {
|
|
6
|
+
const file = args["--file"];
|
|
7
|
+
const emails = args["--emails"];
|
|
8
|
+
const words = args["--words"] || args["--seed-words"];
|
|
9
|
+
|
|
10
|
+
if ((!file && !emails) || !words) {
|
|
11
|
+
console.error(`
|
|
12
|
+
Usage: ses-cli seed-check --emails EMAILS --words WORDS
|
|
13
|
+
ses-cli seed-check --file FILE --words WORDS
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--emails Comma-separated list of emails to check
|
|
17
|
+
--file CSV file with emails (column: email or first column)
|
|
18
|
+
--words Comma-separated seed words to check against
|
|
19
|
+
--seed-words Alias for --words
|
|
20
|
+
|
|
21
|
+
Description:
|
|
22
|
+
Checks emails against your seed word database to identify risky
|
|
23
|
+
addresses that may be spam traps or seed addresses.
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
ses-cli seed-check --emails "user@test.com,info@example.com" --words "trap,seed,monitor"
|
|
27
|
+
ses-cli seed-check --file contacts.csv --words "spamtrap,honeypot,seedlist"
|
|
28
|
+
`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let emailList = [];
|
|
33
|
+
if (file) {
|
|
34
|
+
const csvContent = readFileSync(file, "utf-8");
|
|
35
|
+
const lines = csvContent.trim().split("\n");
|
|
36
|
+
const headers = lines[0].toLowerCase().split(",").map(h => h.trim().replace(/"/g, ""));
|
|
37
|
+
const emailIdx = headers.indexOf("email") !== -1 ? headers.indexOf("email") : 0;
|
|
38
|
+
emailList = lines.slice(1)
|
|
39
|
+
.map(l => l.split(",")[emailIdx]?.trim().replace(/"/g, ""))
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
} else {
|
|
42
|
+
emailList = emails.split(",").map(e => e.trim()).filter(Boolean);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const seedWords = words.split(",").map(w => w.trim()).filter(Boolean);
|
|
46
|
+
|
|
47
|
+
if (emailList.length === 0) {
|
|
48
|
+
console.error("Error: No emails provided");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (emailList.length > 500) {
|
|
53
|
+
console.error("Error: Maximum 500 emails per request");
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
info(`Checking ${emailList.length} emails against ${seedWords.length} seed words...`);
|
|
58
|
+
|
|
59
|
+
const result = await apiRequest("POST", "/seed-check", {
|
|
60
|
+
emails: emailList,
|
|
61
|
+
seedWords: seedWords,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (jsonMode) {
|
|
65
|
+
outputResult(result, true);
|
|
66
|
+
} else {
|
|
67
|
+
const data = result.data || result;
|
|
68
|
+
const results = data.results || [];
|
|
69
|
+
const risky = results.filter(r => r.isRisky || r.riskScore > 50);
|
|
70
|
+
const safe = results.filter(r => !r.isRisky && (r.riskScore || 0) <= 50);
|
|
71
|
+
|
|
72
|
+
success(`Seed check complete: ${safe.length} safe, ${risky.length} risky`);
|
|
73
|
+
|
|
74
|
+
if (risky.length > 0) {
|
|
75
|
+
console.log("\n Risky emails:");
|
|
76
|
+
formatTable(
|
|
77
|
+
["Email", "Risk Score", "Source"],
|
|
78
|
+
risky.map(r => [r.email, `${r.riskScore || 100}%`, r.source || "database"])
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (data.cost) console.log(`\n Cost: €${data.cost.toFixed(2)}`);
|
|
83
|
+
if (data.balance) console.log(` Remaining balance: €${data.balance.toFixed(2)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function seedBalance(args, jsonMode) {
|
|
88
|
+
const result = await apiRequest("GET", "/seed-check/balance");
|
|
89
|
+
|
|
90
|
+
if (jsonMode) {
|
|
91
|
+
outputResult(result, true);
|
|
92
|
+
} else {
|
|
93
|
+
const data = result.data || result;
|
|
94
|
+
console.log(` Balance: €${(data.balance || 0).toFixed(2)}`);
|
|
95
|
+
console.log(` Rate: €${(data.rate || 0.01).toFixed(4)} per email`);
|
|
96
|
+
}
|
|
97
|
+
}
|