shipmail-cli 0.4.10 → 0.4.11

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 +7 -4
  2. package/dist/cli.js +105 -30
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -84,12 +84,12 @@ shipmail mailboxes sandbox-inbound mbx_123 --from customer@example.com --subject
84
84
  # Read inbound mail
85
85
  shipmail inbox list 550e8400-e29b-41d4-a716-446655440000 --folder-role inbox --after 2025-07-20T00:00:00Z --limit 25
86
86
  shipmail inbox message 550e8400-e29b-41d4-a716-446655440000 eml_123
87
- shipmail inbox threads 550e8400-e29b-41d4-a716-446655440000 --reply-state needs_reply --after 2025-07-20T00:00:00Z --limit 50
87
+ shipmail inbox threads 550e8400-e29b-41d4-a716-446655440000 --attention-state needs_reply --after 2025-07-20T00:00:00Z --limit 50
88
88
  shipmail inbox thread 550e8400-e29b-41d4-a716-446655440000 thr_123
89
- shipmail inbox draft 550e8400-e29b-41d4-a716-446655440000 thr_123 --expected-reply-version 7 --text "Thanks for the note"
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
- shipmail inbox thread-state 550e8400-e29b-41d4-a716-446655440000 thr_123 --reply-state resolved --expected-reply-version 8
92
+ shipmail inbox thread-attention 550e8400-e29b-41d4-a716-446655440000 thr_123 --done true --expected-version 8
93
93
  shipmail inbox reply-message 550e8400-e29b-41d4-a716-446655440000 eml_123 --to user@example.com --text "Thanks"
94
94
  shipmail inbox reply-thread 550e8400-e29b-41d4-a716-446655440000 thr_123 --to user@example.com --text "Following up"
95
95
 
@@ -114,6 +114,7 @@ shipmail newsletters preflight nws_123
114
114
  shipmail newsletters schedule nws_123 --scheduled-at 2026-08-01T09:00:00.000Z
115
115
  shipmail newsletter-assets list --kind image
116
116
  shipmail newsletter-assets upload ./hero.png --content-type image/png
117
+ shipmail newsletter-assets upload ./demo.mp4 --content-type video/mp4 --thumbnail ./demo-poster.jpg
117
118
  shipmail newsletter-assets register-url https://cdn.shipmail.to/newsletter-images/org_123/hero.png
118
119
 
119
120
  # Manage webhooks
@@ -136,7 +137,9 @@ Paragraph, quote, callout, list-item, and column bodies in `--blocks` accept bar
136
137
  sanitized inline HTML. Use `<p>` and `<br>` for line breaks. Shipmail keeps `a`, `b`, `br`,
137
138
  `code`, `em`, `i`, `p`, `s`, `span`, `strong`, and `u`, with safe attributes and URLs.
138
139
 
139
- `newsletter-assets list` includes current newsletter media storage usage in the JSON response.
140
+ `newsletter-assets upload` uses a five-minute direct storage PUT, then calls the single-use
141
+ completion URL. Video uploads require a JPEG poster through `--thumbnail`. `newsletter-assets list`
142
+ includes current newsletter media storage usage in the JSON response.
140
143
 
141
144
  ## Generic API Command
142
145
 
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  } from "shipmail";
15
15
 
16
16
  // src/version.ts
17
- var VERSION = "0.4.10";
17
+ var VERSION = "0.4.11";
18
18
 
19
19
  // src/cli.ts
20
20
  var DEFAULT_BASE_URL = "https://shipmail.to/api/v1";
@@ -374,10 +374,10 @@ Usage:
374
374
  shipmail mailboxes sandbox-inbound <id> --from sender@example.com --subject "Test" (--text "Hello" | --html "<p>Hello</p>") [--to support@example.com]
375
375
  shipmail inbox list <mailbox-id> [--folder-role inbox] [--search text] [--after ISO] [--before ISO] [--limit 25] [--cursor cur]
376
376
  shipmail inbox message <mailbox-id> <message-id>
377
- shipmail inbox threads <mailbox-id> [--reply-state needs_reply] [--after ISO] [--before ISO] [--limit 25] [--cursor cur]
377
+ shipmail inbox threads <mailbox-id> [--attention-state needs_reply] [--after ISO] [--before ISO] [--limit 25] [--cursor cur]
378
378
  shipmail inbox thread <mailbox-id> <thread-id>
379
- shipmail inbox thread-state <mailbox-id> <thread-id> --reply-state resolved --expected-reply-version 3
380
- shipmail inbox draft <mailbox-id> <thread-id> --expected-reply-version 3 (--text "Hello" | --html "<p>Hello</p>") [--reply-mode reply_all]
379
+ shipmail inbox thread-attention <mailbox-id> <thread-id> --done true --expected-version 3
380
+ shipmail inbox draft <mailbox-id> <thread-id> --expected-version 3 (--text "Hello" | --html "<p>Hello</p>") [--reply-mode reply_all]
381
381
  shipmail inbox send-draft <mailbox-id> <thread-id> <draft-id>
382
382
  shipmail inbox reply-message <mailbox-id> <message-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>")
383
383
  shipmail inbox reply-thread <mailbox-id> <thread-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>")
@@ -415,7 +415,7 @@ Usage:
415
415
  shipmail newsletters cancel <id>
416
416
  shipmail newsletters resume <id>
417
417
  shipmail newsletter-assets list [--kind image|video] [--q text] [--limit 25] [--cursor cur]
418
- shipmail newsletter-assets upload <file> [--filename name] [--content-type type]
418
+ shipmail newsletter-assets upload <file> [--filename name] [--content-type type] [--thumbnail poster.jpg]
419
419
  shipmail newsletter-assets register-url <url> [--filename name] [--thumbnail-url url]
420
420
  shipmail newsletter-sender-identities list [--limit 25] [--cursor cur]
421
421
  shipmail newsletter-domains list [--limit 25] [--cursor cur]
@@ -912,7 +912,7 @@ async function runInbox(client, parsed, out) {
912
912
  return;
913
913
  }
914
914
  if (subcommand === "threads") {
915
- const replyState = getString(parsed.options, "reply-state");
915
+ const attentionState = getString(parsed.options, "attention-state");
916
916
  const sortBy = getString(parsed.options, "sort-by");
917
917
  const order = getString(parsed.options, "order");
918
918
  writeJson(
@@ -920,8 +920,8 @@ async function runInbox(client, parsed, out) {
920
920
  await client.mailboxes.listInboxThreads(
921
921
  requirePositional(parsed.positional, 2, "mailbox id"),
922
922
  {
923
- ...replyState ? {
924
- reply_state: replyState
923
+ ...attentionState ? {
924
+ attention_state: attentionState
925
925
  } : {},
926
926
  ...sortBy ? { sort_by: sortBy } : {},
927
927
  ...order ? { order } : {},
@@ -934,29 +934,31 @@ async function runInbox(client, parsed, out) {
934
934
  );
935
935
  return;
936
936
  }
937
- if (subcommand === "thread-state") {
938
- const replyState = getString(parsed.options, "reply-state");
939
- const expectedReplyVersion = getNumber(parsed.options, "expected-reply-version");
940
- if (!replyState || expectedReplyVersion === void 0) {
941
- throw new CliUsageError("Provide --reply-state and --expected-reply-version.");
937
+ if (subcommand === "thread-attention") {
938
+ const done = getBoolean(parsed.options, "done");
939
+ const followUpAt = getString(parsed.options, "follow-up-at");
940
+ const expectedVersion = getNumber(parsed.options, "expected-version");
941
+ if (done === void 0 && followUpAt === void 0 || expectedVersion === void 0) {
942
+ throw new CliUsageError("Provide --done or --follow-up-at, and provide --expected-version.");
942
943
  }
943
944
  writeJson(
944
945
  out,
945
- await client.mailboxes.updateInboxThreadReplyState(
946
+ await client.mailboxes.updateInboxThreadAttention(
946
947
  requirePositional(parsed.positional, 2, "mailbox id"),
947
948
  requirePositional(parsed.positional, 3, "thread id"),
948
949
  {
949
- reply_state: replyState,
950
- expected_reply_version: expectedReplyVersion
950
+ ...done === void 0 ? {} : { done },
951
+ ...followUpAt === void 0 ? {} : { follow_up_at: followUpAt === "null" ? null : followUpAt },
952
+ expected_version: expectedVersion
951
953
  }
952
954
  )
953
955
  );
954
956
  return;
955
957
  }
956
958
  if (subcommand === "draft") {
957
- const expectedReplyVersion = getNumber(parsed.options, "expected-reply-version");
958
- if (expectedReplyVersion === void 0) {
959
- throw new CliUsageError("Provide --expected-reply-version.");
959
+ const expectedVersion = getNumber(parsed.options, "expected-version");
960
+ if (expectedVersion === void 0) {
961
+ throw new CliUsageError("Provide --expected-version.");
960
962
  }
961
963
  const text = await readTextInput(getString(parsed.options, "text"));
962
964
  const html = await readTextInput(getString(parsed.options, "html"));
@@ -967,7 +969,7 @@ async function runInbox(client, parsed, out) {
967
969
  requirePositional(parsed.positional, 2, "mailbox id"),
968
970
  requirePositional(parsed.positional, 3, "thread id"),
969
971
  {
970
- expected_reply_version: expectedReplyVersion,
972
+ expected_version: expectedVersion,
971
973
  ...text ? { text } : {},
972
974
  ...html ? { html } : {},
973
975
  ...getString(parsed.options, "reply-mode") ? { reply_mode: getString(parsed.options, "reply-mode") } : {}
@@ -1574,7 +1576,7 @@ async function runNewsletters(client, parsed, out) {
1574
1576
  }
1575
1577
  throw new CliUsageError("Unknown newsletters command.");
1576
1578
  }
1577
- async function runNewsletterAssets(client, parsed, out) {
1579
+ async function runNewsletterAssets(client, parsed, out, fetchImpl) {
1578
1580
  const subcommand = parsed.positional[1];
1579
1581
  if (subcommand === "list") {
1580
1582
  const kind = getString(parsed.options, "kind");
@@ -1595,14 +1597,37 @@ async function runNewsletterAssets(client, parsed, out) {
1595
1597
  if (subcommand === "upload") {
1596
1598
  const filePath = requirePositional(parsed.positional, 2, "file");
1597
1599
  const data = await readFile(realpathSync(filePath));
1598
- writeJson(
1599
- out,
1600
- await client.newsletters.assets.upload({
1601
- filename: getString(parsed.options, "filename") ?? filePath.split(/[\\/]/).at(-1) ?? "newsletter-asset",
1602
- data,
1603
- content_type: getString(parsed.options, "content-type")
1604
- })
1605
- );
1600
+ const filename = getString(parsed.options, "filename") ?? filePath.split(/[\\/]/).at(-1) ?? "newsletter-asset";
1601
+ const contentType = newsletterAssetContentType(
1602
+ filePath,
1603
+ getString(parsed.options, "content-type")
1604
+ );
1605
+ const thumbnail = contentType.startsWith("video/") ? await readNewsletterVideoThumbnail(parsed.options) : void 0;
1606
+ const prepared = await client.newsletters.assets.prepareUpload({
1607
+ filename,
1608
+ content_type: contentType,
1609
+ size: data.byteLength,
1610
+ sha256: createHash("sha256").update(data).digest("hex")
1611
+ });
1612
+ await putPreparedNewsletterBytes(fetchImpl, prepared.upload_url, prepared.upload_headers, data);
1613
+ if (prepared.kind === "video") {
1614
+ if (!thumbnail) throw new CliUsageError("Video uploads require --thumbnail <jpeg-file>.");
1615
+ await putPreparedNewsletterBytes(
1616
+ fetchImpl,
1617
+ prepared.thumbnail_upload_url,
1618
+ prepared.thumbnail_upload_headers,
1619
+ thumbnail
1620
+ );
1621
+ }
1622
+ const completion = await fetchImpl(prepared.complete_url, {
1623
+ method: prepared.complete_method,
1624
+ redirect: "error"
1625
+ });
1626
+ if (!completion.ok) {
1627
+ throw new CliHttpError(completion.status, { response: await completion.text() });
1628
+ }
1629
+ const asset = await completion.json();
1630
+ writeJson(out, asset);
1606
1631
  return;
1607
1632
  }
1608
1633
  if (subcommand === "register-url") {
@@ -1618,6 +1643,55 @@ async function runNewsletterAssets(client, parsed, out) {
1618
1643
  }
1619
1644
  throw new CliUsageError("Unknown newsletter-assets command.");
1620
1645
  }
1646
+ function newsletterAssetContentType(filePath, declared) {
1647
+ const value = declared?.trim().toLowerCase() ?? "";
1648
+ switch (value) {
1649
+ case "image/gif":
1650
+ case "image/jpeg":
1651
+ case "image/png":
1652
+ case "image/webp":
1653
+ case "video/mp4":
1654
+ case "video/quicktime":
1655
+ case "video/webm":
1656
+ return value;
1657
+ default:
1658
+ if (value) throw new CliUsageError("Unsupported newsletter asset content type.");
1659
+ }
1660
+ const lowerPath = filePath.toLowerCase();
1661
+ if (lowerPath.endsWith(".gif")) return "image/gif";
1662
+ if (lowerPath.endsWith(".jpg") || lowerPath.endsWith(".jpeg")) return "image/jpeg";
1663
+ if (lowerPath.endsWith(".png")) return "image/png";
1664
+ if (lowerPath.endsWith(".webp")) return "image/webp";
1665
+ if (lowerPath.endsWith(".mp4")) return "video/mp4";
1666
+ if (lowerPath.endsWith(".mov")) return "video/quicktime";
1667
+ if (lowerPath.endsWith(".webm")) return "video/webm";
1668
+ throw new CliUsageError("Use --content-type for this newsletter asset file.");
1669
+ }
1670
+ async function readNewsletterVideoThumbnail(options) {
1671
+ const thumbnailPath = getString(options, "thumbnail");
1672
+ if (!thumbnailPath) throw new CliUsageError("Video uploads require --thumbnail <jpeg-file>.");
1673
+ const bytes = await readFile(realpathSync(thumbnailPath));
1674
+ if (bytes.byteLength < 3 || bytes[0] !== 255 || bytes[1] !== 216 || bytes[2] !== 255) {
1675
+ throw new CliUsageError("--thumbnail must be a JPEG file.");
1676
+ }
1677
+ return bytes;
1678
+ }
1679
+ function uploadBody(bytes) {
1680
+ const body = new ArrayBuffer(bytes.byteLength);
1681
+ new Uint8Array(body).set(bytes);
1682
+ return body;
1683
+ }
1684
+ async function putPreparedNewsletterBytes(fetchImpl, url, headers, bytes) {
1685
+ const response = await fetchImpl(url, {
1686
+ method: "PUT",
1687
+ headers,
1688
+ body: uploadBody(bytes),
1689
+ redirect: "error"
1690
+ });
1691
+ if (!response.ok) {
1692
+ throw new CliHttpError(response.status, { response: await response.text() });
1693
+ }
1694
+ }
1621
1695
  var CALENDAR_EVENT_STATUSES = [
1622
1696
  "confirmed",
1623
1697
  "tentative",
@@ -2082,7 +2156,8 @@ async function runCli(argv, options = {}) {
2082
2156
  else if (command === "webhooks") await runWebhooks(client, parsed, out);
2083
2157
  else if (command === "suppressions") await runSuppressions(client, parsed, out);
2084
2158
  else if (command === "newsletters") await runNewsletters(client, parsed, out);
2085
- else if (command === "newsletter-assets") await runNewsletterAssets(client, parsed, out);
2159
+ else if (command === "newsletter-assets")
2160
+ await runNewsletterAssets(client, parsed, out, options.fetch ?? globalThis.fetch);
2086
2161
  else if (command === "newsletter-sender-identities")
2087
2162
  await runNewsletterSenderIdentities(client, parsed, out);
2088
2163
  else if (command === "newsletter-domains") await runNewsletterDomains(client, parsed, out);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shipmail-cli",
3
- "version": "0.4.10",
3
+ "version": "0.4.11",
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.4.16"
44
+ "shipmail": "0.4.17"
45
45
  },
46
46
  "devDependencies": {
47
47
  "tsup": "^8.4.0",