shipmail-cli 0.6.4 → 0.6.6
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 +32 -6
- package/dist/cli.js +452 -20
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ shipmail status
|
|
|
40
40
|
# Create a domain, retrieve records, and verify DNS
|
|
41
41
|
shipmail domains create example.com
|
|
42
42
|
shipmail domains dns-records dom_123
|
|
43
|
+
shipmail domains health dom_123
|
|
43
44
|
shipmail domains verify dom_123
|
|
44
45
|
|
|
45
46
|
# Create a mailbox
|
|
@@ -100,11 +101,17 @@ shipmail mailboxes sandbox-inbound mbx_123 --from customer@example.com --subject
|
|
|
100
101
|
shipmail inbox list 550e8400-e29b-41d4-a716-446655440000 --folder-role inbox --after 2025-07-20T00:00:00Z --limit 25
|
|
101
102
|
shipmail inbox list 550e8400-e29b-41d4-a716-446655440000 --from billing@vendor.example --subject invoice --has-attachment
|
|
102
103
|
shipmail inbox message 550e8400-e29b-41d4-a716-446655440000 eml_123
|
|
104
|
+
# Enable API-agent hold with a mailbox-management credential.
|
|
105
|
+
shipmail mailboxes update 550e8400-e29b-41d4-a716-446655440000 --agent-security-hold true
|
|
106
|
+
# Use a separate securityReview credential to inspect and release the exact snapshot.
|
|
107
|
+
SHIPMAIL_API_KEY=sm_live_review_... shipmail inbox message 550e8400-e29b-41d4-a716-446655440000 eml_123
|
|
108
|
+
SHIPMAIL_API_KEY=sm_live_review_... shipmail inbox security-review 550e8400-e29b-41d4-a716-446655440000 eml_123 --reason "Reviewed against the customer request" --expected-content-sha256 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
|
|
103
109
|
shipmail inbox threads 550e8400-e29b-41d4-a716-446655440000 --attention-state needs_reply --after 2025-07-20T00:00:00Z --limit 50
|
|
104
110
|
shipmail inbox thread 550e8400-e29b-41d4-a716-446655440000 thr_123
|
|
105
111
|
shipmail inbox draft 550e8400-e29b-41d4-a716-446655440000 thr_123 --expected-version 7 --text "Thanks for the note"
|
|
106
|
-
#
|
|
107
|
-
shipmail inbox
|
|
112
|
+
# Read and approve content_sha256; later edits and stale drafts fail with 409 without delivery.
|
|
113
|
+
shipmail inbox get-draft 550e8400-e29b-41d4-a716-446655440000 thr_123 drf_123
|
|
114
|
+
shipmail inbox send-draft 550e8400-e29b-41d4-a716-446655440000 thr_123 drf_123 --expected-content-sha256 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
|
|
108
115
|
# Save a new message to Drafts for a human to review and send; this never sends
|
|
109
116
|
shipmail inbox compose-draft 550e8400-e29b-41d4-a716-446655440000 --to user@example.com --subject "Kickoff" --text "Draft for review"
|
|
110
117
|
shipmail inbox thread-attention 550e8400-e29b-41d4-a716-446655440000 thr_123 --done true --expected-version 8
|
|
@@ -125,6 +132,8 @@ shipmail newsletters create \
|
|
|
125
132
|
--subject "What shipped in July" \
|
|
126
133
|
--blocks @blocks.json
|
|
127
134
|
shipmail newsletters update nws_123 --subject "Updated subject"
|
|
135
|
+
# Or write the body in Markdown; Shipmail converts it to blocks
|
|
136
|
+
shipmail newsletters update nws_123 --markdown @issue.md
|
|
128
137
|
# A concurrent update exits with conflict (409); fetch the latest newsletter before retrying.
|
|
129
138
|
shipmail newsletters preview nws_123
|
|
130
139
|
shipmail newsletters test-send nws_123 --recipient-email owner@example.com
|
|
@@ -135,6 +144,11 @@ shipmail newsletter-assets upload ./hero.png --content-type image/png
|
|
|
135
144
|
shipmail newsletter-assets upload ./demo.mp4 --content-type video/mp4 --thumbnail ./demo-poster.jpg
|
|
136
145
|
shipmail newsletter-assets register-url https://cdn.shipmail.to/newsletter-images/org_123/hero.png
|
|
137
146
|
|
|
147
|
+
# Welcome new readers with a sequence of emails
|
|
148
|
+
shipmail audiences welcome add-step aud_123 --subject "Welcome" --markdown @welcome.md
|
|
149
|
+
shipmail audiences welcome add-step aud_123 --subject "Our best posts" --markdown @day-3.md --wait-minutes 4320
|
|
150
|
+
shipmail audiences welcome enable aud_123
|
|
151
|
+
|
|
138
152
|
# Manage webhooks
|
|
139
153
|
shipmail webhooks create \
|
|
140
154
|
--url https://example.com/shipmail/webhook \
|
|
@@ -151,6 +165,11 @@ Paragraph, quote, callout, list-item, and column bodies in `--blocks` accept bar
|
|
|
151
165
|
sanitized inline HTML. Use `<p>` and `<br>` for line breaks. Shipmail keeps `a`, `b`, `br`,
|
|
152
166
|
`code`, `em`, `i`, `p`, `s`, `span`, `strong`, and `u`, with safe attributes and URLs.
|
|
153
167
|
|
|
168
|
+
`--markdown` (text, or `@file` to read a file) is an alternative to `--blocks` and `--body-html`.
|
|
169
|
+
Shipmail converts headings, paragraphs, lists, quotes, code, images, links, dividers and tables
|
|
170
|
+
to blocks. A callout starts with a line `:::callout` (optionally `info`, `warning` or `success`,
|
|
171
|
+
then a title) and ends with a line `:::`.
|
|
172
|
+
|
|
154
173
|
`newsletter-assets upload` uses a five-minute direct storage PUT, then calls the single-use
|
|
155
174
|
completion URL. Video uploads require a JPEG poster through `--thumbnail`. `newsletter-assets list`
|
|
156
175
|
includes current newsletter media storage usage and a `pagination.next_cursor` value in the JSON
|
|
@@ -171,17 +190,18 @@ Use repeatable `--header "Name: value"` flags for request headers. Use `--output
|
|
|
171
190
|
- `status`
|
|
172
191
|
- `api <METHOD> <PATH>`
|
|
173
192
|
- `domains list|search|create|get|verify|delete`
|
|
174
|
-
- `mailboxes list|create|get|suspend|resume|forwarding|forwarding-add|forwarding-remove|app-passwords|folders|identities|stage-attachment|prepare-attachment-upload`
|
|
193
|
+
- `mailboxes list|create|get|update|suspend|resume|forwarding|forwarding-add|forwarding-remove|app-passwords|folders|identities|stage-attachment|prepare-attachment-upload`
|
|
175
194
|
- `members list|get`
|
|
176
|
-
- `inbox list|message|threads|thread|thread-state|draft|send-draft|reply-message|reply-thread|update|move`
|
|
195
|
+
- `inbox list|message|security-review|threads|thread|thread-state|draft|send-draft|reply-message|reply-thread|update|move`
|
|
177
196
|
- `reply-scans create|status|results`
|
|
178
197
|
- `messages send|list|analytics|get|reply` (test keys also support `--sandbox-outcome`)
|
|
179
198
|
- `scheduled-messages list|get|update|cancel`
|
|
180
199
|
- `mailboxes sandbox-inbound`
|
|
181
200
|
- `webhooks list|create|update|test|deliveries|replay-delivery`
|
|
182
|
-
- `suppressions list|remove`
|
|
201
|
+
- `suppressions add|list|remove`
|
|
183
202
|
- `audiences list|create|get|update|delete`
|
|
184
203
|
- `audiences feed get|update|rotate|revoke`
|
|
204
|
+
- `audiences welcome get|enable|disable|add-step|update-step|remove-step`
|
|
185
205
|
- `audiences subscribers add|list|get|by-email|update|unsubscribe|resubscribe|remove`
|
|
186
206
|
- `newsletter-sender-identities list`
|
|
187
207
|
- `newsletter-domains list`
|
|
@@ -192,7 +212,7 @@ Member output uses the roles `owner`, `admin`, and `member`. Its `mailbox_access
|
|
|
192
212
|
`selected`; `mailbox_ids` is empty for `all`, while selected IDs may be limited to mailboxes the
|
|
193
213
|
API key can reach.
|
|
194
214
|
|
|
195
|
-
Configure a publisher-owned Atom self URL and the number of recent
|
|
215
|
+
Configure a publisher-owned Atom self URL and the number of recent newsletters:
|
|
196
216
|
|
|
197
217
|
```bash
|
|
198
218
|
shipmail audiences feed update aud_123 \
|
|
@@ -201,3 +221,9 @@ shipmail audiences feed update aud_123 \
|
|
|
201
221
|
```
|
|
202
222
|
|
|
203
223
|
Run `shipmail --help` for flags and examples.
|
|
224
|
+
|
|
225
|
+
## Links
|
|
226
|
+
|
|
227
|
+
- [CLI guide](https://shipmail.to/docs/cli)
|
|
228
|
+
- [Source](https://github.com/shipmail-to/shipmail-node/tree/main/packages/shipmail-cli)
|
|
229
|
+
- [Issues](https://github.com/shipmail-to/shipmail-node/issues)
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
|
7
7
|
import { env as processEnv, exit, stderr, stdout } from "process";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
9
|
import {
|
|
10
|
+
BOOKING_LINK_STATUSES,
|
|
10
11
|
CONFERENCING_PROVIDER_IDS,
|
|
11
12
|
ReconciliationRequiredError,
|
|
12
13
|
ShipmailClient,
|
|
@@ -350,6 +351,7 @@ var OAuthError = class extends Error {
|
|
|
350
351
|
this.code = code;
|
|
351
352
|
this.name = "OAuthError";
|
|
352
353
|
}
|
|
354
|
+
code;
|
|
353
355
|
};
|
|
354
356
|
function trimSlash(value) {
|
|
355
357
|
return value.replace(/\/+$/, "");
|
|
@@ -649,7 +651,7 @@ async function login(options) {
|
|
|
649
651
|
}
|
|
650
652
|
|
|
651
653
|
// src/version.ts
|
|
652
|
-
var VERSION = "0.6.
|
|
654
|
+
var VERSION = "0.6.6";
|
|
653
655
|
|
|
654
656
|
// src/cli.ts
|
|
655
657
|
var DEFAULT_BASE_URL = "https://shipmail.to/api/v1";
|
|
@@ -685,6 +687,8 @@ var CliHttpError = class extends Error {
|
|
|
685
687
|
this.body = body;
|
|
686
688
|
this.name = "CliHttpError";
|
|
687
689
|
}
|
|
690
|
+
status;
|
|
691
|
+
body;
|
|
688
692
|
};
|
|
689
693
|
function parseCliArgs(argv) {
|
|
690
694
|
const positional = [];
|
|
@@ -989,12 +993,27 @@ async function readNewsletterBlocks(value) {
|
|
|
989
993
|
}
|
|
990
994
|
return parsed;
|
|
991
995
|
}
|
|
996
|
+
async function readNewsletterMarkdown(options) {
|
|
997
|
+
const input = getString(options, "markdown");
|
|
998
|
+
if (input === void 0) return void 0;
|
|
999
|
+
const markdown = await readTextInput(input);
|
|
1000
|
+
if (getString(options, "blocks") !== void 0 || getString(options, "body-html") !== void 0) {
|
|
1001
|
+
throw new CliUsageError("Use only one of --blocks, --body-html, or --markdown.");
|
|
1002
|
+
}
|
|
1003
|
+
return markdown;
|
|
1004
|
+
}
|
|
992
1005
|
function parseArchiveVisibility(options) {
|
|
993
1006
|
const value = getString(options, "archive-visibility");
|
|
994
1007
|
if (value === void 0) return void 0;
|
|
995
1008
|
if (value === "private" || value === "public") return value;
|
|
996
1009
|
throw new CliUsageError("--archive-visibility must be private or public.");
|
|
997
1010
|
}
|
|
1011
|
+
function parsePublishTarget(options) {
|
|
1012
|
+
const value = getString(options, "publish-target");
|
|
1013
|
+
if (value === void 0) return void 0;
|
|
1014
|
+
if (value === "email_and_website" || value === "email" || value === "website") return value;
|
|
1015
|
+
throw new CliUsageError("--publish-target must be email_and_website, email or website.");
|
|
1016
|
+
}
|
|
998
1017
|
function parseStylingMode(options) {
|
|
999
1018
|
const value = getString(options, "styling-mode");
|
|
1000
1019
|
if (value === void 0) return void 0;
|
|
@@ -1027,16 +1046,21 @@ async function newsletterCreateParams(options) {
|
|
|
1027
1046
|
const blocks = blocksInput ? await readNewsletterBlocks(blocksInput) : void 0;
|
|
1028
1047
|
const body_html = bodyHtmlInput ? await readTextInput(bodyHtmlInput) : void 0;
|
|
1029
1048
|
const body_text = bodyTextInput ? await readTextInput(bodyTextInput) : void 0;
|
|
1030
|
-
|
|
1031
|
-
|
|
1049
|
+
const content_markdown = await readNewsletterMarkdown(options);
|
|
1050
|
+
if (!blocks?.length && !body_html?.trim() && !body_text?.trim() && !content_markdown?.trim()) {
|
|
1051
|
+
throw new CliUsageError(
|
|
1052
|
+
"Provide at least one of --blocks, --body-html, --body-text, or --markdown."
|
|
1053
|
+
);
|
|
1032
1054
|
}
|
|
1033
1055
|
const send_window_hours = getNumber(options, "send-window-hours");
|
|
1034
1056
|
const archive_visibility = parseArchiveVisibility(options);
|
|
1057
|
+
const publish_target = parsePublishTarget(options);
|
|
1035
1058
|
const feed_entry_url = getString(options, "feed-entry-url");
|
|
1036
1059
|
const styling_mode = parseStylingMode(options);
|
|
1037
1060
|
const track_engagement = getBoolean(options, "track-engagement");
|
|
1038
1061
|
const add_utm_tags = getBoolean(options, "add-utm-tags");
|
|
1039
1062
|
const preview_text = getString(options, "preview-text");
|
|
1063
|
+
const subjectTest = subjectTestParams(options);
|
|
1040
1064
|
return {
|
|
1041
1065
|
audience_id,
|
|
1042
1066
|
sender_identity_id,
|
|
@@ -1044,14 +1068,32 @@ async function newsletterCreateParams(options) {
|
|
|
1044
1068
|
subject,
|
|
1045
1069
|
...preview_text !== void 0 ? { preview_text } : {},
|
|
1046
1070
|
...blocks ? { blocks } : {},
|
|
1071
|
+
...content_markdown !== void 0 ? { content_markdown } : {},
|
|
1047
1072
|
...body_html !== void 0 ? { body_html } : {},
|
|
1048
1073
|
...body_text !== void 0 ? { body_text } : {},
|
|
1049
1074
|
...send_window_hours !== void 0 ? { send_window_hours } : {},
|
|
1050
1075
|
...archive_visibility !== void 0 ? { archive_visibility } : {},
|
|
1076
|
+
...publish_target !== void 0 ? { publish_target } : {},
|
|
1051
1077
|
...feed_entry_url !== void 0 ? { feed_entry_url } : {},
|
|
1052
1078
|
...styling_mode !== void 0 ? { styling_mode } : {},
|
|
1053
1079
|
...track_engagement !== void 0 ? { track_engagement } : {},
|
|
1054
|
-
...add_utm_tags !== void 0 ? { add_utm_tags } : {}
|
|
1080
|
+
...add_utm_tags !== void 0 ? { add_utm_tags } : {},
|
|
1081
|
+
...subjectTest
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function subjectTestParams(options) {
|
|
1085
|
+
const subject_b = getString(options, "subject-b");
|
|
1086
|
+
const clear = getBoolean(options, "clear-subject-b") === true;
|
|
1087
|
+
if (subject_b !== void 0 && clear) {
|
|
1088
|
+
throw new CliUsageError("Use either --subject-b or --clear-subject-b.");
|
|
1089
|
+
}
|
|
1090
|
+
const subject_test_percent = getNumber(options, "subject-test-percent");
|
|
1091
|
+
const subject_test_hours = getNumber(options, "subject-test-hours");
|
|
1092
|
+
return {
|
|
1093
|
+
...subject_b !== void 0 ? { subject_b } : {},
|
|
1094
|
+
...clear ? { subject_b: null } : {},
|
|
1095
|
+
...subject_test_percent !== void 0 ? { subject_test_percent } : {},
|
|
1096
|
+
...subject_test_hours !== void 0 ? { subject_test_hours } : {}
|
|
1055
1097
|
};
|
|
1056
1098
|
}
|
|
1057
1099
|
async function newsletterUpdateParams(options) {
|
|
@@ -1062,6 +1104,7 @@ async function newsletterUpdateParams(options) {
|
|
|
1062
1104
|
const preview_text = getString(options, "preview-text");
|
|
1063
1105
|
const send_window_hours = getNumber(options, "send-window-hours");
|
|
1064
1106
|
const archive_visibility = parseArchiveVisibility(options);
|
|
1107
|
+
const publish_target = parsePublishTarget(options);
|
|
1065
1108
|
const feed_entry_url = getString(options, "feed-entry-url");
|
|
1066
1109
|
const styling_mode = parseStylingMode(options);
|
|
1067
1110
|
const track_engagement = getBoolean(options, "track-engagement");
|
|
@@ -1072,6 +1115,7 @@ async function newsletterUpdateParams(options) {
|
|
|
1072
1115
|
const blocks = blocksInput ? await readNewsletterBlocks(blocksInput) : void 0;
|
|
1073
1116
|
const body_html = bodyHtmlInput ? await readTextInput(bodyHtmlInput) : void 0;
|
|
1074
1117
|
const body_text = bodyTextInput ? await readTextInput(bodyTextInput) : void 0;
|
|
1118
|
+
const content_markdown = await readNewsletterMarkdown(options);
|
|
1075
1119
|
const params = {
|
|
1076
1120
|
...audience_id ? { audience_id } : {},
|
|
1077
1121
|
...sender_identity_id ? { sender_identity_id } : {},
|
|
@@ -1079,14 +1123,17 @@ async function newsletterUpdateParams(options) {
|
|
|
1079
1123
|
...subject ? { subject } : {},
|
|
1080
1124
|
...preview_text !== void 0 ? { preview_text } : {},
|
|
1081
1125
|
...blocks ? { blocks } : {},
|
|
1126
|
+
...content_markdown !== void 0 ? { content_markdown } : {},
|
|
1082
1127
|
...body_html !== void 0 ? { body_html } : {},
|
|
1083
1128
|
...body_text !== void 0 ? { body_text } : {},
|
|
1084
1129
|
...send_window_hours !== void 0 ? { send_window_hours } : {},
|
|
1085
1130
|
...archive_visibility !== void 0 ? { archive_visibility } : {},
|
|
1131
|
+
...publish_target !== void 0 ? { publish_target } : {},
|
|
1086
1132
|
...feed_entry_url !== void 0 ? { feed_entry_url } : {},
|
|
1087
1133
|
...styling_mode !== void 0 ? { styling_mode } : {},
|
|
1088
1134
|
...track_engagement !== void 0 ? { track_engagement } : {},
|
|
1089
|
-
...add_utm_tags !== void 0 ? { add_utm_tags } : {}
|
|
1135
|
+
...add_utm_tags !== void 0 ? { add_utm_tags } : {},
|
|
1136
|
+
...subjectTestParams(options)
|
|
1090
1137
|
};
|
|
1091
1138
|
if (Object.keys(params).length === 0) {
|
|
1092
1139
|
throw new CliUsageError("Provide at least one field to update.");
|
|
@@ -1179,11 +1226,13 @@ Usage:
|
|
|
1179
1226
|
shipmail domains create <name>
|
|
1180
1227
|
shipmail domains get <id>
|
|
1181
1228
|
shipmail domains dns-records <id>
|
|
1229
|
+
shipmail domains health <id> [--json]
|
|
1182
1230
|
shipmail domains verify <id>
|
|
1183
1231
|
shipmail domains delete <id>
|
|
1184
1232
|
shipmail mailboxes list [--domain-id dom] [--limit 25] [--cursor cur]
|
|
1185
1233
|
shipmail mailboxes create --domain-id dom --address hello (--password value | --password-file path | --generate-password) [--display-name "Hello"]
|
|
1186
1234
|
shipmail mailboxes get <id>
|
|
1235
|
+
shipmail mailboxes update <id> [--display-name "Ops"] [--agent-security-hold true|false]
|
|
1187
1236
|
shipmail mailboxes suspend <id>
|
|
1188
1237
|
shipmail mailboxes resume <id>
|
|
1189
1238
|
shipmail mailboxes export <id>
|
|
@@ -1208,12 +1257,17 @@ Usage:
|
|
|
1208
1257
|
shipmail members get <id>
|
|
1209
1258
|
shipmail inbox list <mailbox-id> [--folder-role inbox] [--search text] [--from text] [--to text] [--subject text] [--has-attachment [true|false]] [--after ISO] [--before ISO] [--limit 25] [--cursor cur]
|
|
1210
1259
|
shipmail inbox message <mailbox-id> <message-id>
|
|
1260
|
+
shipmail inbox security-review <mailbox-id> <message-id> --reason "Reviewed" --expected-content-sha256 HASH
|
|
1211
1261
|
shipmail inbox threads <mailbox-id> [--attention-state needs_reply] [--after ISO] [--before ISO] [--limit 25] [--cursor cur]
|
|
1212
1262
|
shipmail inbox thread <mailbox-id> <thread-id>
|
|
1213
1263
|
shipmail inbox thread-attention <mailbox-id> <thread-id> --done true --expected-version 3
|
|
1214
1264
|
shipmail inbox draft <mailbox-id> <thread-id> --expected-version 3 (--text "Hello" | --html "<p>Hello</p>") [--reply-mode reply_all]
|
|
1215
1265
|
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)
|
|
1216
|
-
shipmail inbox
|
|
1266
|
+
shipmail inbox drafts <mailbox-id> <thread-id>
|
|
1267
|
+
shipmail inbox get-draft <mailbox-id> <thread-id> <draft-id>
|
|
1268
|
+
shipmail inbox update-draft <mailbox-id> <thread-id> <draft-id> (--text "Hello" | --html "<p>Hello</p>") (the new body replaces the old one)
|
|
1269
|
+
shipmail inbox delete-draft <mailbox-id> <thread-id> <draft-id>
|
|
1270
|
+
shipmail inbox send-draft <mailbox-id> <thread-id> <draft-id> [--expected-content-sha256 hash]
|
|
1217
1271
|
shipmail inbox reply-message <mailbox-id> <message-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false]
|
|
1218
1272
|
shipmail inbox reply-thread <mailbox-id> <thread-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false]
|
|
1219
1273
|
shipmail inbox update <mailbox-id> <message-id> [--read true] [--starred false]
|
|
@@ -1234,20 +1288,22 @@ Usage:
|
|
|
1234
1288
|
shipmail webhooks create --url https://example.com/hook --events message.received,message.bounced [--authorization "Bearer key"] [--standard-webhooks-secret whsec_...] [--include-reply-text] [--authenticated-senders-only] [--allowed-sender jane@customer.com --allowed-sender customer.com]
|
|
1235
1289
|
shipmail webhooks update <webhook-id> [--url https://example.com/hook] [--events message.received] [--description text] [--active true|false] [--authorization "Bearer key" | --clear-authorization] [--standard-webhooks-secret whsec_... | --clear-standard-webhooks-secret] [--include-reply-text true|false] [--authenticated-senders-only true|false] [--allowed-sender customer.com ... | --clear-allowed-senders]
|
|
1236
1290
|
shipmail webhooks replay-delivery <webhook-id> <delivery-id> [--idempotency-key key]
|
|
1237
|
-
shipmail suppressions
|
|
1291
|
+
shipmail suppressions add <email> [--scope all|marketing]
|
|
1292
|
+
shipmail suppressions list [--scope all|marketing] [--limit 25] [--cursor cur]
|
|
1238
1293
|
shipmail suppressions remove <email>
|
|
1239
1294
|
shipmail newsletters create \\
|
|
1240
1295
|
--audience-id aud_123 \\
|
|
1241
1296
|
--sender-identity-id nwsid_456 \\
|
|
1242
1297
|
--name "July changelog" \\
|
|
1243
1298
|
--subject "What shipped in July" \\
|
|
1244
|
-
--blocks @blocks.json \\
|
|
1245
|
-
[--feed-entry-url https://example.com/post] [--styling-mode styled|plain] [--track-engagement true|false] [--add-utm-tags true|false]
|
|
1246
|
-
shipmail newsletters update nws_789 --subject "Updated subject" --blocks @blocks-v2.json [--feed-entry-url https://example.com/post] [--styling-mode styled|plain] [--track-engagement true|false] [--add-utm-tags true|false]
|
|
1299
|
+
--blocks @blocks.json | --markdown @issue.md \\
|
|
1300
|
+
[--feed-entry-url https://example.com/post] [--styling-mode styled|plain] [--track-engagement true|false] [--add-utm-tags true|false] [--publish-target email_and_website|email|website]
|
|
1301
|
+
shipmail newsletters update nws_789 --subject "Updated subject" [--blocks @blocks-v2.json | --markdown @issue.md] [--feed-entry-url https://example.com/post] [--styling-mode styled|plain] [--track-engagement true|false] [--add-utm-tags true|false] [--subject-b "Second subject" | --clear-subject-b] [--subject-test-percent 1..25] [--subject-test-hours 1..72]
|
|
1247
1302
|
shipmail newsletters preview <id>
|
|
1248
1303
|
shipmail newsletters preflight <id>
|
|
1249
1304
|
shipmail newsletters test-send <id> --recipient-email owner@example.com
|
|
1250
|
-
shipmail newsletters schedule <id> --scheduled-at 2026-08-01T09:00:00.000Z
|
|
1305
|
+
shipmail newsletters schedule <id> --scheduled-at 2026-08-01T09:00:00.000Z [--limit 500]
|
|
1306
|
+
shipmail newsletters send-more <id> [--count 1000]
|
|
1251
1307
|
shipmail newsletters cancel <id>
|
|
1252
1308
|
shipmail newsletters resume <id>
|
|
1253
1309
|
shipmail newsletter-assets list [--kind image|video] [--q text] [--limit 25] [--cursor cur]
|
|
@@ -1258,12 +1314,17 @@ Usage:
|
|
|
1258
1314
|
shipmail audiences list [--limit 25] [--cursor cur]
|
|
1259
1315
|
shipmail audiences create <name> --consent-source "Signup form" [--description "..."]
|
|
1260
1316
|
shipmail audiences get <id>
|
|
1261
|
-
shipmail audiences update <id> [--name "..."] [--description "..."]
|
|
1317
|
+
shipmail audiences update <id> [--name "..."] [--description "..."] [--cleanup true|false] [--cleanup-ignored-issues 2..100] [--cleanup-inactive-days 30..3650]
|
|
1262
1318
|
shipmail audiences delete <id>
|
|
1263
1319
|
shipmail audiences feed get <audience-id>
|
|
1264
1320
|
shipmail audiences feed update <audience-id> [--enabled true|false] [--title "..."] [--subtitle "..."] [--site-url https://...] [--canonical-url https://...] [--author-name "..."] [--icon-url https://...] [--entry-limit 10..100] [--idempotency-key key]
|
|
1265
1321
|
shipmail audiences feed rotate <audience-id> [--idempotency-key key]
|
|
1266
1322
|
shipmail audiences feed revoke <audience-id> [--idempotency-key key]
|
|
1323
|
+
shipmail audiences welcome get <audience-id>
|
|
1324
|
+
shipmail audiences welcome enable|disable <audience-id> [--idempotency-key key]
|
|
1325
|
+
shipmail audiences welcome add-step <audience-id> --subject "Welcome" (--markdown @welcome.md | --blocks @blocks.json) [--wait-minutes 0] [--preview-text "..."] [--sender-identity-id nwsid_456]
|
|
1326
|
+
shipmail audiences welcome update-step <audience-id> <step-id> [--subject "..."] [--markdown @welcome.md | --blocks @blocks.json] [--wait-minutes 1440]
|
|
1327
|
+
shipmail audiences welcome remove-step <audience-id> <step-id>
|
|
1267
1328
|
shipmail audiences subscribers add <audience-id> <email> [--name "Jane"] [--consent-source "..."]
|
|
1268
1329
|
shipmail audiences subscribers list <audience-id> [--status subscribed] [--email jane@x.com] [--limit 25]
|
|
1269
1330
|
shipmail audiences subscribers get <audience-id> <subscriber-id>
|
|
@@ -1283,11 +1344,27 @@ Usage:
|
|
|
1283
1344
|
shipmail booking-pages get <id>
|
|
1284
1345
|
shipmail booking-pages update <id> [--name "..."] [--conferencing-provider zoom|google_meet|none] [--active false]
|
|
1285
1346
|
shipmail booking-pages delete <id>
|
|
1347
|
+
shipmail booking-links list [--status open|booked|expired|revoked] [--limit 25] [--cursor cur]
|
|
1348
|
+
shipmail booking-links create --name "Call with Sam" --mailbox hello@example.com --duration-minutes 30 --timezone Europe/Lisbon --range 2026-11-02T09:00:00Z/2026-11-02T12:00:00Z [--range ...] [--source-page bkp_...] [--max-bookings 1|unlimited] [--expires-at 2026-11-10T00:00:00Z]
|
|
1349
|
+
shipmail booking-links get <id>
|
|
1350
|
+
shipmail booking-links update <id> [--name "..."] [--range start/end|none] [--max-bookings n|unlimited] [--expires-at iso|none] [--source-page id|none]
|
|
1351
|
+
shipmail booking-links revoke <id>
|
|
1286
1352
|
|
|
1287
1353
|
Booking page conferencing:
|
|
1288
1354
|
--conferencing-provider <zoom|google_meet> Create a meeting for every booking.
|
|
1289
1355
|
--conferencing-provider none Stop creating meeting links on update.
|
|
1290
1356
|
|
|
1357
|
+
One-off booking links:
|
|
1358
|
+
--range <start>/<end> A time the link offers, as two ISO instants. Repeat it, or
|
|
1359
|
+
separate ranges with commas. Without ranges, --source-page
|
|
1360
|
+
offers that booking page's weekly hours.
|
|
1361
|
+
--range none On update, drop the ranges and offer the source page's hours.
|
|
1362
|
+
--max-bookings <n|unlimited> The link stops after this many bookings. Defaults to 1.
|
|
1363
|
+
--expires-at <iso|none> The link stops taking bookings at this time.
|
|
1364
|
+
--source-page <id|none> The booking page to copy weekly hours from.
|
|
1365
|
+
Revoking a link stops it at once and cannot be undone. Other flags: --description,
|
|
1366
|
+
--buffer-minutes, --minimum-notice-minutes, --conferencing-provider, --mailbox.
|
|
1367
|
+
|
|
1291
1368
|
Conversation IDs:
|
|
1292
1369
|
<thread-id> accepts a conversation ID (thd_...) or the mail-server thread ID. Store the
|
|
1293
1370
|
conversation_id every response carries. thread_id is deprecated and unchanged.
|
|
@@ -1299,15 +1376,30 @@ Member access output:
|
|
|
1299
1376
|
Newsletter styling:
|
|
1300
1377
|
--styling-mode <styled|plain> styled applies Shipmail's email theme. plain sends your HTML without injected styles, width, or centering, so the reader's email client styles it.
|
|
1301
1378
|
|
|
1379
|
+
Newsletter publish target:
|
|
1380
|
+
--publish-target <email_and_website|email|website> Where the newsletter goes. email_and_website (default) sends it, and its archive visibility decides whether it also appears on the website and feed. email never appears on the web. website is published on the audience's newsletter website and feed at the scheduled time without any email; the website must be on.
|
|
1381
|
+
|
|
1302
1382
|
Newsletter tracking:
|
|
1303
|
-
--track-engagement <true|false> Track opens and clicks for this
|
|
1304
|
-
--add-utm-tags <true|false> Add utm_source, utm_medium and utm_campaign to the
|
|
1383
|
+
--track-engagement <true|false> Track opens and clicks for this newsletter. Off by default. Changes only while the newsletter is a draft or scheduled for later.
|
|
1384
|
+
--add-utm-tags <true|false> Add utm_source, utm_medium and utm_campaign to the newsletter's links in the email and the feed. Off by default. Changes only while the newsletter is a draft or scheduled for later.
|
|
1385
|
+
|
|
1386
|
+
Newsletter subject test:
|
|
1387
|
+
--subject-b <text> Test a second subject. Each subject goes to --subject-test-percent of the subscribers (default 10); after --subject-test-hours (default 4) the one with more human clicks goes to the rest. Needs --track-engagement true.
|
|
1388
|
+
--clear-subject-b Remove the second subject.
|
|
1389
|
+
|
|
1390
|
+
Engaged first:
|
|
1391
|
+
--limit <n> On schedule, send only to the n most engaged subscribers. Send the rest with send-more, which takes --count or sends to everyone left.
|
|
1392
|
+
|
|
1393
|
+
List cleanup:
|
|
1394
|
+
--cleanup <true|false> A cold subscriber gets one last-chance email instead of a newsletter, and becomes dormant if they do not click keep within 14 days.
|
|
1395
|
+
--cleanup-ignored-issues <n> Tracked newsletters ignored in a row before a subscriber is cold (2 to 100, default 6).
|
|
1396
|
+
--cleanup-inactive-days <n> Days without a sign of life before a subscriber is cold (30 to 3650, default 180).
|
|
1305
1397
|
|
|
1306
1398
|
Newsletter block prose:
|
|
1307
1399
|
Paragraph, quote, callout, list-item, and column bodies accept bare text or sanitized inline HTML. Use <p> and <br> for line breaks.
|
|
1308
1400
|
|
|
1309
1401
|
Newsletter feeds:
|
|
1310
|
-
--feed-entry-url <url> Link this
|
|
1402
|
+
--feed-entry-url <url> Link this newsletter's Atom feed title to an http(s) URL instead of its Shipmail archive.
|
|
1311
1403
|
|
|
1312
1404
|
Sign-in:
|
|
1313
1405
|
Run shipmail login once to sign in through the browser instead of using an API key.
|
|
@@ -1557,6 +1649,44 @@ async function replyToMessageParams(options) {
|
|
|
1557
1649
|
} : {}
|
|
1558
1650
|
};
|
|
1559
1651
|
}
|
|
1652
|
+
var DOMAIN_HEALTH_PROVIDER_LABELS = {
|
|
1653
|
+
google: "Gmail",
|
|
1654
|
+
microsoft: "Outlook",
|
|
1655
|
+
yahoo: "Yahoo",
|
|
1656
|
+
apple: "iCloud",
|
|
1657
|
+
other: "Other"
|
|
1658
|
+
};
|
|
1659
|
+
function formatRecipients(count) {
|
|
1660
|
+
return `${count} ${count === 1 ? "recipient" : "recipients"}`;
|
|
1661
|
+
}
|
|
1662
|
+
function formatDeliveryRate(rate) {
|
|
1663
|
+
return rate === null ? "no results yet" : `${rate}% delivered`;
|
|
1664
|
+
}
|
|
1665
|
+
function writeDomainHealth(out, health) {
|
|
1666
|
+
const { dns, blocklist, delivery } = health;
|
|
1667
|
+
const checks = [
|
|
1668
|
+
["MX", dns.mx],
|
|
1669
|
+
["SPF", dns.spf],
|
|
1670
|
+
["DKIM", dns.dkim],
|
|
1671
|
+
["DMARC", dns.dmarc]
|
|
1672
|
+
];
|
|
1673
|
+
const lines = [
|
|
1674
|
+
`${health.domain} (${health.domain_id})`,
|
|
1675
|
+
"",
|
|
1676
|
+
`DNS (checked ${dns.last_checked_at ?? "never"})`,
|
|
1677
|
+
...checks.map(([name, ok]) => ` ${name.padEnd(6)}${ok ? "pass" : "fail"}`),
|
|
1678
|
+
"",
|
|
1679
|
+
`Spamhaus DBL: ${blocklist.listed ? `listed since ${blocklist.listed_since ?? "unknown"}` : "not listed"}`,
|
|
1680
|
+
"",
|
|
1681
|
+
`Delivery, last 30 days: ${formatDeliveryRate(delivery.delivery_rate)}`,
|
|
1682
|
+
` ${formatRecipients(delivery.recipients)}: ${delivery.delivered} delivered, ${delivery.bounced} bounced, ${delivery.deferred} deferred, ${delivery.complained} complained`,
|
|
1683
|
+
...delivery.providers.map(
|
|
1684
|
+
(p) => ` ${DOMAIN_HEALTH_PROVIDER_LABELS[p.provider].padEnd(8)}${formatDeliveryRate(p.delivery_rate)}, ${formatRecipients(p.recipients)}, ${p.bounced} bounced, ${p.deferred} deferred, ${p.complained} complained`
|
|
1685
|
+
)
|
|
1686
|
+
];
|
|
1687
|
+
out.write(`${lines.join("\n")}
|
|
1688
|
+
`);
|
|
1689
|
+
}
|
|
1560
1690
|
async function runDomains(client, parsed, out) {
|
|
1561
1691
|
const subcommand = parsed.positional[1];
|
|
1562
1692
|
if (subcommand === "list") {
|
|
@@ -1594,6 +1724,14 @@ async function runDomains(client, parsed, out) {
|
|
|
1594
1724
|
);
|
|
1595
1725
|
return;
|
|
1596
1726
|
}
|
|
1727
|
+
if (subcommand === "health") {
|
|
1728
|
+
const health = await client.domains.getHealth(
|
|
1729
|
+
requirePositional(parsed.positional, 2, "domain id")
|
|
1730
|
+
);
|
|
1731
|
+
if (getBoolean(parsed.options, "json") === true) writeJson(out, health);
|
|
1732
|
+
else writeDomainHealth(out, health);
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1597
1735
|
if (subcommand === "verify") {
|
|
1598
1736
|
writeJson(
|
|
1599
1737
|
out,
|
|
@@ -1658,6 +1796,21 @@ async function runMailboxes(client, parsed, out) {
|
|
|
1658
1796
|
);
|
|
1659
1797
|
return;
|
|
1660
1798
|
}
|
|
1799
|
+
if (subcommand === "update") {
|
|
1800
|
+
const displayName = getString(parsed.options, "display-name");
|
|
1801
|
+
const agentSecurityHold = getBoolean(parsed.options, "agent-security-hold");
|
|
1802
|
+
if (displayName === void 0 && agentSecurityHold === void 0) {
|
|
1803
|
+
throw new CliUsageError("Provide --display-name or --agent-security-hold.");
|
|
1804
|
+
}
|
|
1805
|
+
writeJson(
|
|
1806
|
+
out,
|
|
1807
|
+
await client.mailboxes.update(requirePositional(parsed.positional, 2, "mailbox id"), {
|
|
1808
|
+
...displayName !== void 0 ? { display_name: displayName } : {},
|
|
1809
|
+
...agentSecurityHold !== void 0 ? { agent_security_hold: agentSecurityHold } : {}
|
|
1810
|
+
})
|
|
1811
|
+
);
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1661
1814
|
if (subcommand === "suspend") {
|
|
1662
1815
|
writeJson(
|
|
1663
1816
|
out,
|
|
@@ -1925,6 +2078,21 @@ async function runInbox(client, parsed, out) {
|
|
|
1925
2078
|
);
|
|
1926
2079
|
return;
|
|
1927
2080
|
}
|
|
2081
|
+
if (subcommand === "security-review") {
|
|
2082
|
+
writeJson(
|
|
2083
|
+
out,
|
|
2084
|
+
await client.mailboxes.releaseInboxMessageSecurity(
|
|
2085
|
+
requirePositional(parsed.positional, 2, "mailbox id"),
|
|
2086
|
+
requirePositional(parsed.positional, 3, "message id"),
|
|
2087
|
+
{
|
|
2088
|
+
decision: "release",
|
|
2089
|
+
reason: getRequiredString(parsed.options, "reason"),
|
|
2090
|
+
expected_content_sha256: getRequiredString(parsed.options, "expected-content-sha256")
|
|
2091
|
+
}
|
|
2092
|
+
)
|
|
2093
|
+
);
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
1928
2096
|
if (subcommand === "threads") {
|
|
1929
2097
|
const attentionState = getString(parsed.options, "attention-state");
|
|
1930
2098
|
const sortBy = getString(parsed.options, "sort-by");
|
|
@@ -2019,13 +2187,64 @@ async function runInbox(client, parsed, out) {
|
|
|
2019
2187
|
);
|
|
2020
2188
|
return;
|
|
2021
2189
|
}
|
|
2190
|
+
if (subcommand === "drafts") {
|
|
2191
|
+
writeJson(
|
|
2192
|
+
out,
|
|
2193
|
+
await client.mailboxes.listInboxReplyDrafts(
|
|
2194
|
+
requirePositional(parsed.positional, 2, "mailbox id"),
|
|
2195
|
+
requirePositional(parsed.positional, 3, "thread id")
|
|
2196
|
+
)
|
|
2197
|
+
);
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
if (subcommand === "get-draft") {
|
|
2201
|
+
writeJson(
|
|
2202
|
+
out,
|
|
2203
|
+
await client.mailboxes.getInboxReplyDraft(
|
|
2204
|
+
requirePositional(parsed.positional, 2, "mailbox id"),
|
|
2205
|
+
requirePositional(parsed.positional, 3, "thread id"),
|
|
2206
|
+
requirePositional(parsed.positional, 4, "draft id")
|
|
2207
|
+
)
|
|
2208
|
+
);
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
if (subcommand === "update-draft") {
|
|
2212
|
+
const text = await readTextInput(getString(parsed.options, "text"));
|
|
2213
|
+
const html = await readTextInput(getString(parsed.options, "html"));
|
|
2214
|
+
if (!text && !html) throw new CliUsageError("Provide --text or --html.");
|
|
2215
|
+
writeJson(
|
|
2216
|
+
out,
|
|
2217
|
+
await client.mailboxes.updateInboxReplyDraft(
|
|
2218
|
+
requirePositional(parsed.positional, 2, "mailbox id"),
|
|
2219
|
+
requirePositional(parsed.positional, 3, "thread id"),
|
|
2220
|
+
requirePositional(parsed.positional, 4, "draft id"),
|
|
2221
|
+
{
|
|
2222
|
+
...text ? { text } : {},
|
|
2223
|
+
...html ? { html } : {}
|
|
2224
|
+
}
|
|
2225
|
+
)
|
|
2226
|
+
);
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
if (subcommand === "delete-draft") {
|
|
2230
|
+
const draftId = requirePositional(parsed.positional, 4, "draft id");
|
|
2231
|
+
await client.mailboxes.deleteInboxReplyDraft(
|
|
2232
|
+
requirePositional(parsed.positional, 2, "mailbox id"),
|
|
2233
|
+
requirePositional(parsed.positional, 3, "thread id"),
|
|
2234
|
+
draftId
|
|
2235
|
+
);
|
|
2236
|
+
writeJson(out, { ok: true, id: draftId });
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2022
2239
|
if (subcommand === "send-draft") {
|
|
2240
|
+
const expectedContentSha256 = getString(parsed.options, "expected-content-sha256");
|
|
2023
2241
|
writeJson(
|
|
2024
2242
|
out,
|
|
2025
2243
|
await client.mailboxes.sendInboxReplyDraft(
|
|
2026
2244
|
requirePositional(parsed.positional, 2, "mailbox id"),
|
|
2027
2245
|
requirePositional(parsed.positional, 3, "thread id"),
|
|
2028
|
-
requirePositional(parsed.positional, 4, "draft id")
|
|
2246
|
+
requirePositional(parsed.positional, 4, "draft id"),
|
|
2247
|
+
expectedContentSha256 ? { expectedContentSha256 } : void 0
|
|
2029
2248
|
)
|
|
2030
2249
|
);
|
|
2031
2250
|
return;
|
|
@@ -2320,14 +2539,27 @@ async function runMembers(client, parsed, out) {
|
|
|
2320
2539
|
}
|
|
2321
2540
|
throw new CliUsageError("Unknown members command.");
|
|
2322
2541
|
}
|
|
2542
|
+
var SUPPRESSION_SCOPES = ["all", "marketing"];
|
|
2323
2543
|
async function runSuppressions(client, parsed, out) {
|
|
2324
2544
|
const subcommand = parsed.positional[1];
|
|
2545
|
+
if (subcommand === "add") {
|
|
2546
|
+
const scope = getEnum(parsed.options, "scope", SUPPRESSION_SCOPES);
|
|
2547
|
+
writeJson(
|
|
2548
|
+
out,
|
|
2549
|
+
await client.suppressions.create({
|
|
2550
|
+
email: requirePositional(parsed.positional, 2, "email"),
|
|
2551
|
+
...scope ? { scope } : {}
|
|
2552
|
+
})
|
|
2553
|
+
);
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2325
2556
|
if (subcommand === "list") {
|
|
2326
2557
|
writeJson(
|
|
2327
2558
|
out,
|
|
2328
2559
|
await client.suppressions.list({
|
|
2329
2560
|
cursor: getString(parsed.options, "cursor"),
|
|
2330
|
-
limit: getNumber(parsed.options, "limit")
|
|
2561
|
+
limit: getNumber(parsed.options, "limit"),
|
|
2562
|
+
scope: getEnum(parsed.options, "scope", SUPPRESSION_SCOPES)
|
|
2331
2563
|
})
|
|
2332
2564
|
);
|
|
2333
2565
|
return;
|
|
@@ -2347,7 +2579,8 @@ var SUBSCRIBER_STATUS_VALUES = [
|
|
|
2347
2579
|
"bounced",
|
|
2348
2580
|
"complained",
|
|
2349
2581
|
"suppressed",
|
|
2350
|
-
"transactional_only"
|
|
2582
|
+
"transactional_only",
|
|
2583
|
+
"dormant"
|
|
2351
2584
|
];
|
|
2352
2585
|
function subscriberListParams(options) {
|
|
2353
2586
|
const email = getString(options, "email");
|
|
@@ -2504,6 +2737,89 @@ async function runAudienceFeed(client, parsed, out) {
|
|
|
2504
2737
|
}
|
|
2505
2738
|
throw new CliUsageError("Unknown audiences feed command.");
|
|
2506
2739
|
}
|
|
2740
|
+
async function welcomeStepParams(options) {
|
|
2741
|
+
const subject = getString(options, "subject");
|
|
2742
|
+
const previewText = getString(options, "preview-text");
|
|
2743
|
+
const waitMinutes = getNumber(options, "wait-minutes");
|
|
2744
|
+
const senderIdentityId = getString(options, "sender-identity-id");
|
|
2745
|
+
const stylingMode = parseStylingMode(options);
|
|
2746
|
+
const trackEngagement = getBoolean(options, "track-engagement");
|
|
2747
|
+
const addUtmTags = getBoolean(options, "add-utm-tags");
|
|
2748
|
+
const blocksInput = getString(options, "blocks");
|
|
2749
|
+
const blocks = blocksInput ? await readNewsletterBlocks(blocksInput) : void 0;
|
|
2750
|
+
const contentMarkdown = await readNewsletterMarkdown(options);
|
|
2751
|
+
if (waitMinutes !== void 0 && (!Number.isInteger(waitMinutes) || waitMinutes < 0)) {
|
|
2752
|
+
throw new CliUsageError("--wait-minutes must be a whole number of minutes, 0 or more.");
|
|
2753
|
+
}
|
|
2754
|
+
return {
|
|
2755
|
+
...subject !== void 0 ? { subject } : {},
|
|
2756
|
+
...previewText !== void 0 ? { preview_text: previewText } : {},
|
|
2757
|
+
...blocks ? { blocks } : {},
|
|
2758
|
+
...contentMarkdown !== void 0 ? { content_markdown: contentMarkdown } : {},
|
|
2759
|
+
...waitMinutes !== void 0 ? { wait_minutes: waitMinutes } : {},
|
|
2760
|
+
...senderIdentityId !== void 0 ? { sender_identity_id: senderIdentityId } : {},
|
|
2761
|
+
...stylingMode !== void 0 ? { styling_mode: stylingMode } : {},
|
|
2762
|
+
...trackEngagement !== void 0 ? { track_engagement: trackEngagement } : {},
|
|
2763
|
+
...addUtmTags !== void 0 ? { add_utm_tags: addUtmTags } : {}
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
async function runAudienceWelcome(client, parsed, out) {
|
|
2767
|
+
const action = parsed.positional[2];
|
|
2768
|
+
const audienceId = requirePositional(parsed.positional, 3, "audience id");
|
|
2769
|
+
if (action === "get") {
|
|
2770
|
+
writeJson(out, await client.audiences.welcomeSequence.get(audienceId));
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
const idempotencyKey = getString(parsed.options, "idempotency-key");
|
|
2774
|
+
const methodOptions = idempotencyKey ? { idempotencyKey } : void 0;
|
|
2775
|
+
if (action === "enable" || action === "disable") {
|
|
2776
|
+
writeJson(
|
|
2777
|
+
out,
|
|
2778
|
+
await client.audiences.welcomeSequence.update(
|
|
2779
|
+
audienceId,
|
|
2780
|
+
{ enabled: action === "enable" },
|
|
2781
|
+
methodOptions
|
|
2782
|
+
)
|
|
2783
|
+
);
|
|
2784
|
+
return;
|
|
2785
|
+
}
|
|
2786
|
+
if (action === "add-step") {
|
|
2787
|
+
const params = await welcomeStepParams(parsed.options);
|
|
2788
|
+
if (!params.subject) throw new CliUsageError("--subject is required.");
|
|
2789
|
+
if (!params.blocks?.length && !params.content_markdown?.trim()) {
|
|
2790
|
+
throw new CliUsageError("Provide --blocks or --markdown.");
|
|
2791
|
+
}
|
|
2792
|
+
writeJson(
|
|
2793
|
+
out,
|
|
2794
|
+
await client.audiences.welcomeSequence.createStep(
|
|
2795
|
+
audienceId,
|
|
2796
|
+
{ ...params, subject: params.subject },
|
|
2797
|
+
methodOptions
|
|
2798
|
+
)
|
|
2799
|
+
);
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
const stepId = requirePositional(parsed.positional, 4, "step id");
|
|
2803
|
+
if (action === "update-step") {
|
|
2804
|
+
const params = await welcomeStepParams(parsed.options);
|
|
2805
|
+
if (Object.keys(params).length === 0) {
|
|
2806
|
+
throw new CliUsageError("Provide at least one field to update.");
|
|
2807
|
+
}
|
|
2808
|
+
writeJson(
|
|
2809
|
+
out,
|
|
2810
|
+
await client.audiences.welcomeSequence.updateStep(audienceId, stepId, params, methodOptions)
|
|
2811
|
+
);
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
if (action === "remove-step") {
|
|
2815
|
+
writeJson(
|
|
2816
|
+
out,
|
|
2817
|
+
await client.audiences.welcomeSequence.deleteStep(audienceId, stepId, methodOptions)
|
|
2818
|
+
);
|
|
2819
|
+
return;
|
|
2820
|
+
}
|
|
2821
|
+
throw new CliUsageError("Unknown audiences welcome command.");
|
|
2822
|
+
}
|
|
2507
2823
|
async function runAudiences(client, parsed, out) {
|
|
2508
2824
|
const subcommand = parsed.positional[1];
|
|
2509
2825
|
if (subcommand === "subscribers") {
|
|
@@ -2514,6 +2830,10 @@ async function runAudiences(client, parsed, out) {
|
|
|
2514
2830
|
await runAudienceFeed(client, parsed, out);
|
|
2515
2831
|
return;
|
|
2516
2832
|
}
|
|
2833
|
+
if (subcommand === "welcome") {
|
|
2834
|
+
await runAudienceWelcome(client, parsed, out);
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2517
2837
|
if (subcommand === "list") {
|
|
2518
2838
|
writeJson(
|
|
2519
2839
|
out,
|
|
@@ -2547,11 +2867,17 @@ async function runAudiences(client, parsed, out) {
|
|
|
2547
2867
|
const id = requirePositional(parsed.positional, 2, "audience id");
|
|
2548
2868
|
const name = getString(parsed.options, "name");
|
|
2549
2869
|
const description = getString(parsed.options, "description");
|
|
2870
|
+
const cleanup_enabled = getBoolean(parsed.options, "cleanup");
|
|
2871
|
+
const cleanup_ignored_issues = getNumber(parsed.options, "cleanup-ignored-issues");
|
|
2872
|
+
const cleanup_inactive_days = getNumber(parsed.options, "cleanup-inactive-days");
|
|
2550
2873
|
writeJson(
|
|
2551
2874
|
out,
|
|
2552
2875
|
await client.audiences.update(id, {
|
|
2553
2876
|
...name ? { name } : {},
|
|
2554
|
-
...description ? { description } : {}
|
|
2877
|
+
...description ? { description } : {},
|
|
2878
|
+
...cleanup_enabled !== void 0 ? { cleanup_enabled } : {},
|
|
2879
|
+
...cleanup_ignored_issues !== void 0 ? { cleanup_ignored_issues } : {},
|
|
2880
|
+
...cleanup_inactive_days !== void 0 ? { cleanup_inactive_days } : {}
|
|
2555
2881
|
})
|
|
2556
2882
|
);
|
|
2557
2883
|
return;
|
|
@@ -2630,14 +2956,27 @@ async function runNewsletters(client, parsed, out) {
|
|
|
2630
2956
|
return;
|
|
2631
2957
|
}
|
|
2632
2958
|
if (subcommand === "schedule") {
|
|
2959
|
+
const limit = getNumber(parsed.options, "limit");
|
|
2633
2960
|
writeJson(
|
|
2634
2961
|
out,
|
|
2635
2962
|
await client.newsletters.schedule(requirePositional(parsed.positional, 2, "newsletter id"), {
|
|
2636
|
-
scheduled_at: getRequiredString(parsed.options, "scheduled-at")
|
|
2963
|
+
scheduled_at: getRequiredString(parsed.options, "scheduled-at"),
|
|
2964
|
+
...limit !== void 0 ? { limit } : {}
|
|
2637
2965
|
})
|
|
2638
2966
|
);
|
|
2639
2967
|
return;
|
|
2640
2968
|
}
|
|
2969
|
+
if (subcommand === "send-more") {
|
|
2970
|
+
const count = getNumber(parsed.options, "count");
|
|
2971
|
+
writeJson(
|
|
2972
|
+
out,
|
|
2973
|
+
await client.newsletters.sendToMore(
|
|
2974
|
+
requirePositional(parsed.positional, 2, "newsletter id"),
|
|
2975
|
+
count !== void 0 ? { count } : {}
|
|
2976
|
+
)
|
|
2977
|
+
);
|
|
2978
|
+
return;
|
|
2979
|
+
}
|
|
2641
2980
|
if (subcommand === "cancel") {
|
|
2642
2981
|
writeJson(
|
|
2643
2982
|
out,
|
|
@@ -2957,6 +3296,93 @@ async function runCalendar(client, parsed, out) {
|
|
|
2957
3296
|
}
|
|
2958
3297
|
throw new CliUsageError("Unknown calendar command.");
|
|
2959
3298
|
}
|
|
3299
|
+
function parseBookingLinkRanges(options) {
|
|
3300
|
+
const values = getStringList(options, "range");
|
|
3301
|
+
if (values.length === 0) return void 0;
|
|
3302
|
+
if (values.length === 1 && values[0] === "none") return null;
|
|
3303
|
+
return values.map((value) => {
|
|
3304
|
+
const [start, end, extra] = value.split("/");
|
|
3305
|
+
if (!start || !end || extra !== void 0) {
|
|
3306
|
+
throw new CliUsageError("--range must be <start>/<end>, two ISO 8601 instants.");
|
|
3307
|
+
}
|
|
3308
|
+
return { start, end };
|
|
3309
|
+
});
|
|
3310
|
+
}
|
|
3311
|
+
function getMaxBookings(options) {
|
|
3312
|
+
if (getString(options, "max-bookings") === "unlimited") return null;
|
|
3313
|
+
return getNumber(options, "max-bookings");
|
|
3314
|
+
}
|
|
3315
|
+
function getNullableString(options, key) {
|
|
3316
|
+
const value = getString(options, key);
|
|
3317
|
+
return value === "none" ? null : value;
|
|
3318
|
+
}
|
|
3319
|
+
async function runBookingLinks(client, parsed, out) {
|
|
3320
|
+
const subcommand = parsed.positional[1];
|
|
3321
|
+
const options = parsed.options;
|
|
3322
|
+
if (subcommand === "list") {
|
|
3323
|
+
writeJson(
|
|
3324
|
+
out,
|
|
3325
|
+
await client.bookingLinks.list({
|
|
3326
|
+
cursor: getString(options, "cursor"),
|
|
3327
|
+
limit: getNumber(options, "limit"),
|
|
3328
|
+
status: getEnum(options, "status", BOOKING_LINK_STATUSES)
|
|
3329
|
+
})
|
|
3330
|
+
);
|
|
3331
|
+
return;
|
|
3332
|
+
}
|
|
3333
|
+
if (subcommand === "create") {
|
|
3334
|
+
const ranges = parseBookingLinkRanges(options);
|
|
3335
|
+
if (ranges === null) throw new CliUsageError("--range none only applies to update.");
|
|
3336
|
+
const params = {
|
|
3337
|
+
name: getRequiredString(options, "name"),
|
|
3338
|
+
mailbox: getRequiredString(options, "mailbox"),
|
|
3339
|
+
duration_minutes: requireNumber(options, "duration-minutes"),
|
|
3340
|
+
timezone: getRequiredString(options, "timezone"),
|
|
3341
|
+
offered_ranges: ranges,
|
|
3342
|
+
source_page_id: getString(options, "source-page"),
|
|
3343
|
+
description: getString(options, "description"),
|
|
3344
|
+
buffer_minutes: getNumber(options, "buffer-minutes"),
|
|
3345
|
+
minimum_notice_minutes: getNumber(options, "minimum-notice-minutes"),
|
|
3346
|
+
conferencing_provider: getConferencingProvider(options),
|
|
3347
|
+
max_bookings: getMaxBookings(options),
|
|
3348
|
+
expires_at: getString(options, "expires-at")
|
|
3349
|
+
};
|
|
3350
|
+
writeJson(out, await client.bookingLinks.create(params));
|
|
3351
|
+
return;
|
|
3352
|
+
}
|
|
3353
|
+
if (subcommand === "get") {
|
|
3354
|
+
writeJson(
|
|
3355
|
+
out,
|
|
3356
|
+
await client.bookingLinks.get(requirePositional(parsed.positional, 2, "booking link id"))
|
|
3357
|
+
);
|
|
3358
|
+
return;
|
|
3359
|
+
}
|
|
3360
|
+
if (subcommand === "update") {
|
|
3361
|
+
const id = requirePositional(parsed.positional, 2, "booking link id");
|
|
3362
|
+
const params = {
|
|
3363
|
+
name: getString(options, "name"),
|
|
3364
|
+
mailbox: getString(options, "mailbox"),
|
|
3365
|
+
duration_minutes: getNumber(options, "duration-minutes"),
|
|
3366
|
+
timezone: getString(options, "timezone"),
|
|
3367
|
+
offered_ranges: parseBookingLinkRanges(options),
|
|
3368
|
+
source_page_id: getNullableString(options, "source-page"),
|
|
3369
|
+
description: getString(options, "description"),
|
|
3370
|
+
buffer_minutes: getNumber(options, "buffer-minutes"),
|
|
3371
|
+
minimum_notice_minutes: getNumber(options, "minimum-notice-minutes"),
|
|
3372
|
+
conferencing_provider: getConferencingProvider(options),
|
|
3373
|
+
max_bookings: getMaxBookings(options),
|
|
3374
|
+
expires_at: getNullableString(options, "expires-at")
|
|
3375
|
+
};
|
|
3376
|
+
writeJson(out, await client.bookingLinks.update(id, params));
|
|
3377
|
+
return;
|
|
3378
|
+
}
|
|
3379
|
+
if (subcommand === "revoke") {
|
|
3380
|
+
const id = requirePositional(parsed.positional, 2, "booking link id");
|
|
3381
|
+
writeJson(out, await client.bookingLinks.revoke(id));
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
throw new CliUsageError("Unknown booking-links command.");
|
|
3385
|
+
}
|
|
2960
3386
|
async function runBookingPages(client, parsed, out) {
|
|
2961
3387
|
const subcommand = parsed.positional[1];
|
|
2962
3388
|
const options = parsed.options;
|
|
@@ -3097,6 +3523,11 @@ ${JSON.stringify(err.body, null, 2)}
|
|
|
3097
3523
|
if (shipmailError.actionUrl) {
|
|
3098
3524
|
const reason = shipmailError.reason ? ` (${shipmailError.reason})` : "";
|
|
3099
3525
|
errOut.write(`Ask the organization owner to open${reason}: ${shipmailError.actionUrl}
|
|
3526
|
+
`);
|
|
3527
|
+
}
|
|
3528
|
+
if (shipmailError.limit) {
|
|
3529
|
+
const { resource, used, limit, scope } = shipmailError.limit;
|
|
3530
|
+
errOut.write(`Limit: ${resource} ${used} of ${limit} (${scope})
|
|
3100
3531
|
`);
|
|
3101
3532
|
}
|
|
3102
3533
|
return;
|
|
@@ -3181,6 +3612,7 @@ async function runCli(argv, options = {}) {
|
|
|
3181
3612
|
else if (command === "audiences") await runAudiences(client, parsed, out);
|
|
3182
3613
|
else if (command === "calendar") await runCalendar(client, parsed, out);
|
|
3183
3614
|
else if (command === "booking-pages") await runBookingPages(client, parsed, out);
|
|
3615
|
+
else if (command === "booking-links") await runBookingLinks(client, parsed, out);
|
|
3184
3616
|
else throw new CliUsageError(`Unknown command: ${command}.`);
|
|
3185
3617
|
return 0;
|
|
3186
3618
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shipmail-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "Official CLI for the Shipmail API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,9 +31,12 @@
|
|
|
31
31
|
"homepage": "https://shipmail.to/docs/cli",
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/shipmail-to/
|
|
34
|
+
"url": "git+https://github.com/shipmail-to/shipmail-node.git",
|
|
35
35
|
"directory": "packages/shipmail-cli"
|
|
36
36
|
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/shipmail-to/shipmail-node/issues"
|
|
39
|
+
},
|
|
37
40
|
"engines": {
|
|
38
41
|
"node": ">=18"
|
|
39
42
|
},
|
|
@@ -41,7 +44,7 @@
|
|
|
41
44
|
"access": "public"
|
|
42
45
|
},
|
|
43
46
|
"dependencies": {
|
|
44
|
-
"shipmail": "0.6.
|
|
47
|
+
"shipmail": "0.6.7",
|
|
45
48
|
"zod": "^4.6.5"
|
|
46
49
|
},
|
|
47
50
|
"devDependencies": {
|