shipmail-cli 0.1.7 → 0.1.9

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 +24 -6
  2. package/dist/cli.js +229 -9
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -22,19 +22,35 @@ You can also pass `--api-key` to any command. Use `SHIPMAIL_BASE_URL` or `--base
22
22
  # Check public API health
23
23
  shipmail status
24
24
 
25
- # Create a domain and verify DNS
25
+ # Create a domain, retrieve records, and verify DNS
26
26
  shipmail domains create example.com
27
+ shipmail domains dns-records dom_123
27
28
  shipmail domains verify dom_123
28
29
 
29
30
  # Create a mailbox
30
- shipmail mailboxes create --domain-id dom_123 --address hello --display-name "Hello"
31
+ shipmail mailboxes create --domain-id dom_123 --address hello --password-file ./mailbox-password.txt --display-name "Hello"
32
+ shipmail mailboxes suspend mbx_123
33
+ shipmail mailboxes resume mbx_123
34
+ shipmail mailboxes forwarding-add mbx_123 --destination owner@example.net
35
+ shipmail mailboxes forwarding mbx_123
36
+ shipmail mailboxes forwarding-remove mbx_123 fwd_123
31
37
 
32
38
  # Send email
33
39
  shipmail messages send \
34
40
  --mailbox-id mbx_123 \
35
41
  --to user@example.com \
36
42
  --subject "Hello" \
37
- --text "It works."
43
+ --text "It works." \
44
+ --client-reference crm-123 \
45
+ --metadata '{"campaign":"onboarding"}' \
46
+ --source-rfc-message-id '<crm-123@example.com>'
47
+
48
+ shipmail messages list --client-reference crm-123
49
+ shipmail messages reply msg_123 --to user@example.com --text "Thanks"
50
+
51
+ # Use an sm_test_... key to simulate an outbound result and fake inbound reply
52
+ shipmail messages send --mailbox-id mbx_123 --to user@example.com --subject "Test" --text "Not delivered" --sandbox-outcome bounced
53
+ shipmail mailboxes sandbox-inbound mbx_123 --from customer@example.com --subject "Re: Test" --text "Fake inbound reply"
38
54
 
39
55
  # Read inbound mail
40
56
  shipmail inbox list mbx_123 --folder-role inbox --limit 25
@@ -62,6 +78,7 @@ shipmail webhooks create \
62
78
  --url https://example.com/shipmail/webhook \
63
79
  --events message.received,message.bounced
64
80
  shipmail webhooks deliveries whk_123 --status failed
81
+ shipmail webhooks replay-delivery whk_123 dlv_123 --idempotency-key replay-123
65
82
  ```
66
83
 
67
84
  `newsletter-assets list` includes current newsletter media storage usage in the JSON response.
@@ -87,10 +104,11 @@ Use repeatable `--header "Name: value"` flags for request headers. Use `--output
87
104
  - `status`
88
105
  - `api <METHOD> <PATH>`
89
106
  - `domains list|search|create|get|verify|delete`
90
- - `mailboxes list|create|get|folders|identities|rules`
107
+ - `mailboxes list|create|get|suspend|resume|forwarding|forwarding-add|forwarding-remove|folders|identities|rules`
91
108
  - `inbox list|thread|update|move`
92
- - `messages send|list|get`
93
- - `webhooks list|create|test|deliveries`
109
+ - `messages send|list|get|reply` (test keys also support `--sandbox-outcome`)
110
+ - `mailboxes sandbox-inbound`
111
+ - `webhooks list|create|test|deliveries|replay-delivery`
94
112
  - `suppressions list|remove`
95
113
  - `newsletter-domains list`
96
114
  - `newsletters create|update|preview|preflight|test-send|schedule|cancel|resume`
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  } from "shipmail";
13
13
 
14
14
  // src/version.ts
15
- var VERSION = "0.1.7";
15
+ var VERSION = "0.1.9";
16
16
 
17
17
  // src/cli.ts
18
18
  var DEFAULT_BASE_URL = "https://shipmail.to/api/v1";
@@ -311,21 +311,34 @@ Usage:
311
311
  shipmail domains search <keyword>
312
312
  shipmail domains create <name>
313
313
  shipmail domains get <id>
314
+ shipmail domains dns-records <id>
314
315
  shipmail domains verify <id>
315
316
  shipmail domains delete <id>
316
317
  shipmail mailboxes list [--domain-id dom] [--limit 25] [--cursor cur]
317
- shipmail mailboxes create --domain-id dom --address hello [--display-name "Hello"]
318
+ shipmail mailboxes create --domain-id dom --address hello (--password value | --password-file path) [--display-name "Hello"]
318
319
  shipmail mailboxes get <id>
320
+ shipmail mailboxes suspend <id>
321
+ shipmail mailboxes resume <id>
322
+ shipmail mailboxes export <id>
323
+ shipmail mailboxes export-status <id> <export-id>
319
324
  shipmail mailboxes folders <id>
320
325
  shipmail mailboxes identities <id>
321
326
  shipmail mailboxes rules <id>
327
+ shipmail mailboxes forwarding <id>
328
+ shipmail mailboxes forwarding-add <id> --destination owner@example.com
329
+ shipmail mailboxes forwarding-remove <id> <forwarding-id>
330
+ shipmail mailboxes sandbox-inbound <id> --from sender@example.com --subject "Test" (--text "Hello" | --html "<p>Hello</p>") [--to support@example.com]
322
331
  shipmail inbox list <mailbox-id> [--folder-role inbox] [--limit 25]
323
332
  shipmail inbox thread <mailbox-id> <thread-id>
324
333
  shipmail inbox update <mailbox-id> <message-id> [--read true] [--starred false]
325
334
  shipmail inbox move <mailbox-id> <message-id> (--target-role archive | --target-folder-id fld)
326
- shipmail messages send --mailbox-id mbx --to user@example.com --subject "Hi" --text "Hello"
335
+ 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']
336
+ shipmail messages list (--mailbox-id mbx | --client-reference crm-123) [--limit 25] [--cursor cur]
337
+ shipmail messages get <message-id>
338
+ shipmail messages reply <message-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--client-reference crm-reply-123]
327
339
  shipmail webhooks list
328
340
  shipmail webhooks create --url https://example.com/hook --events message.received,message.bounced
341
+ shipmail webhooks replay-delivery <webhook-id> <delivery-id> [--idempotency-key key]
329
342
  shipmail suppressions list
330
343
  shipmail suppressions remove <email>
331
344
  shipmail newsletters create \\
@@ -359,7 +372,7 @@ Usage:
359
372
  shipmail audiences subscribers resubscribe <audience-id> <subscriber-id> --consent-source "Signup form"
360
373
  shipmail audiences subscribers remove <audience-id> <subscriber-id>
361
374
  shipmail calendar events list --mailbox hello@example.com --from 2026-08-01T00:00:00Z --to 2026-08-08T00:00:00Z [--expand true] [--limit 100]
362
- shipmail calendar events create --mailbox hello@example.com --title "Sync" --start 2026-08-01T14:00:00 [--timezone Europe/Paris] [--duration PT30M] [--all-day true] [--location "HQ"] [--video-url https://...] [--reminders JSON|@file]
375
+ shipmail calendar events create --mailbox hello@example.com --title "Sync" --start 2026-08-01T14:00:00 [--timezone Europe/Paris] [--duration PT30M] [--attendees JSON|@file] [--reminders JSON|@file]
363
376
  shipmail calendar events get <event-id> --mailbox hello@example.com
364
377
  shipmail calendar events update <event-id> --mailbox hello@example.com [--title "..."] [--start ...]
365
378
  shipmail calendar events delete <event-id> --mailbox hello@example.com
@@ -425,7 +438,24 @@ function webhookDeliveryParams(options) {
425
438
  ...eventType ? { event_type: eventType } : {}
426
439
  };
427
440
  }
428
- function sendMessageParams(options) {
441
+ function outboundMessageHeaders(options) {
442
+ const headers = getStringList(options, "header");
443
+ return headers.map((value) => {
444
+ const index = value.indexOf(":");
445
+ if (index <= 0) throw new CliUsageError("--header must use 'Name: value' format.");
446
+ return { name: value.slice(0, index).trim(), value: value.slice(index + 1).trim() };
447
+ });
448
+ }
449
+ async function messageMetadata(options) {
450
+ const raw = getString(options, "metadata");
451
+ if (!raw) return void 0;
452
+ const parsed = await readJsonInput(raw);
453
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
454
+ throw new CliUsageError("--metadata must be a JSON object or @path/to/file.json.");
455
+ }
456
+ return parsed;
457
+ }
458
+ async function sendMessageParams(options) {
429
459
  const mailboxId = getString(options, "mailbox-id");
430
460
  const from = getString(options, "from");
431
461
  const to = messageRecipients(options, "to");
@@ -435,6 +465,8 @@ function sendMessageParams(options) {
435
465
  const subject = getRequiredString(options, "subject");
436
466
  const text = getString(options, "text");
437
467
  const html = getString(options, "html");
468
+ const headers = outboundMessageHeaders(options);
469
+ const metadata = await messageMetadata(options);
438
470
  if (!mailboxId && !from) throw new CliUsageError("Provide --mailbox-id or --from.");
439
471
  if (to.length === 0) throw new CliUsageError("Provide at least one --to recipient.");
440
472
  if (!text && !html) throw new CliUsageError("Provide --text or --html.");
@@ -447,7 +479,47 @@ function sendMessageParams(options) {
447
479
  ...replyTo ? { reply_to: replyTo } : {},
448
480
  subject,
449
481
  ...html ? { html } : {},
450
- ...text ? { text } : {}
482
+ ...text ? { text } : {},
483
+ ...getString(options, "client-reference") ? { client_reference: getString(options, "client-reference") } : {},
484
+ ...metadata ? { metadata } : {},
485
+ ...getString(options, "source-rfc-message-id") ? { source_rfc_message_id: getString(options, "source-rfc-message-id") } : {},
486
+ ...headers.length > 0 ? { headers } : {},
487
+ ...getString(options, "scheduled-at") ? { scheduled_at: getString(options, "scheduled-at") } : {},
488
+ ...getString(options, "sandbox-outcome") ? {
489
+ sandbox_outcome: getEnum(options, "sandbox-outcome", [
490
+ "delivered",
491
+ "bounced",
492
+ "complained"
493
+ ])
494
+ } : {}
495
+ };
496
+ }
497
+ async function replyToMessageParams(options) {
498
+ const to = messageRecipients(options, "to");
499
+ const cc = messageRecipients(options, "cc");
500
+ const text = getString(options, "text");
501
+ const html = getString(options, "html");
502
+ const headers = outboundMessageHeaders(options);
503
+ const metadata = await messageMetadata(options);
504
+ if (to.length === 0) throw new CliUsageError("Provide at least one --to recipient.");
505
+ if (!text && !html) throw new CliUsageError("Provide --text or --html.");
506
+ return {
507
+ to,
508
+ ...cc.length > 0 ? { cc } : {},
509
+ ...html ? { html } : {},
510
+ ...text ? { text } : {},
511
+ ...getString(options, "client-reference") ? { client_reference: getString(options, "client-reference") } : {},
512
+ ...metadata ? { metadata } : {},
513
+ ...getString(options, "source-rfc-message-id") ? { source_rfc_message_id: getString(options, "source-rfc-message-id") } : {},
514
+ ...headers.length > 0 ? { headers } : {},
515
+ ...getString(options, "scheduled-at") ? { scheduled_at: getString(options, "scheduled-at") } : {},
516
+ ...getString(options, "sandbox-outcome") ? {
517
+ sandbox_outcome: getEnum(options, "sandbox-outcome", [
518
+ "delivered",
519
+ "bounced",
520
+ "complained"
521
+ ])
522
+ } : {}
451
523
  };
452
524
  }
453
525
  async function runDomains(client, parsed, out) {
@@ -480,6 +552,13 @@ async function runDomains(client, parsed, out) {
480
552
  writeJson(out, await client.domains.get(requirePositional(parsed.positional, 2, "domain id")));
481
553
  return;
482
554
  }
555
+ if (subcommand === "dns-records") {
556
+ writeJson(
557
+ out,
558
+ await client.domains.getDnsRecords(requirePositional(parsed.positional, 2, "domain id"))
559
+ );
560
+ return;
561
+ }
483
562
  if (subcommand === "verify") {
484
563
  writeJson(
485
564
  out,
@@ -510,11 +589,22 @@ async function runMailboxes(client, parsed, out) {
510
589
  }
511
590
  if (subcommand === "create") {
512
591
  const displayName = getString(parsed.options, "display-name");
592
+ const passwordValue = getString(parsed.options, "password");
593
+ const passwordFile = getString(parsed.options, "password-file");
594
+ if (passwordValue && passwordFile) {
595
+ throw new CliUsageError("Pass either --password or --password-file, not both.");
596
+ }
597
+ if (!passwordValue && !passwordFile) {
598
+ throw new CliUsageError("Missing --password or --password-file.");
599
+ }
600
+ const password = passwordFile ? (await readFile(passwordFile, "utf8")).replace(/\r?\n$/, "") : passwordValue;
601
+ if (!password) throw new CliUsageError("Mailbox password must not be empty.");
513
602
  writeJson(
514
603
  out,
515
604
  await client.mailboxes.create({
516
605
  domain_id: getRequiredString(parsed.options, "domain-id"),
517
606
  address: getRequiredString(parsed.options, "address"),
607
+ password,
518
608
  ...displayName ? { display_name: displayName } : {}
519
609
  })
520
610
  );
@@ -527,6 +617,37 @@ async function runMailboxes(client, parsed, out) {
527
617
  );
528
618
  return;
529
619
  }
620
+ if (subcommand === "suspend") {
621
+ writeJson(
622
+ out,
623
+ await client.mailboxes.suspend(requirePositional(parsed.positional, 2, "mailbox id"))
624
+ );
625
+ return;
626
+ }
627
+ if (subcommand === "resume") {
628
+ writeJson(
629
+ out,
630
+ await client.mailboxes.resume(requirePositional(parsed.positional, 2, "mailbox id"))
631
+ );
632
+ return;
633
+ }
634
+ if (subcommand === "export") {
635
+ writeJson(
636
+ out,
637
+ await client.mailboxes.createExport(requirePositional(parsed.positional, 2, "mailbox id"))
638
+ );
639
+ return;
640
+ }
641
+ if (subcommand === "export-status") {
642
+ writeJson(
643
+ out,
644
+ await client.mailboxes.getExport(
645
+ requirePositional(parsed.positional, 2, "mailbox id"),
646
+ requirePositional(parsed.positional, 3, "export id")
647
+ )
648
+ );
649
+ return;
650
+ }
530
651
  if (subcommand === "folders") {
531
652
  writeJson(
532
653
  out,
@@ -548,6 +669,58 @@ async function runMailboxes(client, parsed, out) {
548
669
  );
549
670
  return;
550
671
  }
672
+ if (subcommand === "forwarding") {
673
+ writeJson(
674
+ out,
675
+ await client.mailboxes.listForwarding(requirePositional(parsed.positional, 2, "mailbox id"))
676
+ );
677
+ return;
678
+ }
679
+ if (subcommand === "forwarding-add") {
680
+ writeJson(
681
+ out,
682
+ await client.mailboxes.createForwarding(
683
+ requirePositional(parsed.positional, 2, "mailbox id"),
684
+ { destination: getRequiredString(parsed.options, "destination") }
685
+ )
686
+ );
687
+ return;
688
+ }
689
+ if (subcommand === "forwarding-remove") {
690
+ const mailboxId = requirePositional(parsed.positional, 2, "mailbox id");
691
+ const forwardingId = requirePositional(parsed.positional, 3, "forwarding id");
692
+ await client.mailboxes.deleteForwarding(mailboxId, forwardingId);
693
+ writeJson(out, { ok: true, id: forwardingId });
694
+ return;
695
+ }
696
+ if (subcommand === "sandbox-inbound") {
697
+ const text = getString(parsed.options, "text");
698
+ const html = getString(parsed.options, "html");
699
+ if (!text && !html) throw new CliUsageError("Provide --text or --html.");
700
+ const to = messageRecipients(parsed.options, "to");
701
+ const cc = messageRecipients(parsed.options, "cc");
702
+ const headers = outboundMessageHeaders(parsed.options);
703
+ writeJson(
704
+ out,
705
+ await client.mailboxes.injectSandboxInbound(
706
+ requirePositional(parsed.positional, 2, "mailbox id"),
707
+ {
708
+ from: getRequiredString(parsed.options, "from"),
709
+ ...to.length > 0 ? { to } : {},
710
+ ...cc.length > 0 ? { cc } : {},
711
+ subject: getRequiredString(parsed.options, "subject"),
712
+ ...text ? { text } : {},
713
+ ...html ? { html } : {},
714
+ ...getString(parsed.options, "message-id") ? { message_id: getString(parsed.options, "message-id") } : {},
715
+ ...getString(parsed.options, "in-reply-to") ? { in_reply_to: getString(parsed.options, "in-reply-to") } : {},
716
+ ...getStringList(parsed.options, "reference").length > 0 ? { references: getStringList(parsed.options, "reference") } : {},
717
+ ...getString(parsed.options, "received-at") ? { received_at: getString(parsed.options, "received-at") } : {},
718
+ ...headers.length > 0 ? { headers } : {}
719
+ }
720
+ )
721
+ );
722
+ return;
723
+ }
551
724
  throw new CliUsageError("Unknown mailboxes command.");
552
725
  }
553
726
  async function runInbox(client, parsed, out) {
@@ -616,20 +789,36 @@ async function runInbox(client, parsed, out) {
616
789
  async function runMessages(client, parsed, out) {
617
790
  const subcommand = parsed.positional[1];
618
791
  if (subcommand === "send") {
619
- writeJson(out, await client.messages.send(sendMessageParams(parsed.options)));
792
+ writeJson(out, await client.messages.send(await sendMessageParams(parsed.options)));
620
793
  return;
621
794
  }
622
795
  if (subcommand === "list") {
796
+ const mailboxId = getString(parsed.options, "mailbox-id");
797
+ const clientReference = getString(parsed.options, "client-reference");
798
+ if (!mailboxId && !clientReference) {
799
+ throw new CliUsageError("Provide --mailbox-id or --client-reference.");
800
+ }
801
+ const filter = mailboxId ? { mailbox_id: mailboxId, ...clientReference ? { client_reference: clientReference } : {} } : { client_reference: clientReference };
623
802
  writeJson(
624
803
  out,
625
804
  await client.messages.list({
626
- mailbox_id: getRequiredString(parsed.options, "mailbox-id"),
805
+ ...filter,
627
806
  cursor: getString(parsed.options, "cursor"),
628
807
  limit: getNumber(parsed.options, "limit")
629
808
  })
630
809
  );
631
810
  return;
632
811
  }
812
+ if (subcommand === "reply") {
813
+ writeJson(
814
+ out,
815
+ await client.messages.reply(
816
+ requirePositional(parsed.positional, 2, "message id"),
817
+ await replyToMessageParams(parsed.options)
818
+ )
819
+ );
820
+ return;
821
+ }
633
822
  if (subcommand === "get") {
634
823
  writeJson(
635
824
  out,
@@ -680,6 +869,18 @@ async function runWebhooks(client, parsed, out) {
680
869
  );
681
870
  return;
682
871
  }
872
+ if (subcommand === "replay-delivery") {
873
+ const idempotencyKey = getString(parsed.options, "idempotency-key");
874
+ writeJson(
875
+ out,
876
+ await client.webhooks.replayDelivery(
877
+ requirePositional(parsed.positional, 2, "webhook id"),
878
+ requirePositional(parsed.positional, 3, "delivery id"),
879
+ idempotencyKey ? { idempotencyKey } : void 0
880
+ )
881
+ );
882
+ return;
883
+ }
683
884
  throw new CliUsageError("Unknown webhooks command.");
684
885
  }
685
886
  async function runSuppressions(client, parsed, out) {
@@ -704,6 +905,7 @@ async function runSuppressions(client, parsed, out) {
704
905
  }
705
906
  var SUBSCRIBER_STATUS_VALUES = [
706
907
  "subscribed",
908
+ "pending",
707
909
  "unsubscribed",
708
910
  "bounced",
709
911
  "complained",
@@ -1033,6 +1235,22 @@ async function readRemindersInput(value) {
1033
1235
  }
1034
1236
  return parsed;
1035
1237
  }
1238
+ function isAttendeeArray(value) {
1239
+ return Array.isArray(value) && value.every((item) => {
1240
+ if (typeof item !== "object" || item === null || Array.isArray(item)) return false;
1241
+ const email = Reflect.get(item, "email");
1242
+ const name = Reflect.get(item, "name");
1243
+ return typeof email === "string" && (name === void 0 || typeof name === "string");
1244
+ });
1245
+ }
1246
+ async function readAttendeesInput(value) {
1247
+ const parsed = await readJsonInput(value);
1248
+ if (parsed === void 0) return void 0;
1249
+ if (!isAttendeeArray(parsed)) {
1250
+ throw new CliUsageError("--attendees must be a JSON array of { email, name? } objects.");
1251
+ }
1252
+ return parsed;
1253
+ }
1036
1254
  async function calendarEventFields(options) {
1037
1255
  const description = getString(options, "description");
1038
1256
  const timezone = getString(options, "timezone");
@@ -1045,6 +1263,7 @@ async function calendarEventFields(options) {
1045
1263
  const freeBusyStatus = getEnum(options, "free-busy-status", CALENDAR_FREE_BUSY_STATUSES);
1046
1264
  const privacy = getEnum(options, "privacy", CALENDAR_PRIVACIES);
1047
1265
  const reminders = await readRemindersInput(getString(options, "reminders"));
1266
+ const attendees = await readAttendeesInput(getString(options, "attendees"));
1048
1267
  return {
1049
1268
  ...description !== void 0 ? { description } : {},
1050
1269
  ...timezone !== void 0 ? { timezone } : {},
@@ -1056,7 +1275,8 @@ async function calendarEventFields(options) {
1056
1275
  ...status !== void 0 ? { status } : {},
1057
1276
  ...freeBusyStatus !== void 0 ? { free_busy_status: freeBusyStatus } : {},
1058
1277
  ...privacy !== void 0 ? { privacy } : {},
1059
- ...reminders !== void 0 ? { reminders } : {}
1278
+ ...reminders !== void 0 ? { reminders } : {},
1279
+ ...attendees !== void 0 ? { attendees } : {}
1060
1280
  };
1061
1281
  }
1062
1282
  async function runCalendarEvents(client, parsed, out) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shipmail-cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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.1.30"
44
+ "shipmail": "0.1.32"
45
45
  },
46
46
  "devDependencies": {
47
47
  "tsup": "^8.4.0",