shipmail-cli 0.5.2 → 0.5.4
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 +2 -0
- package/dist/cli.js +115 -16
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -89,6 +89,8 @@ shipmail inbox thread 550e8400-e29b-41d4-a716-446655440000 thr_123
|
|
|
89
89
|
shipmail inbox draft 550e8400-e29b-41d4-a716-446655440000 thr_123 --expected-version 7 --text "Thanks for the note"
|
|
90
90
|
# Apply your approval policy; stale drafts fail with 409 without delivery.
|
|
91
91
|
shipmail inbox send-draft 550e8400-e29b-41d4-a716-446655440000 thr_123 drf_123
|
|
92
|
+
# Save a new message to Drafts for a human to review and send; this never sends
|
|
93
|
+
shipmail inbox compose-draft 550e8400-e29b-41d4-a716-446655440000 --to user@example.com --subject "Kickoff" --text "Draft for review"
|
|
92
94
|
shipmail inbox thread-attention 550e8400-e29b-41d4-a716-446655440000 thr_123 --done true --expected-version 8
|
|
93
95
|
shipmail inbox reply-message 550e8400-e29b-41d4-a716-446655440000 eml_123 --to user@example.com --text "Thanks"
|
|
94
96
|
shipmail inbox reply-thread 550e8400-e29b-41d4-a716-446655440000 thr_123 --to user@example.com --text "Following up"
|
package/dist/cli.js
CHANGED
|
@@ -15,10 +15,28 @@ import {
|
|
|
15
15
|
} from "shipmail";
|
|
16
16
|
|
|
17
17
|
// src/version.ts
|
|
18
|
-
var VERSION = "0.5.
|
|
18
|
+
var VERSION = "0.5.4";
|
|
19
19
|
|
|
20
20
|
// src/cli.ts
|
|
21
21
|
var DEFAULT_BASE_URL = "https://shipmail.to/api/v1";
|
|
22
|
+
var ALLOWED_BASE_URL_HOSTS = ["shipmail.to"];
|
|
23
|
+
var RAW_REQUEST_TIMEOUT_MS = 3e4;
|
|
24
|
+
var NEWSLETTER_BLOCK_TYPES = [
|
|
25
|
+
"heading",
|
|
26
|
+
"paragraph",
|
|
27
|
+
"list",
|
|
28
|
+
"callout",
|
|
29
|
+
"button",
|
|
30
|
+
"image",
|
|
31
|
+
"video",
|
|
32
|
+
"columns",
|
|
33
|
+
"link_list",
|
|
34
|
+
"quote",
|
|
35
|
+
"divider",
|
|
36
|
+
"spacer",
|
|
37
|
+
"code",
|
|
38
|
+
"custom_html"
|
|
39
|
+
];
|
|
22
40
|
var WEBHOOK_EVENT_TYPE_SET = new Set(WEBHOOK_EVENT_TYPES);
|
|
23
41
|
var CliUsageError = class extends Error {
|
|
24
42
|
constructor(message) {
|
|
@@ -39,6 +57,7 @@ function parseCliArgs(argv) {
|
|
|
39
57
|
const options = {};
|
|
40
58
|
for (let i = 0; i < argv.length; i += 1) {
|
|
41
59
|
const token = argv[i];
|
|
60
|
+
if (token === void 0) break;
|
|
42
61
|
if (token === "--") {
|
|
43
62
|
positional.push(...argv.slice(i + 1));
|
|
44
63
|
break;
|
|
@@ -117,14 +136,35 @@ function getStringList(options, key) {
|
|
|
117
136
|
if (isStringArray(value)) return value.flatMap(splitValues);
|
|
118
137
|
return splitValues(value);
|
|
119
138
|
}
|
|
120
|
-
function
|
|
121
|
-
|
|
139
|
+
function validateBaseUrl(rawValue, allowInsecure) {
|
|
140
|
+
let parsed;
|
|
141
|
+
try {
|
|
142
|
+
parsed = new URL(rawValue);
|
|
143
|
+
} catch {
|
|
144
|
+
throw new CliUsageError(`Base URL is not a valid URL: ${rawValue}`);
|
|
145
|
+
}
|
|
146
|
+
const normalized = parsed.toString().replace(/\/+$/, "");
|
|
147
|
+
if (allowInsecure) return normalized;
|
|
148
|
+
if (parsed.protocol !== "https:") {
|
|
149
|
+
throw new CliUsageError(
|
|
150
|
+
"Base URL must use https. Set SHIPMAIL_ALLOW_INSECURE_BASE_URL=1 for development."
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const host = parsed.hostname.toLowerCase();
|
|
154
|
+
if (!ALLOWED_BASE_URL_HOSTS.some((allowed) => host === allowed || host.endsWith(`.${allowed}`))) {
|
|
155
|
+
throw new CliUsageError(
|
|
156
|
+
`Base URL host "${host}" is not allowed. Set SHIPMAIL_ALLOW_INSECURE_BASE_URL=1 for development.`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return normalized;
|
|
122
160
|
}
|
|
123
161
|
function apiKey(parsed, env) {
|
|
124
162
|
return getString(parsed.options, "api-key") ?? env["SHIPMAIL_API_KEY"];
|
|
125
163
|
}
|
|
126
164
|
function baseUrl(parsed, env) {
|
|
127
|
-
|
|
165
|
+
const raw = getString(parsed.options, "base-url") ?? env["SHIPMAIL_BASE_URL"];
|
|
166
|
+
if (!raw) return DEFAULT_BASE_URL;
|
|
167
|
+
return validateBaseUrl(raw, env["SHIPMAIL_ALLOW_INSECURE_BASE_URL"] === "1");
|
|
128
168
|
}
|
|
129
169
|
function organizationId(parsed, env) {
|
|
130
170
|
return getString(parsed.options, "organization-id") ?? env["SHIPMAIL_ORGANIZATION_ID"];
|
|
@@ -164,6 +204,23 @@ async function readTextInput(value) {
|
|
|
164
204
|
if (value.startsWith("@")) return readFile(value.slice(1), "utf8");
|
|
165
205
|
return value;
|
|
166
206
|
}
|
|
207
|
+
function isJsonObject(value) {
|
|
208
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
209
|
+
}
|
|
210
|
+
function isNewsletterBlock(value) {
|
|
211
|
+
return isJsonObject(value) && typeof value["type"] === "string" && NEWSLETTER_BLOCK_TYPES.includes(value["type"]);
|
|
212
|
+
}
|
|
213
|
+
function isAutomationDefinition(value) {
|
|
214
|
+
if (!isJsonObject(value) || !isJsonObject(value["trigger"]) || !isJsonObject(value["scope"])) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
return typeof value["trigger"]["type"] === "string" && Array.isArray(value["conditions"]) && Array.isArray(value["actions"]) && (value["mode"] === "draft" || value["mode"] === "send") && Array.isArray(value["scope"]["mailbox_ids"]) && Array.isArray(value["scope"]["calendar_addresses"]);
|
|
218
|
+
}
|
|
219
|
+
function isMessageMetadata(value) {
|
|
220
|
+
return isJsonObject(value) && Object.values(value).every(
|
|
221
|
+
(item) => item === null || typeof item === "string" || typeof item === "number" || typeof item === "boolean"
|
|
222
|
+
);
|
|
223
|
+
}
|
|
167
224
|
async function readNewsletterBlocks(value) {
|
|
168
225
|
const parsed = await readJsonInput(value);
|
|
169
226
|
if (!Array.isArray(parsed)) {
|
|
@@ -172,6 +229,11 @@ async function readNewsletterBlocks(value) {
|
|
|
172
229
|
if (parsed.length === 0) {
|
|
173
230
|
throw new CliUsageError("--blocks must include at least one block.");
|
|
174
231
|
}
|
|
232
|
+
if (!parsed.every(isNewsletterBlock)) {
|
|
233
|
+
throw new CliUsageError(
|
|
234
|
+
`Every --blocks item must be an object whose type is one of: ${NEWSLETTER_BLOCK_TYPES.join(", ")}.`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
175
237
|
return parsed;
|
|
176
238
|
}
|
|
177
239
|
function parseArchiveVisibility(options) {
|
|
@@ -188,8 +250,10 @@ function parseStylingMode(options) {
|
|
|
188
250
|
}
|
|
189
251
|
async function automationDefinition(value) {
|
|
190
252
|
const parsed = await readJsonInput(value);
|
|
191
|
-
if (
|
|
192
|
-
throw new CliUsageError(
|
|
253
|
+
if (!isAutomationDefinition(parsed)) {
|
|
254
|
+
throw new CliUsageError(
|
|
255
|
+
"--definition must be a JSON object with trigger, conditions, actions, mode (draft or send) and scope { mailbox_ids, calendar_addresses }."
|
|
256
|
+
);
|
|
193
257
|
}
|
|
194
258
|
return parsed;
|
|
195
259
|
}
|
|
@@ -306,7 +370,8 @@ async function rawApiRequest(parsed, runOptions, method, path, body, authRequire
|
|
|
306
370
|
const response = await (runOptions.fetch ?? fetch)(`${baseUrl(parsed, env)}${path}`, {
|
|
307
371
|
method,
|
|
308
372
|
headers,
|
|
309
|
-
body: requestBody
|
|
373
|
+
body: requestBody,
|
|
374
|
+
signal: AbortSignal.timeout(RAW_REQUEST_TIMEOUT_MS)
|
|
310
375
|
});
|
|
311
376
|
if (!response.ok) {
|
|
312
377
|
const contentType = response.headers.get("content-type") ?? "";
|
|
@@ -385,6 +450,7 @@ Usage:
|
|
|
385
450
|
shipmail inbox thread <mailbox-id> <thread-id>
|
|
386
451
|
shipmail inbox thread-attention <mailbox-id> <thread-id> --done true --expected-version 3
|
|
387
452
|
shipmail inbox draft <mailbox-id> <thread-id> --expected-version 3 (--text "Hello" | --html "<p>Hello</p>") [--reply-mode reply_all]
|
|
453
|
+
shipmail inbox compose-draft <mailbox-id> --to user@example.com [--cc ...] [--bcc ...] [--subject "Hi"] (--text "Hello" | --html "<p>Hello</p>") [--idempotency-key key] (saves a new message to Drafts; never sends)
|
|
388
454
|
shipmail inbox send-draft <mailbox-id> <thread-id> <draft-id>
|
|
389
455
|
shipmail inbox reply-message <mailbox-id> <message-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false]
|
|
390
456
|
shipmail inbox reply-thread <mailbox-id> <thread-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false]
|
|
@@ -491,7 +557,9 @@ Global options:
|
|
|
491
557
|
|
|
492
558
|
Environment:
|
|
493
559
|
SHIPMAIL_API_KEY API key for authenticated commands.
|
|
494
|
-
SHIPMAIL_BASE_URL Optional API base URL.
|
|
560
|
+
SHIPMAIL_BASE_URL Optional API base URL. Must be https on shipmail.to.
|
|
561
|
+
SHIPMAIL_ALLOW_INSECURE_BASE_URL=1
|
|
562
|
+
Permit a non-https or non-shipmail.to base URL (development only).
|
|
495
563
|
SHIPMAIL_ORGANIZATION_ID Optional delegated child organization ID.
|
|
496
564
|
`;
|
|
497
565
|
}
|
|
@@ -556,8 +624,10 @@ async function messageMetadata(options) {
|
|
|
556
624
|
const raw = getString(options, "metadata");
|
|
557
625
|
if (!raw) return void 0;
|
|
558
626
|
const parsed = await readJsonInput(raw);
|
|
559
|
-
if (!
|
|
560
|
-
throw new CliUsageError(
|
|
627
|
+
if (!isMessageMetadata(parsed)) {
|
|
628
|
+
throw new CliUsageError(
|
|
629
|
+
"--metadata must be a JSON object of string, number, boolean or null values, or @path/to/file.json."
|
|
630
|
+
);
|
|
561
631
|
}
|
|
562
632
|
return parsed;
|
|
563
633
|
}
|
|
@@ -716,7 +786,8 @@ async function runMailboxes(client, parsed, out) {
|
|
|
716
786
|
throw new CliUsageError("Missing --password, --password-file, or --generate-password.");
|
|
717
787
|
}
|
|
718
788
|
const password = passwordFile ? (await readFile(passwordFile, "utf8")).replace(/\r?\n$/, "") : passwordValue;
|
|
719
|
-
|
|
789
|
+
const credentials = generatePassword ? { generate_password: true } : password ? { password } : void 0;
|
|
790
|
+
if (!credentials) {
|
|
720
791
|
throw new CliUsageError("Mailbox password must not be empty.");
|
|
721
792
|
}
|
|
722
793
|
writeJson(
|
|
@@ -724,7 +795,7 @@ async function runMailboxes(client, parsed, out) {
|
|
|
724
795
|
await client.mailboxes.create({
|
|
725
796
|
domain_id: getRequiredString(parsed.options, "domain-id"),
|
|
726
797
|
address: getRequiredString(parsed.options, "address"),
|
|
727
|
-
...
|
|
798
|
+
...credentials,
|
|
728
799
|
...displayName ? { display_name: displayName } : {}
|
|
729
800
|
})
|
|
730
801
|
);
|
|
@@ -886,12 +957,13 @@ async function runMailboxes(client, parsed, out) {
|
|
|
886
957
|
}
|
|
887
958
|
params = { mode: "keep" };
|
|
888
959
|
} else if (mode === "fixed") {
|
|
889
|
-
|
|
960
|
+
const [destinationMailboxId] = destinationMailboxIds;
|
|
961
|
+
if (destinationMailboxIds.length !== 1 || destinationMailboxId === void 0) {
|
|
890
962
|
throw new CliUsageError("--mode fixed requires exactly one --destination-mailbox-id.");
|
|
891
963
|
}
|
|
892
964
|
params = {
|
|
893
965
|
mode: "fixed",
|
|
894
|
-
destination_mailbox_id:
|
|
966
|
+
destination_mailbox_id: destinationMailboxId,
|
|
895
967
|
fallback_mailbox_id: getRequiredString(parsed.options, "fallback-mailbox-id")
|
|
896
968
|
};
|
|
897
969
|
} else if (mode === "round-robin") {
|
|
@@ -1069,6 +1141,33 @@ async function runInbox(client, parsed, out) {
|
|
|
1069
1141
|
);
|
|
1070
1142
|
return;
|
|
1071
1143
|
}
|
|
1144
|
+
if (subcommand === "compose-draft") {
|
|
1145
|
+
const to = messageRecipients(parsed.options, "to");
|
|
1146
|
+
const cc = messageRecipients(parsed.options, "cc");
|
|
1147
|
+
const bcc = messageRecipients(parsed.options, "bcc");
|
|
1148
|
+
const subject = getString(parsed.options, "subject");
|
|
1149
|
+
const text = await readTextInput(getString(parsed.options, "text"));
|
|
1150
|
+
const html = await readTextInput(getString(parsed.options, "html"));
|
|
1151
|
+
const idempotencyKey = getString(parsed.options, "idempotency-key");
|
|
1152
|
+
if (to.length === 0) throw new CliUsageError("Provide at least one --to recipient.");
|
|
1153
|
+
if (!text && !html) throw new CliUsageError("Provide --text or --html.");
|
|
1154
|
+
writeJson(
|
|
1155
|
+
out,
|
|
1156
|
+
await client.mailboxes.createInboxDraft(
|
|
1157
|
+
requirePositional(parsed.positional, 2, "mailbox id"),
|
|
1158
|
+
{
|
|
1159
|
+
to,
|
|
1160
|
+
...cc.length > 0 ? { cc } : {},
|
|
1161
|
+
...bcc.length > 0 ? { bcc } : {},
|
|
1162
|
+
...subject !== void 0 ? { subject } : {},
|
|
1163
|
+
...text ? { text } : {},
|
|
1164
|
+
...html ? { html } : {}
|
|
1165
|
+
},
|
|
1166
|
+
idempotencyKey ? { idempotencyKey } : void 0
|
|
1167
|
+
)
|
|
1168
|
+
);
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1072
1171
|
if (subcommand === "send-draft") {
|
|
1073
1172
|
writeJson(
|
|
1074
1173
|
out,
|
|
@@ -1187,10 +1286,10 @@ async function runMessages(client, parsed, out) {
|
|
|
1187
1286
|
if (subcommand === "list") {
|
|
1188
1287
|
const mailboxId = getString(parsed.options, "mailbox-id");
|
|
1189
1288
|
const clientReference = getString(parsed.options, "client-reference");
|
|
1190
|
-
|
|
1289
|
+
const filter = mailboxId ? { mailbox_id: mailboxId, ...clientReference ? { client_reference: clientReference } : {} } : clientReference ? { client_reference: clientReference } : void 0;
|
|
1290
|
+
if (!filter) {
|
|
1191
1291
|
throw new CliUsageError("Provide --mailbox-id or --client-reference.");
|
|
1192
1292
|
}
|
|
1193
|
-
const filter = mailboxId ? { mailbox_id: mailboxId, ...clientReference ? { client_reference: clientReference } : {} } : { client_reference: clientReference };
|
|
1194
1293
|
writeJson(
|
|
1195
1294
|
out,
|
|
1196
1295
|
await client.messages.list({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shipmail-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Official CLI for the Shipmail API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"shipmail": "0.5.
|
|
44
|
+
"shipmail": "0.5.10"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"tsup": "^8.4.0",
|