shipmail-cli 0.4.10 → 0.4.12

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 +135 -34
  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.12";
18
18
 
19
19
  // src/cli.ts
20
20
  var DEFAULT_BASE_URL = "https://shipmail.to/api/v1";
@@ -360,6 +360,8 @@ Usage:
360
360
  shipmail mailboxes resume <id>
361
361
  shipmail mailboxes export <id>
362
362
  shipmail mailboxes export-status <id> <export-id>
363
+ shipmail mailboxes import-resume <id> <import-id>
364
+ shipmail mailboxes import-restore <id> <import-id> --file-id impf_...
363
365
  shipmail mailboxes app-passwords list <id>
364
366
  shipmail mailboxes app-passwords create <id> --name "Desktop mail" [--expires-at 2026-10-01T00:00:00Z] [--allowed-cidr 203.0.113.0/24]
365
367
  shipmail mailboxes app-passwords revoke <id> <app-password-id>
@@ -374,20 +376,20 @@ Usage:
374
376
  shipmail mailboxes sandbox-inbound <id> --from sender@example.com --subject "Test" (--text "Hello" | --html "<p>Hello</p>") [--to support@example.com]
375
377
  shipmail inbox list <mailbox-id> [--folder-role inbox] [--search text] [--after ISO] [--before ISO] [--limit 25] [--cursor cur]
376
378
  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]
379
+ shipmail inbox threads <mailbox-id> [--attention-state needs_reply] [--after ISO] [--before ISO] [--limit 25] [--cursor cur]
378
380
  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]
381
+ shipmail inbox thread-attention <mailbox-id> <thread-id> --done true --expected-version 3
382
+ shipmail inbox draft <mailbox-id> <thread-id> --expected-version 3 (--text "Hello" | --html "<p>Hello</p>") [--reply-mode reply_all]
381
383
  shipmail inbox send-draft <mailbox-id> <thread-id> <draft-id>
382
- shipmail inbox reply-message <mailbox-id> <message-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>")
383
- shipmail inbox reply-thread <mailbox-id> <thread-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>")
384
+ shipmail inbox reply-message <mailbox-id> <message-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply]
385
+ shipmail inbox reply-thread <mailbox-id> <thread-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply]
384
386
  shipmail inbox update <mailbox-id> <message-id> [--read true] [--starred false]
385
387
  shipmail inbox move <mailbox-id> <message-id> (--target-role archive | --target-folder-id fld)
386
- shipmail messages send --mailbox-id mbx --to user@example.com --subject "Hi" --text "Hello" [--sandbox-outcome delivered|bounced|complained] [--client-reference crm-123] [--metadata JSON|@file] [--source-rfc-message-id '<crm-123@example.com>'] [--header 'X-CRM-ID: 123']
388
+ shipmail messages send --mailbox-id mbx --to user@example.com --subject "Hi" --text "Hello" [--track-reply] [--sandbox-outcome delivered|bounced|complained] [--client-reference crm-123] [--metadata JSON|@file] [--source-rfc-message-id '<crm-123@example.com>'] [--header 'X-CRM-ID: 123']
387
389
  shipmail messages list (--mailbox-id mbx | --client-reference crm-123) [--limit 25] [--cursor cur]
388
390
  shipmail messages analytics [--updated-after ISO] [--updated-before ISO] [--limit 100] [--cursor cur]
389
391
  shipmail messages get <message-id>
390
- shipmail messages reply <message-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--client-reference crm-reply-123]
392
+ shipmail messages reply <message-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply] [--client-reference crm-reply-123]
391
393
  shipmail scheduled-messages list [--include-held] [--search text] [--limit 100]
392
394
  shipmail scheduled-messages get <message-id>
393
395
  shipmail scheduled-messages update <message-id> --to user@example.com --subject "Hi" --scheduled-at ISO (--text "Hello" | --html "<p>Hello</p>") [--staged-attachment-id sat_...]
@@ -415,7 +417,7 @@ Usage:
415
417
  shipmail newsletters cancel <id>
416
418
  shipmail newsletters resume <id>
417
419
  shipmail newsletter-assets list [--kind image|video] [--q text] [--limit 25] [--cursor cur]
418
- shipmail newsletter-assets upload <file> [--filename name] [--content-type type]
420
+ shipmail newsletter-assets upload <file> [--filename name] [--content-type type] [--thumbnail poster.jpg]
419
421
  shipmail newsletter-assets register-url <url> [--filename name] [--thumbnail-url url]
420
422
  shipmail newsletter-sender-identities list [--limit 25] [--cursor cur]
421
423
  shipmail newsletter-domains list [--limit 25] [--cursor cur]
@@ -581,6 +583,7 @@ async function sendMessageParams(options) {
581
583
  ...headers.length > 0 ? { headers } : {},
582
584
  ...getStringList(options, "staged-attachment-id").length > 0 ? { staged_attachment_ids: getStringList(options, "staged-attachment-id") } : {},
583
585
  ...getString(options, "scheduled-at") ? { scheduled_at: getString(options, "scheduled-at") } : {},
586
+ ...getBoolean(options, "track-reply") ? { track_reply: true } : {},
584
587
  ...getString(options, "sandbox-outcome") ? {
585
588
  sandbox_outcome: getEnum(options, "sandbox-outcome", [
586
589
  "delivered",
@@ -609,6 +612,7 @@ async function replyToMessageParams(options) {
609
612
  ...getString(options, "source-rfc-message-id") ? { source_rfc_message_id: getString(options, "source-rfc-message-id") } : {},
610
613
  ...headers.length > 0 ? { headers } : {},
611
614
  ...getString(options, "scheduled-at") ? { scheduled_at: getString(options, "scheduled-at") } : {},
615
+ ...getBoolean(options, "track-reply") ? { track_reply: true } : {},
612
616
  ...getString(options, "sandbox-outcome") ? {
613
617
  sandbox_outcome: getEnum(options, "sandbox-outcome", [
614
618
  "delivered",
@@ -749,6 +753,28 @@ async function runMailboxes(client, parsed, out) {
749
753
  );
750
754
  return;
751
755
  }
756
+ if (subcommand === "import-resume") {
757
+ writeJson(
758
+ out,
759
+ await client.mailboxes.resumeImport(
760
+ requirePositional(parsed.positional, 2, "mailbox id"),
761
+ requirePositional(parsed.positional, 3, "import id")
762
+ )
763
+ );
764
+ return;
765
+ }
766
+ if (subcommand === "import-restore") {
767
+ const fileIds = getStringList(parsed.options, "file-id");
768
+ writeJson(
769
+ out,
770
+ await client.mailboxes.restoreImport(
771
+ requirePositional(parsed.positional, 2, "mailbox id"),
772
+ requirePositional(parsed.positional, 3, "import id"),
773
+ { file_ids: fileIds }
774
+ )
775
+ );
776
+ return;
777
+ }
752
778
  if (subcommand === "app-passwords") {
753
779
  const command = requirePositional(parsed.positional, 2, "app-password command");
754
780
  const mailboxId = requirePositional(parsed.positional, 3, "mailbox id");
@@ -912,7 +938,7 @@ async function runInbox(client, parsed, out) {
912
938
  return;
913
939
  }
914
940
  if (subcommand === "threads") {
915
- const replyState = getString(parsed.options, "reply-state");
941
+ const attentionState = getString(parsed.options, "attention-state");
916
942
  const sortBy = getString(parsed.options, "sort-by");
917
943
  const order = getString(parsed.options, "order");
918
944
  writeJson(
@@ -920,8 +946,8 @@ async function runInbox(client, parsed, out) {
920
946
  await client.mailboxes.listInboxThreads(
921
947
  requirePositional(parsed.positional, 2, "mailbox id"),
922
948
  {
923
- ...replyState ? {
924
- reply_state: replyState
949
+ ...attentionState ? {
950
+ attention_state: attentionState
925
951
  } : {},
926
952
  ...sortBy ? { sort_by: sortBy } : {},
927
953
  ...order ? { order } : {},
@@ -934,29 +960,31 @@ async function runInbox(client, parsed, out) {
934
960
  );
935
961
  return;
936
962
  }
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.");
963
+ if (subcommand === "thread-attention") {
964
+ const done = getBoolean(parsed.options, "done");
965
+ const followUpAt = getString(parsed.options, "follow-up-at");
966
+ const expectedVersion = getNumber(parsed.options, "expected-version");
967
+ if (done === void 0 && followUpAt === void 0 || expectedVersion === void 0) {
968
+ throw new CliUsageError("Provide --done or --follow-up-at, and provide --expected-version.");
942
969
  }
943
970
  writeJson(
944
971
  out,
945
- await client.mailboxes.updateInboxThreadReplyState(
972
+ await client.mailboxes.updateInboxThreadAttention(
946
973
  requirePositional(parsed.positional, 2, "mailbox id"),
947
974
  requirePositional(parsed.positional, 3, "thread id"),
948
975
  {
949
- reply_state: replyState,
950
- expected_reply_version: expectedReplyVersion
976
+ ...done === void 0 ? {} : { done },
977
+ ...followUpAt === void 0 ? {} : { follow_up_at: followUpAt === "null" ? null : followUpAt },
978
+ expected_version: expectedVersion
951
979
  }
952
980
  )
953
981
  );
954
982
  return;
955
983
  }
956
984
  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.");
985
+ const expectedVersion = getNumber(parsed.options, "expected-version");
986
+ if (expectedVersion === void 0) {
987
+ throw new CliUsageError("Provide --expected-version.");
960
988
  }
961
989
  const text = await readTextInput(getString(parsed.options, "text"));
962
990
  const html = await readTextInput(getString(parsed.options, "html"));
@@ -967,7 +995,7 @@ async function runInbox(client, parsed, out) {
967
995
  requirePositional(parsed.positional, 2, "mailbox id"),
968
996
  requirePositional(parsed.positional, 3, "thread id"),
969
997
  {
970
- expected_reply_version: expectedReplyVersion,
998
+ expected_version: expectedVersion,
971
999
  ...text ? { text } : {},
972
1000
  ...html ? { html } : {},
973
1001
  ...getString(parsed.options, "reply-mode") ? { reply_mode: getString(parsed.options, "reply-mode") } : {}
@@ -1574,7 +1602,7 @@ async function runNewsletters(client, parsed, out) {
1574
1602
  }
1575
1603
  throw new CliUsageError("Unknown newsletters command.");
1576
1604
  }
1577
- async function runNewsletterAssets(client, parsed, out) {
1605
+ async function runNewsletterAssets(client, parsed, out, fetchImpl) {
1578
1606
  const subcommand = parsed.positional[1];
1579
1607
  if (subcommand === "list") {
1580
1608
  const kind = getString(parsed.options, "kind");
@@ -1595,14 +1623,37 @@ async function runNewsletterAssets(client, parsed, out) {
1595
1623
  if (subcommand === "upload") {
1596
1624
  const filePath = requirePositional(parsed.positional, 2, "file");
1597
1625
  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
- );
1626
+ const filename = getString(parsed.options, "filename") ?? filePath.split(/[\\/]/).at(-1) ?? "newsletter-asset";
1627
+ const contentType = newsletterAssetContentType(
1628
+ filePath,
1629
+ getString(parsed.options, "content-type")
1630
+ );
1631
+ const thumbnail = contentType.startsWith("video/") ? await readNewsletterVideoThumbnail(parsed.options) : void 0;
1632
+ const prepared = await client.newsletters.assets.prepareUpload({
1633
+ filename,
1634
+ content_type: contentType,
1635
+ size: data.byteLength,
1636
+ sha256: createHash("sha256").update(data).digest("hex")
1637
+ });
1638
+ await putPreparedNewsletterBytes(fetchImpl, prepared.upload_url, prepared.upload_headers, data);
1639
+ if (prepared.kind === "video") {
1640
+ if (!thumbnail) throw new CliUsageError("Video uploads require --thumbnail <jpeg-file>.");
1641
+ await putPreparedNewsletterBytes(
1642
+ fetchImpl,
1643
+ prepared.thumbnail_upload_url,
1644
+ prepared.thumbnail_upload_headers,
1645
+ thumbnail
1646
+ );
1647
+ }
1648
+ const completion = await fetchImpl(prepared.complete_url, {
1649
+ method: prepared.complete_method,
1650
+ redirect: "error"
1651
+ });
1652
+ if (!completion.ok) {
1653
+ throw new CliHttpError(completion.status, { response: await completion.text() });
1654
+ }
1655
+ const asset = await completion.json();
1656
+ writeJson(out, asset);
1606
1657
  return;
1607
1658
  }
1608
1659
  if (subcommand === "register-url") {
@@ -1618,6 +1669,55 @@ async function runNewsletterAssets(client, parsed, out) {
1618
1669
  }
1619
1670
  throw new CliUsageError("Unknown newsletter-assets command.");
1620
1671
  }
1672
+ function newsletterAssetContentType(filePath, declared) {
1673
+ const value = declared?.trim().toLowerCase() ?? "";
1674
+ switch (value) {
1675
+ case "image/gif":
1676
+ case "image/jpeg":
1677
+ case "image/png":
1678
+ case "image/webp":
1679
+ case "video/mp4":
1680
+ case "video/quicktime":
1681
+ case "video/webm":
1682
+ return value;
1683
+ default:
1684
+ if (value) throw new CliUsageError("Unsupported newsletter asset content type.");
1685
+ }
1686
+ const lowerPath = filePath.toLowerCase();
1687
+ if (lowerPath.endsWith(".gif")) return "image/gif";
1688
+ if (lowerPath.endsWith(".jpg") || lowerPath.endsWith(".jpeg")) return "image/jpeg";
1689
+ if (lowerPath.endsWith(".png")) return "image/png";
1690
+ if (lowerPath.endsWith(".webp")) return "image/webp";
1691
+ if (lowerPath.endsWith(".mp4")) return "video/mp4";
1692
+ if (lowerPath.endsWith(".mov")) return "video/quicktime";
1693
+ if (lowerPath.endsWith(".webm")) return "video/webm";
1694
+ throw new CliUsageError("Use --content-type for this newsletter asset file.");
1695
+ }
1696
+ async function readNewsletterVideoThumbnail(options) {
1697
+ const thumbnailPath = getString(options, "thumbnail");
1698
+ if (!thumbnailPath) throw new CliUsageError("Video uploads require --thumbnail <jpeg-file>.");
1699
+ const bytes = await readFile(realpathSync(thumbnailPath));
1700
+ if (bytes.byteLength < 3 || bytes[0] !== 255 || bytes[1] !== 216 || bytes[2] !== 255) {
1701
+ throw new CliUsageError("--thumbnail must be a JPEG file.");
1702
+ }
1703
+ return bytes;
1704
+ }
1705
+ function uploadBody(bytes) {
1706
+ const body = new ArrayBuffer(bytes.byteLength);
1707
+ new Uint8Array(body).set(bytes);
1708
+ return body;
1709
+ }
1710
+ async function putPreparedNewsletterBytes(fetchImpl, url, headers, bytes) {
1711
+ const response = await fetchImpl(url, {
1712
+ method: "PUT",
1713
+ headers,
1714
+ body: uploadBody(bytes),
1715
+ redirect: "error"
1716
+ });
1717
+ if (!response.ok) {
1718
+ throw new CliHttpError(response.status, { response: await response.text() });
1719
+ }
1720
+ }
1621
1721
  var CALENDAR_EVENT_STATUSES = [
1622
1722
  "confirmed",
1623
1723
  "tentative",
@@ -2082,7 +2182,8 @@ async function runCli(argv, options = {}) {
2082
2182
  else if (command === "webhooks") await runWebhooks(client, parsed, out);
2083
2183
  else if (command === "suppressions") await runSuppressions(client, parsed, out);
2084
2184
  else if (command === "newsletters") await runNewsletters(client, parsed, out);
2085
- else if (command === "newsletter-assets") await runNewsletterAssets(client, parsed, out);
2185
+ else if (command === "newsletter-assets")
2186
+ await runNewsletterAssets(client, parsed, out, options.fetch ?? globalThis.fetch);
2086
2187
  else if (command === "newsletter-sender-identities")
2087
2188
  await runNewsletterSenderIdentities(client, parsed, out);
2088
2189
  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.12",
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.20"
45
45
  },
46
46
  "devDependencies": {
47
47
  "tsup": "^8.4.0",