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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/cli.js +115 -16
  3. 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.2";
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 normalizeBaseUrl(value) {
121
- return (value ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
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
- return normalizeBaseUrl(getString(parsed.options, "base-url") ?? env["SHIPMAIL_BASE_URL"]);
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 (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
192
- throw new CliUsageError("--definition must be a JSON automation definition object.");
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 (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
560
- throw new CliUsageError("--metadata must be a JSON object or @path/to/file.json.");
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
- if (!generatePassword && !password) {
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
- ...generatePassword ? { generate_password: true } : { password },
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
- if (destinationMailboxIds.length !== 1) {
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: destinationMailboxIds[0],
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
- if (!mailboxId && !clientReference) {
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.2",
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.6"
44
+ "shipmail": "0.5.10"
45
45
  },
46
46
  "devDependencies": {
47
47
  "tsup": "^8.4.0",