@sesender/cli 1.0.0

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 ADDED
@@ -0,0 +1,146 @@
1
+ # @sesender/cli
2
+
3
+ The official SESender command-line tool. Send SMS/emails, manage contacts, view analytics, and control campaigns directly from your terminal.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @sesender/cli
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ # Configure your API key
15
+ ses-cli config set --api-key YOUR_API_KEY --base-url https://sesender.com
16
+
17
+ # Send an SMS
18
+ ses-cli sms send --to +1234567890 --body "Hello from SESender CLI!"
19
+
20
+ # Send an email
21
+ ses-cli email send --to user@example.com --subject "Hello" --body "Welcome!"
22
+
23
+ # View analytics
24
+ ses-cli analytics
25
+ ```
26
+
27
+ ## Configuration
28
+
29
+ The CLI stores configuration in `~/.sesenderrc`. You can also use environment variables:
30
+
31
+ ```bash
32
+ export SES_API_KEY=your_api_key_here
33
+ export SES_BASE_URL=https://sesender.com
34
+ ```
35
+
36
+ ### Get your API key
37
+
38
+ 1. Log in to your SESender dashboard
39
+ 2. Go to **Settings > API Keys**
40
+ 3. Create a new API key with the permissions you need
41
+
42
+ ## Commands
43
+
44
+ ### Config
45
+
46
+ ```bash
47
+ ses-cli config set --api-key KEY --base-url URL # Save configuration
48
+ ses-cli config show # Show current config
49
+ ses-cli config clear # Remove saved config
50
+ ```
51
+
52
+ ### SMS
53
+
54
+ ```bash
55
+ ses-cli sms send --to PHONE --body MESSAGE [--from SENDER_ID]
56
+ ses-cli sms bulk --file contacts.csv --body MESSAGE [--from SENDER_ID]
57
+ ses-cli sms history [--limit 20] [--page 1]
58
+ ```
59
+
60
+ ### Email
61
+
62
+ ```bash
63
+ ses-cli email send --to EMAIL --subject SUBJECT --body TEXT [--html file.html] [--from SENDER]
64
+ ses-cli email bulk --file contacts.csv --subject SUBJECT --body TEXT [--html file.html]
65
+ ses-cli email history [--limit 20] [--page 1]
66
+ ```
67
+
68
+ ### Contacts
69
+
70
+ ```bash
71
+ ses-cli contacts list [--limit 20] [--page 1] [--group GROUP_NAME]
72
+ ses-cli contacts import --file contacts.csv [--group GROUP_NAME]
73
+ ses-cli contacts groups
74
+ ```
75
+
76
+ ### Campaigns & Analytics
77
+
78
+ ```bash
79
+ ses-cli campaigns list [--limit 20]
80
+ ses-cli analytics [--json]
81
+ ```
82
+
83
+ ### Validation
84
+
85
+ ```bash
86
+ ses-cli validate phone --phone +1234567890
87
+ ses-cli validate email --email user@example.com
88
+ ```
89
+
90
+ ## Global Options
91
+
92
+ | Flag | Description |
93
+ |------|-------------|
94
+ | `--json` | Output in JSON format for scripting |
95
+ | `--help` | Show help message |
96
+ | `--version` | Show version number |
97
+
98
+ ## CSV Format
99
+
100
+ ### For SMS bulk:
101
+ ```csv
102
+ phone
103
+ +1234567890
104
+ +0987654321
105
+ ```
106
+
107
+ ### For Email bulk:
108
+ ```csv
109
+ email
110
+ user1@example.com
111
+ user2@example.com
112
+ ```
113
+
114
+ ### For Contact import:
115
+ ```csv
116
+ name,phone,email
117
+ John Doe,+1234567890,john@example.com
118
+ Jane Smith,+0987654321,jane@example.com
119
+ ```
120
+
121
+ ## Scripting Examples
122
+
123
+ ```bash
124
+ # Send SMS to a list and get JSON output
125
+ ses-cli sms bulk --file numbers.csv --body "Flash sale!" --json > result.json
126
+
127
+ # Check if a phone is valid in a script
128
+ if ses-cli validate phone +1234567890 --json | grep -q '"valid":true'; then
129
+ echo "Phone is valid"
130
+ fi
131
+
132
+ # Export analytics to file
133
+ ses-cli analytics --json > analytics_$(date +%Y%m%d).json
134
+ ```
135
+
136
+ ## Rate Limits
137
+
138
+ The CLI respects the same rate limits as your API key. Check your current limits:
139
+
140
+ ```bash
141
+ ses-cli analytics --json | grep rateLimit
142
+ ```
143
+
144
+ ## License
145
+
146
+ MIT
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { configSet, configShow, configClear } from "../src/commands/config.mjs";
4
+ import { smsSend, smsBulk, smsHistory } from "../src/commands/sms.mjs";
5
+ import { emailSend, emailBulk, emailHistory } from "../src/commands/email.mjs";
6
+ import { contactsList, contactsImport, contactsGroups } from "../src/commands/contacts.mjs";
7
+ import { campaignsList, analyticsOverview } from "../src/commands/campaigns.mjs";
8
+ import { validatePhone, validateEmail } from "../src/commands/validate.mjs";
9
+
10
+ const VERSION = "1.0.0";
11
+
12
+ function parseArgs(argv) {
13
+ const args = { _positional: [] };
14
+ let i = 0;
15
+ while (i < argv.length) {
16
+ if (argv[i].startsWith("--")) {
17
+ const key = argv[i];
18
+ if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
19
+ args[key] = argv[i + 1];
20
+ i += 2;
21
+ } else {
22
+ args[key] = true;
23
+ i++;
24
+ }
25
+ } else {
26
+ args._positional.push(argv[i]);
27
+ i++;
28
+ }
29
+ }
30
+ return args;
31
+ }
32
+
33
+ function showHelp() {
34
+ console.log(`
35
+ SESender CLI v${VERSION}
36
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
37
+
38
+ USAGE
39
+ ses-cli <command> <subcommand> [options]
40
+
41
+ COMMANDS
42
+ config set Configure API key and base URL
43
+ config show Show current configuration
44
+ config clear Remove saved configuration
45
+
46
+ sms send Send a single SMS
47
+ sms bulk Send bulk SMS from CSV file
48
+ sms history View SMS message history
49
+
50
+ email send Send a single email
51
+ email bulk Send bulk emails from CSV file
52
+ email history View email message history
53
+
54
+ contacts list List contacts
55
+ contacts import Import contacts from CSV
56
+ contacts groups List contact groups
57
+
58
+ campaigns list List campaigns with stats
59
+ analytics View analytics overview
60
+
61
+ validate phone Validate a phone number
62
+ validate email Validate an email address
63
+
64
+ GLOBAL OPTIONS
65
+ --json Output in JSON format (machine-readable)
66
+ --help Show this help message
67
+ --version Show version number
68
+
69
+ EXAMPLES
70
+ ses-cli config set --api-key sk_live_abc123 --base-url https://sesender.com
71
+ ses-cli sms send --to +1234567890 --body "Hello from CLI!"
72
+ ses-cli email send --to user@example.com --subject "Test" --body "Hello"
73
+ ses-cli sms bulk --file numbers.csv --body "Promo: 20% off!"
74
+ ses-cli contacts import --file contacts.csv --group "Newsletter"
75
+ ses-cli validate phone +1234567890
76
+ ses-cli analytics --json
77
+
78
+ ENVIRONMENT VARIABLES
79
+ SES_API_KEY API key (overrides config file)
80
+ SES_BASE_URL Base URL (overrides config file)
81
+
82
+ CONFIGURATION
83
+ Config file: ~/.sesenderrc
84
+ Create API keys at: Settings > API Keys in your SESender dashboard
85
+ `);
86
+ }
87
+
88
+ async function main() {
89
+ const rawArgs = process.argv.slice(2);
90
+
91
+ if (rawArgs.length === 0 || rawArgs.includes("--help") || rawArgs.includes("-h")) {
92
+ showHelp();
93
+ return;
94
+ }
95
+
96
+ if (rawArgs.includes("--version") || rawArgs.includes("-v")) {
97
+ console.log(`ses-cli v${VERSION}`);
98
+ return;
99
+ }
100
+
101
+ const command = rawArgs[0];
102
+ const subcommand = rawArgs[1] && !rawArgs[1].startsWith("--") ? rawArgs[1] : null;
103
+ const argsStart = subcommand ? 2 : 1;
104
+ const args = parseArgs(rawArgs.slice(argsStart));
105
+ const jsonMode = rawArgs.includes("--json");
106
+
107
+ try {
108
+ switch (command) {
109
+ case "config":
110
+ switch (subcommand) {
111
+ case "set": configSet(args); break;
112
+ case "show": configShow(); break;
113
+ case "clear": configClear(); break;
114
+ default:
115
+ console.error("Usage: ses-cli config <set|show|clear>");
116
+ process.exit(1);
117
+ }
118
+ break;
119
+
120
+ case "sms":
121
+ switch (subcommand) {
122
+ case "send": await smsSend(args, jsonMode); break;
123
+ case "bulk": await smsBulk(args, jsonMode); break;
124
+ case "history": await smsHistory(args, jsonMode); break;
125
+ default:
126
+ console.error("Usage: ses-cli sms <send|bulk|history>");
127
+ process.exit(1);
128
+ }
129
+ break;
130
+
131
+ case "email":
132
+ switch (subcommand) {
133
+ case "send": await emailSend(args, jsonMode); break;
134
+ case "bulk": await emailBulk(args, jsonMode); break;
135
+ case "history": await emailHistory(args, jsonMode); break;
136
+ default:
137
+ console.error("Usage: ses-cli email <send|bulk|history>");
138
+ process.exit(1);
139
+ }
140
+ break;
141
+
142
+ case "contacts":
143
+ switch (subcommand) {
144
+ case "list": await contactsList(args, jsonMode); break;
145
+ case "import": await contactsImport(args, jsonMode); break;
146
+ case "groups": await contactsGroups(args, jsonMode); break;
147
+ default:
148
+ console.error("Usage: ses-cli contacts <list|import|groups>");
149
+ process.exit(1);
150
+ }
151
+ break;
152
+
153
+ case "campaigns":
154
+ switch (subcommand) {
155
+ case "list": await campaignsList(args, jsonMode); break;
156
+ default:
157
+ console.error("Usage: ses-cli campaigns <list>");
158
+ process.exit(1);
159
+ }
160
+ break;
161
+
162
+ case "analytics":
163
+ await analyticsOverview(args, jsonMode);
164
+ break;
165
+
166
+ case "validate":
167
+ switch (subcommand) {
168
+ case "phone": await validatePhone(args, jsonMode); break;
169
+ case "email": await validateEmail(args, jsonMode); break;
170
+ default:
171
+ console.error("Usage: ses-cli validate <phone|email>");
172
+ process.exit(1);
173
+ }
174
+ break;
175
+
176
+ default:
177
+ console.error(`Unknown command: ${command}`);
178
+ console.error("Run 'ses-cli --help' for usage information.");
179
+ process.exit(1);
180
+ }
181
+ } catch (error) {
182
+ if (error.message !== "process.exit") {
183
+ console.error(`Unexpected error: ${error.message}`);
184
+ process.exit(1);
185
+ }
186
+ }
187
+ }
188
+
189
+ main();
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@sesender/cli",
3
+ "version": "1.0.0",
4
+ "description": "SESender CLI - Send SMS/emails, manage contacts, and view analytics from your terminal",
5
+ "type": "module",
6
+ "bin": {
7
+ "ses-cli": "./bin/ses-cli.mjs"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "README.md"
13
+ ],
14
+ "keywords": ["sesender", "sms", "email", "marketing", "cli", "bulk-sms", "bulk-email"],
15
+ "author": "SESender Team",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/sesender/cli"
20
+ },
21
+ "engines": {
22
+ "node": ">=18.0.0"
23
+ },
24
+ "dependencies": {}
25
+ }
package/src/api.mjs ADDED
@@ -0,0 +1,87 @@
1
+ import { readFileSync, writeFileSync, existsSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+
5
+ const CONFIG_PATH = join(homedir(), ".sesenderrc");
6
+
7
+ export function getConfig() {
8
+ if (!existsSync(CONFIG_PATH)) return {};
9
+ try {
10
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
11
+ } catch {
12
+ return {};
13
+ }
14
+ }
15
+
16
+ export function saveConfig(config) {
17
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
18
+ }
19
+
20
+ export function getApiKey() {
21
+ return process.env.SES_API_KEY || getConfig().apiKey;
22
+ }
23
+
24
+ export function getBaseUrl() {
25
+ return process.env.SES_BASE_URL || getConfig().baseUrl || "https://sesender.com";
26
+ }
27
+
28
+ export async function apiRequest(method, path, body = null, queryParams = null) {
29
+ const apiKey = getApiKey();
30
+ if (!apiKey) {
31
+ console.error("Error: No API key configured.");
32
+ console.error("Run: ses-cli config set --api-key YOUR_KEY");
33
+ console.error("Or set the SES_API_KEY environment variable.");
34
+ process.exit(1);
35
+ }
36
+
37
+ const baseUrl = getBaseUrl();
38
+ let url = `${baseUrl}/api/v1${path}`;
39
+
40
+ if (queryParams) {
41
+ const params = new URLSearchParams();
42
+ for (const [key, value] of Object.entries(queryParams)) {
43
+ if (value !== undefined && value !== null) {
44
+ params.append(key, String(value));
45
+ }
46
+ }
47
+ const qs = params.toString();
48
+ if (qs) url += `?${qs}`;
49
+ }
50
+
51
+ const options = {
52
+ method,
53
+ headers: {
54
+ "X-API-Key": apiKey,
55
+ "Content-Type": "application/json",
56
+ },
57
+ };
58
+
59
+ if (body && method !== "GET") {
60
+ options.body = JSON.stringify(body);
61
+ }
62
+
63
+ try {
64
+ const response = await fetch(url, options);
65
+ const data = await response.json();
66
+
67
+ if (!response.ok) {
68
+ if (data.error) {
69
+ console.error(`Error (${response.status}): ${data.error}`);
70
+ if (data.details) console.error("Details:", data.details);
71
+ } else {
72
+ console.error(`HTTP Error: ${response.status} ${response.statusText}`);
73
+ }
74
+ process.exit(1);
75
+ }
76
+
77
+ return data;
78
+ } catch (error) {
79
+ if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
80
+ console.error(`Error: Cannot connect to ${baseUrl}`);
81
+ console.error("Check your base URL with: ses-cli config show");
82
+ } else {
83
+ console.error(`Request failed: ${error.message}`);
84
+ }
85
+ process.exit(1);
86
+ }
87
+ }
@@ -0,0 +1,69 @@
1
+ import { apiRequest } from "../api.mjs";
2
+ import { formatTable, outputResult, success } from "../output.mjs";
3
+
4
+ export async function campaignsList(args, jsonMode) {
5
+ const limit = args["--limit"] || "20";
6
+ const result = await apiRequest("GET", "/analytics/campaigns", null, { limit });
7
+
8
+ if (jsonMode) {
9
+ outputResult(result, true);
10
+ } else {
11
+ const campaigns = result.campaigns || result.data || [];
12
+ if (campaigns.length === 0) {
13
+ console.log("No campaigns found.");
14
+ return;
15
+ }
16
+ formatTable(
17
+ ["ID", "Name", "Type", "Status", "Sent", "Opens", "Clicks"],
18
+ campaigns.map((c) => [
19
+ c.id || c.campaignId,
20
+ (c.name || c.campaignName || "").slice(0, 25),
21
+ c.type || "email",
22
+ c.status || "-",
23
+ c.sent || c.totalSent || 0,
24
+ c.opens || c.totalOpens || 0,
25
+ c.clicks || c.totalClicks || 0,
26
+ ])
27
+ );
28
+ console.log(`\nShowing ${campaigns.length} campaigns`);
29
+ }
30
+ }
31
+
32
+ export async function analyticsOverview(args, jsonMode) {
33
+ const result = await apiRequest("GET", "/analytics/overview");
34
+
35
+ if (jsonMode) {
36
+ outputResult(result, true);
37
+ } else {
38
+ console.log("═══════════════════════════════════════");
39
+ console.log(" SESender Analytics Overview");
40
+ console.log("═══════════════════════════════════════");
41
+ console.log("");
42
+
43
+ if (result.sms) {
44
+ console.log(" SMS:");
45
+ console.log(` Total Sent: ${result.sms.totalSent || 0}`);
46
+ console.log(` Delivered: ${result.sms.delivered || 0}`);
47
+ console.log(` Failed: ${result.sms.failed || 0}`);
48
+ console.log("");
49
+ }
50
+
51
+ if (result.email) {
52
+ console.log(" Email:");
53
+ console.log(` Total Sent: ${result.email.totalSent || 0}`);
54
+ console.log(` Opens: ${result.email.totalOpens || 0}`);
55
+ console.log(` Clicks: ${result.email.totalClicks || 0}`);
56
+ console.log(` Bounces: ${result.email.bounces || 0}`);
57
+ console.log("");
58
+ }
59
+
60
+ if (result.contacts) {
61
+ console.log(" Contacts:");
62
+ console.log(` Total: ${result.contacts.total || 0}`);
63
+ console.log(` Groups: ${result.contacts.groups || 0}`);
64
+ console.log("");
65
+ }
66
+
67
+ console.log("═══════════════════════════════════════");
68
+ }
69
+ }
@@ -0,0 +1,36 @@
1
+ import { getConfig, saveConfig } from "../api.mjs";
2
+ import { success, info } from "../output.mjs";
3
+
4
+ export function configSet(args) {
5
+ const config = getConfig();
6
+
7
+ if (args["--api-key"]) {
8
+ config.apiKey = args["--api-key"];
9
+ success("API key saved");
10
+ }
11
+ if (args["--base-url"]) {
12
+ config.baseUrl = args["--base-url"];
13
+ success(`Base URL set to: ${config.baseUrl}`);
14
+ }
15
+
16
+ saveConfig(config);
17
+ }
18
+
19
+ export function configShow() {
20
+ const config = getConfig();
21
+ if (!config.apiKey && !config.baseUrl) {
22
+ info("No configuration found. Run: ses-cli config set --api-key YOUR_KEY");
23
+ return;
24
+ }
25
+ console.log("Current configuration:");
26
+ if (config.apiKey) {
27
+ const masked = config.apiKey.slice(0, 8) + "..." + config.apiKey.slice(-4);
28
+ console.log(` API Key: ${masked}`);
29
+ }
30
+ console.log(` Base URL: ${config.baseUrl || "https://sesender.com (default)"}`);
31
+ }
32
+
33
+ export function configClear() {
34
+ saveConfig({});
35
+ success("Configuration cleared");
36
+ }
@@ -0,0 +1,96 @@
1
+ import { readFileSync } from "fs";
2
+ import { apiRequest } from "../api.mjs";
3
+ import { formatTable, outputResult, success } from "../output.mjs";
4
+
5
+ export async function contactsList(args, jsonMode) {
6
+ const limit = args["--limit"] || "20";
7
+ const page = args["--page"] || "1";
8
+ const group = args["--group"];
9
+
10
+ const params = { limit, page };
11
+ if (group) params.group = group;
12
+
13
+ const result = await apiRequest("GET", "/contacts", null, params);
14
+
15
+ if (jsonMode) {
16
+ outputResult(result, true);
17
+ } else {
18
+ const contacts = result.contacts || result.data || [];
19
+ if (contacts.length === 0) {
20
+ console.log("No contacts found.");
21
+ return;
22
+ }
23
+ formatTable(
24
+ ["Name", "Phone", "Email", "Groups"],
25
+ contacts.map((c) => [
26
+ c.name || c.firstName || "-",
27
+ c.phone || "-",
28
+ c.email || "-",
29
+ (c.groups || []).join(", ") || "-",
30
+ ])
31
+ );
32
+ console.log(`\nShowing ${contacts.length} contacts (page ${page})`);
33
+ if (result.total) console.log(`Total: ${result.total}`);
34
+ }
35
+ }
36
+
37
+ export async function contactsImport(args, jsonMode) {
38
+ const file = args["--file"];
39
+ const group = args["--group"];
40
+
41
+ if (!file) {
42
+ console.error("Usage: ses-cli contacts import --file contacts.csv [--group GROUP_NAME]");
43
+ console.error("\nCSV format: name,phone,email (first row as headers)");
44
+ process.exit(1);
45
+ }
46
+
47
+ const csvContent = readFileSync(file, "utf-8");
48
+ const lines = csvContent.trim().split("\n");
49
+
50
+ if (lines.length < 2) {
51
+ console.error("Error: CSV file must have a header row and at least one data row");
52
+ process.exit(1);
53
+ }
54
+
55
+ const headers = lines[0].split(",").map((h) => h.trim().toLowerCase());
56
+ const contacts = lines.slice(1).map((line) => {
57
+ const values = line.split(",").map((v) => v.trim());
58
+ const contact = {};
59
+ headers.forEach((h, i) => {
60
+ if (values[i]) contact[h] = values[i];
61
+ });
62
+ return contact;
63
+ });
64
+
65
+ const payload = { contacts };
66
+ if (group) payload.group = group;
67
+
68
+ const result = await apiRequest("POST", "/contacts/bulk", payload);
69
+
70
+ if (jsonMode) {
71
+ outputResult(result, true);
72
+ } else {
73
+ success(`Import complete: ${result.imported || result.created || contacts.length} contacts imported`);
74
+ if (result.duplicates) console.log(` Duplicates skipped: ${result.duplicates}`);
75
+ if (result.errors) console.log(` Errors: ${result.errors}`);
76
+ if (group) console.log(` Assigned to group: ${group}`);
77
+ }
78
+ }
79
+
80
+ export async function contactsGroups(args, jsonMode) {
81
+ const result = await apiRequest("GET", "/contacts/groups");
82
+
83
+ if (jsonMode) {
84
+ outputResult(result, true);
85
+ } else {
86
+ const groups = result.groups || result.data || [];
87
+ if (groups.length === 0) {
88
+ console.log("No contact groups found.");
89
+ return;
90
+ }
91
+ formatTable(
92
+ ["ID", "Name", "Contacts"],
93
+ groups.map((g) => [g.id, g.name, g.contactCount || g.count || "-"])
94
+ );
95
+ }
96
+ }
@@ -0,0 +1,117 @@
1
+ import { readFileSync } from "fs";
2
+ import { apiRequest } from "../api.mjs";
3
+ import { formatTable, outputResult, success } from "../output.mjs";
4
+
5
+ export async function emailSend(args, jsonMode) {
6
+ const to = args["--to"];
7
+ const subject = args["--subject"];
8
+ const body = args["--body"];
9
+ const html = args["--html"];
10
+ const from = args["--from"];
11
+
12
+ if (!to || !subject || (!body && !html)) {
13
+ console.error("Usage: ses-cli email send --to EMAIL --subject SUBJECT --body TEXT [--html FILE] [--from SENDER]");
14
+ process.exit(1);
15
+ }
16
+
17
+ const payload = { to, subject };
18
+ if (from) payload.from = from;
19
+
20
+ if (html) {
21
+ payload.html = readFileSync(html, "utf-8");
22
+ } else {
23
+ payload.body = body;
24
+ }
25
+
26
+ const result = await apiRequest("POST", "/email/send", payload);
27
+
28
+ if (jsonMode) {
29
+ outputResult(result, true);
30
+ } else {
31
+ success(`Email sent to ${to}`);
32
+ if (result.messageId) console.log(` Message ID: ${result.messageId}`);
33
+ }
34
+ }
35
+
36
+ export async function emailBulk(args, jsonMode) {
37
+ const file = args["--file"];
38
+ const subject = args["--subject"];
39
+ const body = args["--body"];
40
+ const html = args["--html"];
41
+ const from = args["--from"];
42
+
43
+ if (!file || !subject || (!body && !html)) {
44
+ console.error("Usage: ses-cli email bulk --file contacts.csv --subject SUBJECT --body TEXT [--html FILE] [--from SENDER]");
45
+ console.error("\nCSV format: email (one email per line, or column header 'email')");
46
+ process.exit(1);
47
+ }
48
+
49
+ const csvContent = readFileSync(file, "utf-8");
50
+ const lines = csvContent.trim().split("\n");
51
+ const hasHeader = lines[0].toLowerCase().includes("email");
52
+ const emails = (hasHeader ? lines.slice(1) : lines)
53
+ .map((l) => l.trim().split(",")[0].trim())
54
+ .filter(Boolean);
55
+
56
+ if (emails.length === 0) {
57
+ console.error("Error: No email addresses found in CSV file");
58
+ process.exit(1);
59
+ }
60
+
61
+ const htmlContent = html ? readFileSync(html, "utf-8") : null;
62
+ console.log(`Sending to ${emails.length} recipients...`);
63
+
64
+ let sent = 0;
65
+ let failed = 0;
66
+ const errors = [];
67
+
68
+ for (const email of emails) {
69
+ try {
70
+ const payload = { to: email, subject };
71
+ if (from) payload.from = from;
72
+ if (htmlContent) payload.html = htmlContent;
73
+ else payload.body = body;
74
+ await apiRequest("POST", "/email/send", payload);
75
+ sent++;
76
+ } catch {
77
+ failed++;
78
+ errors.push(email);
79
+ }
80
+ }
81
+
82
+ if (jsonMode) {
83
+ outputResult({ sent, failed, total: emails.length, errors }, true);
84
+ } else {
85
+ success(`Bulk email complete: ${sent} sent, ${failed} failed out of ${emails.length}`);
86
+ if (errors.length > 0) {
87
+ console.log(` Failed emails: ${errors.slice(0, 5).join(", ")}${errors.length > 5 ? "..." : ""}`);
88
+ }
89
+ }
90
+ }
91
+
92
+ export async function emailHistory(args, jsonMode) {
93
+ const limit = args["--limit"] || "20";
94
+ const page = args["--page"] || "1";
95
+
96
+ const result = await apiRequest("GET", "/email/history", null, { limit, page });
97
+
98
+ if (jsonMode) {
99
+ outputResult(result, true);
100
+ } else {
101
+ const messages = result.messages || result.data || [];
102
+ if (messages.length === 0) {
103
+ console.log("No email messages found.");
104
+ return;
105
+ }
106
+ formatTable(
107
+ ["To", "Subject", "Status", "Date"],
108
+ messages.map((m) => [
109
+ m.to || m.recipient,
110
+ (m.subject || "").slice(0, 30),
111
+ m.status || "sent",
112
+ new Date(m.createdAt || m.sentAt).toLocaleString(),
113
+ ])
114
+ );
115
+ console.log(`\nShowing ${messages.length} messages (page ${page})`);
116
+ }
117
+ }
@@ -0,0 +1,105 @@
1
+ import { readFileSync } from "fs";
2
+ import { apiRequest } from "../api.mjs";
3
+ import { formatTable, outputResult, success } from "../output.mjs";
4
+
5
+ export async function smsSend(args, jsonMode) {
6
+ const to = args["--to"];
7
+ const body = args["--body"];
8
+ const from = args["--from"];
9
+
10
+ if (!to || !body) {
11
+ console.error("Usage: ses-cli sms send --to PHONE --body MESSAGE [--from SENDER_ID]");
12
+ process.exit(1);
13
+ }
14
+
15
+ const payload = { to, body };
16
+ if (from) payload.from = from;
17
+
18
+ const result = await apiRequest("POST", "/sms/send", payload);
19
+
20
+ if (jsonMode) {
21
+ outputResult(result, true);
22
+ } else {
23
+ success(`SMS sent to ${to}`);
24
+ if (result.messageId) console.log(` Message ID: ${result.messageId}`);
25
+ if (result.cost) console.log(` Cost: $${result.cost}`);
26
+ }
27
+ }
28
+
29
+ export async function smsBulk(args, jsonMode) {
30
+ const file = args["--file"];
31
+ const body = args["--body"];
32
+ const from = args["--from"];
33
+
34
+ if (!file) {
35
+ console.error("Usage: ses-cli sms bulk --file contacts.csv --body MESSAGE [--from SENDER_ID]");
36
+ console.error("\nCSV format: phone (one number per line, or column header 'phone')");
37
+ process.exit(1);
38
+ }
39
+
40
+ const csvContent = readFileSync(file, "utf-8");
41
+ const lines = csvContent.trim().split("\n");
42
+ const hasHeader = lines[0].toLowerCase().includes("phone");
43
+ const numbers = (hasHeader ? lines.slice(1) : lines)
44
+ .map((l) => l.trim().split(",")[0].trim())
45
+ .filter(Boolean);
46
+
47
+ if (numbers.length === 0) {
48
+ console.error("Error: No phone numbers found in CSV file");
49
+ process.exit(1);
50
+ }
51
+
52
+ console.log(`Sending to ${numbers.length} recipients...`);
53
+
54
+ let sent = 0;
55
+ let failed = 0;
56
+ const errors = [];
57
+
58
+ for (const phone of numbers) {
59
+ try {
60
+ const payload = { to: phone, body };
61
+ if (from) payload.from = from;
62
+ await apiRequest("POST", "/sms/send", payload);
63
+ sent++;
64
+ } catch {
65
+ failed++;
66
+ errors.push(phone);
67
+ }
68
+ }
69
+
70
+ if (jsonMode) {
71
+ outputResult({ sent, failed, total: numbers.length, errors }, true);
72
+ } else {
73
+ success(`Bulk SMS complete: ${sent} sent, ${failed} failed out of ${numbers.length}`);
74
+ if (errors.length > 0) {
75
+ console.log(` Failed numbers: ${errors.slice(0, 5).join(", ")}${errors.length > 5 ? "..." : ""}`);
76
+ }
77
+ }
78
+ }
79
+
80
+ export async function smsHistory(args, jsonMode) {
81
+ const limit = args["--limit"] || "20";
82
+ const page = args["--page"] || "1";
83
+
84
+ const result = await apiRequest("GET", "/sms/history", null, { limit, page });
85
+
86
+ if (jsonMode) {
87
+ outputResult(result, true);
88
+ } else {
89
+ const messages = result.messages || result.data || [];
90
+ if (messages.length === 0) {
91
+ console.log("No SMS messages found.");
92
+ return;
93
+ }
94
+ formatTable(
95
+ ["To", "Status", "Date", "Body"],
96
+ messages.map((m) => [
97
+ m.to || m.recipient,
98
+ m.status || "sent",
99
+ new Date(m.createdAt || m.sentAt).toLocaleString(),
100
+ (m.body || m.message || "").slice(0, 40),
101
+ ])
102
+ );
103
+ console.log(`\nShowing ${messages.length} messages (page ${page})`);
104
+ }
105
+ }
@@ -0,0 +1,47 @@
1
+ import { apiRequest } from "../api.mjs";
2
+ import { outputResult, success } from "../output.mjs";
3
+
4
+ export async function validatePhone(args, jsonMode) {
5
+ const phone = args["--phone"] || args._positional?.[0];
6
+
7
+ if (!phone) {
8
+ console.error("Usage: ses-cli validate phone --phone PHONE_NUMBER");
9
+ console.error(" or: ses-cli validate phone +1234567890");
10
+ process.exit(1);
11
+ }
12
+
13
+ const result = await apiRequest("POST", "/validate/phone", { phone });
14
+
15
+ if (jsonMode) {
16
+ outputResult(result, true);
17
+ } else {
18
+ console.log(`Phone: ${phone}`);
19
+ console.log(` Valid: ${result.valid ? "✓ Yes" : "✗ No"}`);
20
+ if (result.carrier) console.log(` Carrier: ${result.carrier}`);
21
+ if (result.type) console.log(` Type: ${result.type}`);
22
+ if (result.country) console.log(` Country: ${result.country}`);
23
+ if (result.formatted) console.log(` Format: ${result.formatted}`);
24
+ }
25
+ }
26
+
27
+ export async function validateEmail(args, jsonMode) {
28
+ const email = args["--email"] || args._positional?.[0];
29
+
30
+ if (!email) {
31
+ console.error("Usage: ses-cli validate email --email ADDRESS");
32
+ console.error(" or: ses-cli validate email user@example.com");
33
+ process.exit(1);
34
+ }
35
+
36
+ const result = await apiRequest("POST", "/validate/email", { email });
37
+
38
+ if (jsonMode) {
39
+ outputResult(result, true);
40
+ } else {
41
+ console.log(`Email: ${email}`);
42
+ console.log(` Valid: ${result.valid ? "✓ Yes" : "✗ No"}`);
43
+ if (result.disposable !== undefined) console.log(` Disposable: ${result.disposable ? "⚠ Yes" : "No"}`);
44
+ if (result.mx) console.log(` MX Record: ${result.mx}`);
45
+ if (result.reason) console.log(` Reason: ${result.reason}`);
46
+ }
47
+ }
package/src/output.mjs ADDED
@@ -0,0 +1,38 @@
1
+ export function formatTable(headers, rows) {
2
+ const colWidths = headers.map((h, i) => {
3
+ const maxRow = rows.reduce((max, row) => Math.max(max, String(row[i] ?? "").length), 0);
4
+ return Math.max(h.length, maxRow);
5
+ });
6
+
7
+ const separator = colWidths.map((w) => "─".repeat(w + 2)).join("┼");
8
+ const headerLine = headers.map((h, i) => ` ${h.padEnd(colWidths[i])} `).join("│");
9
+ const dataLines = rows.map((row) =>
10
+ row.map((cell, i) => ` ${String(cell ?? "").padEnd(colWidths[i])} `).join("│")
11
+ );
12
+
13
+ console.log(headerLine);
14
+ console.log(separator);
15
+ dataLines.forEach((line) => console.log(line));
16
+ }
17
+
18
+ export function outputResult(data, jsonMode) {
19
+ if (jsonMode) {
20
+ console.log(JSON.stringify(data, null, 2));
21
+ } else if (typeof data === "string") {
22
+ console.log(data);
23
+ } else {
24
+ console.log(JSON.stringify(data, null, 2));
25
+ }
26
+ }
27
+
28
+ export function success(message) {
29
+ console.log(`✓ ${message}`);
30
+ }
31
+
32
+ export function info(message) {
33
+ console.log(`ℹ ${message}`);
34
+ }
35
+
36
+ export function warn(message) {
37
+ console.log(`⚠ ${message}`);
38
+ }