shipmail-cli 0.6.4 → 0.6.5
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 +26 -6
- package/dist/cli.js +449 -20
- package/package.json +2 -2
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 \
|
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,
|
|
@@ -649,7 +650,7 @@ async function login(options) {
|
|
|
649
650
|
}
|
|
650
651
|
|
|
651
652
|
// src/version.ts
|
|
652
|
-
var VERSION = "0.6.
|
|
653
|
+
var VERSION = "0.6.5";
|
|
653
654
|
|
|
654
655
|
// src/cli.ts
|
|
655
656
|
var DEFAULT_BASE_URL = "https://shipmail.to/api/v1";
|
|
@@ -989,12 +990,27 @@ async function readNewsletterBlocks(value) {
|
|
|
989
990
|
}
|
|
990
991
|
return parsed;
|
|
991
992
|
}
|
|
993
|
+
async function readNewsletterMarkdown(options) {
|
|
994
|
+
const input = getString(options, "markdown");
|
|
995
|
+
if (input === void 0) return void 0;
|
|
996
|
+
const markdown = await readTextInput(input);
|
|
997
|
+
if (getString(options, "blocks") !== void 0 || getString(options, "body-html") !== void 0) {
|
|
998
|
+
throw new CliUsageError("Use only one of --blocks, --body-html, or --markdown.");
|
|
999
|
+
}
|
|
1000
|
+
return markdown;
|
|
1001
|
+
}
|
|
992
1002
|
function parseArchiveVisibility(options) {
|
|
993
1003
|
const value = getString(options, "archive-visibility");
|
|
994
1004
|
if (value === void 0) return void 0;
|
|
995
1005
|
if (value === "private" || value === "public") return value;
|
|
996
1006
|
throw new CliUsageError("--archive-visibility must be private or public.");
|
|
997
1007
|
}
|
|
1008
|
+
function parsePublishTarget(options) {
|
|
1009
|
+
const value = getString(options, "publish-target");
|
|
1010
|
+
if (value === void 0) return void 0;
|
|
1011
|
+
if (value === "email_and_website" || value === "email" || value === "website") return value;
|
|
1012
|
+
throw new CliUsageError("--publish-target must be email_and_website, email or website.");
|
|
1013
|
+
}
|
|
998
1014
|
function parseStylingMode(options) {
|
|
999
1015
|
const value = getString(options, "styling-mode");
|
|
1000
1016
|
if (value === void 0) return void 0;
|
|
@@ -1027,16 +1043,21 @@ async function newsletterCreateParams(options) {
|
|
|
1027
1043
|
const blocks = blocksInput ? await readNewsletterBlocks(blocksInput) : void 0;
|
|
1028
1044
|
const body_html = bodyHtmlInput ? await readTextInput(bodyHtmlInput) : void 0;
|
|
1029
1045
|
const body_text = bodyTextInput ? await readTextInput(bodyTextInput) : void 0;
|
|
1030
|
-
|
|
1031
|
-
|
|
1046
|
+
const content_markdown = await readNewsletterMarkdown(options);
|
|
1047
|
+
if (!blocks?.length && !body_html?.trim() && !body_text?.trim() && !content_markdown?.trim()) {
|
|
1048
|
+
throw new CliUsageError(
|
|
1049
|
+
"Provide at least one of --blocks, --body-html, --body-text, or --markdown."
|
|
1050
|
+
);
|
|
1032
1051
|
}
|
|
1033
1052
|
const send_window_hours = getNumber(options, "send-window-hours");
|
|
1034
1053
|
const archive_visibility = parseArchiveVisibility(options);
|
|
1054
|
+
const publish_target = parsePublishTarget(options);
|
|
1035
1055
|
const feed_entry_url = getString(options, "feed-entry-url");
|
|
1036
1056
|
const styling_mode = parseStylingMode(options);
|
|
1037
1057
|
const track_engagement = getBoolean(options, "track-engagement");
|
|
1038
1058
|
const add_utm_tags = getBoolean(options, "add-utm-tags");
|
|
1039
1059
|
const preview_text = getString(options, "preview-text");
|
|
1060
|
+
const subjectTest = subjectTestParams(options);
|
|
1040
1061
|
return {
|
|
1041
1062
|
audience_id,
|
|
1042
1063
|
sender_identity_id,
|
|
@@ -1044,14 +1065,32 @@ async function newsletterCreateParams(options) {
|
|
|
1044
1065
|
subject,
|
|
1045
1066
|
...preview_text !== void 0 ? { preview_text } : {},
|
|
1046
1067
|
...blocks ? { blocks } : {},
|
|
1068
|
+
...content_markdown !== void 0 ? { content_markdown } : {},
|
|
1047
1069
|
...body_html !== void 0 ? { body_html } : {},
|
|
1048
1070
|
...body_text !== void 0 ? { body_text } : {},
|
|
1049
1071
|
...send_window_hours !== void 0 ? { send_window_hours } : {},
|
|
1050
1072
|
...archive_visibility !== void 0 ? { archive_visibility } : {},
|
|
1073
|
+
...publish_target !== void 0 ? { publish_target } : {},
|
|
1051
1074
|
...feed_entry_url !== void 0 ? { feed_entry_url } : {},
|
|
1052
1075
|
...styling_mode !== void 0 ? { styling_mode } : {},
|
|
1053
1076
|
...track_engagement !== void 0 ? { track_engagement } : {},
|
|
1054
|
-
...add_utm_tags !== void 0 ? { add_utm_tags } : {}
|
|
1077
|
+
...add_utm_tags !== void 0 ? { add_utm_tags } : {},
|
|
1078
|
+
...subjectTest
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
function subjectTestParams(options) {
|
|
1082
|
+
const subject_b = getString(options, "subject-b");
|
|
1083
|
+
const clear = getBoolean(options, "clear-subject-b") === true;
|
|
1084
|
+
if (subject_b !== void 0 && clear) {
|
|
1085
|
+
throw new CliUsageError("Use either --subject-b or --clear-subject-b.");
|
|
1086
|
+
}
|
|
1087
|
+
const subject_test_percent = getNumber(options, "subject-test-percent");
|
|
1088
|
+
const subject_test_hours = getNumber(options, "subject-test-hours");
|
|
1089
|
+
return {
|
|
1090
|
+
...subject_b !== void 0 ? { subject_b } : {},
|
|
1091
|
+
...clear ? { subject_b: null } : {},
|
|
1092
|
+
...subject_test_percent !== void 0 ? { subject_test_percent } : {},
|
|
1093
|
+
...subject_test_hours !== void 0 ? { subject_test_hours } : {}
|
|
1055
1094
|
};
|
|
1056
1095
|
}
|
|
1057
1096
|
async function newsletterUpdateParams(options) {
|
|
@@ -1062,6 +1101,7 @@ async function newsletterUpdateParams(options) {
|
|
|
1062
1101
|
const preview_text = getString(options, "preview-text");
|
|
1063
1102
|
const send_window_hours = getNumber(options, "send-window-hours");
|
|
1064
1103
|
const archive_visibility = parseArchiveVisibility(options);
|
|
1104
|
+
const publish_target = parsePublishTarget(options);
|
|
1065
1105
|
const feed_entry_url = getString(options, "feed-entry-url");
|
|
1066
1106
|
const styling_mode = parseStylingMode(options);
|
|
1067
1107
|
const track_engagement = getBoolean(options, "track-engagement");
|
|
@@ -1072,6 +1112,7 @@ async function newsletterUpdateParams(options) {
|
|
|
1072
1112
|
const blocks = blocksInput ? await readNewsletterBlocks(blocksInput) : void 0;
|
|
1073
1113
|
const body_html = bodyHtmlInput ? await readTextInput(bodyHtmlInput) : void 0;
|
|
1074
1114
|
const body_text = bodyTextInput ? await readTextInput(bodyTextInput) : void 0;
|
|
1115
|
+
const content_markdown = await readNewsletterMarkdown(options);
|
|
1075
1116
|
const params = {
|
|
1076
1117
|
...audience_id ? { audience_id } : {},
|
|
1077
1118
|
...sender_identity_id ? { sender_identity_id } : {},
|
|
@@ -1079,14 +1120,17 @@ async function newsletterUpdateParams(options) {
|
|
|
1079
1120
|
...subject ? { subject } : {},
|
|
1080
1121
|
...preview_text !== void 0 ? { preview_text } : {},
|
|
1081
1122
|
...blocks ? { blocks } : {},
|
|
1123
|
+
...content_markdown !== void 0 ? { content_markdown } : {},
|
|
1082
1124
|
...body_html !== void 0 ? { body_html } : {},
|
|
1083
1125
|
...body_text !== void 0 ? { body_text } : {},
|
|
1084
1126
|
...send_window_hours !== void 0 ? { send_window_hours } : {},
|
|
1085
1127
|
...archive_visibility !== void 0 ? { archive_visibility } : {},
|
|
1128
|
+
...publish_target !== void 0 ? { publish_target } : {},
|
|
1086
1129
|
...feed_entry_url !== void 0 ? { feed_entry_url } : {},
|
|
1087
1130
|
...styling_mode !== void 0 ? { styling_mode } : {},
|
|
1088
1131
|
...track_engagement !== void 0 ? { track_engagement } : {},
|
|
1089
|
-
...add_utm_tags !== void 0 ? { add_utm_tags } : {}
|
|
1132
|
+
...add_utm_tags !== void 0 ? { add_utm_tags } : {},
|
|
1133
|
+
...subjectTestParams(options)
|
|
1090
1134
|
};
|
|
1091
1135
|
if (Object.keys(params).length === 0) {
|
|
1092
1136
|
throw new CliUsageError("Provide at least one field to update.");
|
|
@@ -1179,11 +1223,13 @@ Usage:
|
|
|
1179
1223
|
shipmail domains create <name>
|
|
1180
1224
|
shipmail domains get <id>
|
|
1181
1225
|
shipmail domains dns-records <id>
|
|
1226
|
+
shipmail domains health <id> [--json]
|
|
1182
1227
|
shipmail domains verify <id>
|
|
1183
1228
|
shipmail domains delete <id>
|
|
1184
1229
|
shipmail mailboxes list [--domain-id dom] [--limit 25] [--cursor cur]
|
|
1185
1230
|
shipmail mailboxes create --domain-id dom --address hello (--password value | --password-file path | --generate-password) [--display-name "Hello"]
|
|
1186
1231
|
shipmail mailboxes get <id>
|
|
1232
|
+
shipmail mailboxes update <id> [--display-name "Ops"] [--agent-security-hold true|false]
|
|
1187
1233
|
shipmail mailboxes suspend <id>
|
|
1188
1234
|
shipmail mailboxes resume <id>
|
|
1189
1235
|
shipmail mailboxes export <id>
|
|
@@ -1208,12 +1254,17 @@ Usage:
|
|
|
1208
1254
|
shipmail members get <id>
|
|
1209
1255
|
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
1256
|
shipmail inbox message <mailbox-id> <message-id>
|
|
1257
|
+
shipmail inbox security-review <mailbox-id> <message-id> --reason "Reviewed" --expected-content-sha256 HASH
|
|
1211
1258
|
shipmail inbox threads <mailbox-id> [--attention-state needs_reply] [--after ISO] [--before ISO] [--limit 25] [--cursor cur]
|
|
1212
1259
|
shipmail inbox thread <mailbox-id> <thread-id>
|
|
1213
1260
|
shipmail inbox thread-attention <mailbox-id> <thread-id> --done true --expected-version 3
|
|
1214
1261
|
shipmail inbox draft <mailbox-id> <thread-id> --expected-version 3 (--text "Hello" | --html "<p>Hello</p>") [--reply-mode reply_all]
|
|
1215
1262
|
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
|
|
1263
|
+
shipmail inbox drafts <mailbox-id> <thread-id>
|
|
1264
|
+
shipmail inbox get-draft <mailbox-id> <thread-id> <draft-id>
|
|
1265
|
+
shipmail inbox update-draft <mailbox-id> <thread-id> <draft-id> (--text "Hello" | --html "<p>Hello</p>") (the new body replaces the old one)
|
|
1266
|
+
shipmail inbox delete-draft <mailbox-id> <thread-id> <draft-id>
|
|
1267
|
+
shipmail inbox send-draft <mailbox-id> <thread-id> <draft-id> [--expected-content-sha256 hash]
|
|
1217
1268
|
shipmail inbox reply-message <mailbox-id> <message-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false]
|
|
1218
1269
|
shipmail inbox reply-thread <mailbox-id> <thread-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false]
|
|
1219
1270
|
shipmail inbox update <mailbox-id> <message-id> [--read true] [--starred false]
|
|
@@ -1234,20 +1285,22 @@ Usage:
|
|
|
1234
1285
|
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
1286
|
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
1287
|
shipmail webhooks replay-delivery <webhook-id> <delivery-id> [--idempotency-key key]
|
|
1237
|
-
shipmail suppressions
|
|
1288
|
+
shipmail suppressions add <email> [--scope all|marketing]
|
|
1289
|
+
shipmail suppressions list [--scope all|marketing] [--limit 25] [--cursor cur]
|
|
1238
1290
|
shipmail suppressions remove <email>
|
|
1239
1291
|
shipmail newsletters create \\
|
|
1240
1292
|
--audience-id aud_123 \\
|
|
1241
1293
|
--sender-identity-id nwsid_456 \\
|
|
1242
1294
|
--name "July changelog" \\
|
|
1243
1295
|
--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]
|
|
1296
|
+
--blocks @blocks.json | --markdown @issue.md \\
|
|
1297
|
+
[--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]
|
|
1298
|
+
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
1299
|
shipmail newsletters preview <id>
|
|
1248
1300
|
shipmail newsletters preflight <id>
|
|
1249
1301
|
shipmail newsletters test-send <id> --recipient-email owner@example.com
|
|
1250
|
-
shipmail newsletters schedule <id> --scheduled-at 2026-08-01T09:00:00.000Z
|
|
1302
|
+
shipmail newsletters schedule <id> --scheduled-at 2026-08-01T09:00:00.000Z [--limit 500]
|
|
1303
|
+
shipmail newsletters send-more <id> [--count 1000]
|
|
1251
1304
|
shipmail newsletters cancel <id>
|
|
1252
1305
|
shipmail newsletters resume <id>
|
|
1253
1306
|
shipmail newsletter-assets list [--kind image|video] [--q text] [--limit 25] [--cursor cur]
|
|
@@ -1258,12 +1311,17 @@ Usage:
|
|
|
1258
1311
|
shipmail audiences list [--limit 25] [--cursor cur]
|
|
1259
1312
|
shipmail audiences create <name> --consent-source "Signup form" [--description "..."]
|
|
1260
1313
|
shipmail audiences get <id>
|
|
1261
|
-
shipmail audiences update <id> [--name "..."] [--description "..."]
|
|
1314
|
+
shipmail audiences update <id> [--name "..."] [--description "..."] [--cleanup true|false] [--cleanup-ignored-issues 2..100] [--cleanup-inactive-days 30..3650]
|
|
1262
1315
|
shipmail audiences delete <id>
|
|
1263
1316
|
shipmail audiences feed get <audience-id>
|
|
1264
1317
|
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
1318
|
shipmail audiences feed rotate <audience-id> [--idempotency-key key]
|
|
1266
1319
|
shipmail audiences feed revoke <audience-id> [--idempotency-key key]
|
|
1320
|
+
shipmail audiences welcome get <audience-id>
|
|
1321
|
+
shipmail audiences welcome enable|disable <audience-id> [--idempotency-key key]
|
|
1322
|
+
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]
|
|
1323
|
+
shipmail audiences welcome update-step <audience-id> <step-id> [--subject "..."] [--markdown @welcome.md | --blocks @blocks.json] [--wait-minutes 1440]
|
|
1324
|
+
shipmail audiences welcome remove-step <audience-id> <step-id>
|
|
1267
1325
|
shipmail audiences subscribers add <audience-id> <email> [--name "Jane"] [--consent-source "..."]
|
|
1268
1326
|
shipmail audiences subscribers list <audience-id> [--status subscribed] [--email jane@x.com] [--limit 25]
|
|
1269
1327
|
shipmail audiences subscribers get <audience-id> <subscriber-id>
|
|
@@ -1283,11 +1341,27 @@ Usage:
|
|
|
1283
1341
|
shipmail booking-pages get <id>
|
|
1284
1342
|
shipmail booking-pages update <id> [--name "..."] [--conferencing-provider zoom|google_meet|none] [--active false]
|
|
1285
1343
|
shipmail booking-pages delete <id>
|
|
1344
|
+
shipmail booking-links list [--status open|booked|expired|revoked] [--limit 25] [--cursor cur]
|
|
1345
|
+
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]
|
|
1346
|
+
shipmail booking-links get <id>
|
|
1347
|
+
shipmail booking-links update <id> [--name "..."] [--range start/end|none] [--max-bookings n|unlimited] [--expires-at iso|none] [--source-page id|none]
|
|
1348
|
+
shipmail booking-links revoke <id>
|
|
1286
1349
|
|
|
1287
1350
|
Booking page conferencing:
|
|
1288
1351
|
--conferencing-provider <zoom|google_meet> Create a meeting for every booking.
|
|
1289
1352
|
--conferencing-provider none Stop creating meeting links on update.
|
|
1290
1353
|
|
|
1354
|
+
One-off booking links:
|
|
1355
|
+
--range <start>/<end> A time the link offers, as two ISO instants. Repeat it, or
|
|
1356
|
+
separate ranges with commas. Without ranges, --source-page
|
|
1357
|
+
offers that booking page's weekly hours.
|
|
1358
|
+
--range none On update, drop the ranges and offer the source page's hours.
|
|
1359
|
+
--max-bookings <n|unlimited> The link stops after this many bookings. Defaults to 1.
|
|
1360
|
+
--expires-at <iso|none> The link stops taking bookings at this time.
|
|
1361
|
+
--source-page <id|none> The booking page to copy weekly hours from.
|
|
1362
|
+
Revoking a link stops it at once and cannot be undone. Other flags: --description,
|
|
1363
|
+
--buffer-minutes, --minimum-notice-minutes, --conferencing-provider, --mailbox.
|
|
1364
|
+
|
|
1291
1365
|
Conversation IDs:
|
|
1292
1366
|
<thread-id> accepts a conversation ID (thd_...) or the mail-server thread ID. Store the
|
|
1293
1367
|
conversation_id every response carries. thread_id is deprecated and unchanged.
|
|
@@ -1299,15 +1373,30 @@ Member access output:
|
|
|
1299
1373
|
Newsletter styling:
|
|
1300
1374
|
--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
1375
|
|
|
1376
|
+
Newsletter publish target:
|
|
1377
|
+
--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.
|
|
1378
|
+
|
|
1302
1379
|
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
|
|
1380
|
+
--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.
|
|
1381
|
+
--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.
|
|
1382
|
+
|
|
1383
|
+
Newsletter subject test:
|
|
1384
|
+
--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.
|
|
1385
|
+
--clear-subject-b Remove the second subject.
|
|
1386
|
+
|
|
1387
|
+
Engaged first:
|
|
1388
|
+
--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.
|
|
1389
|
+
|
|
1390
|
+
List cleanup:
|
|
1391
|
+
--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.
|
|
1392
|
+
--cleanup-ignored-issues <n> Tracked newsletters ignored in a row before a subscriber is cold (2 to 100, default 6).
|
|
1393
|
+
--cleanup-inactive-days <n> Days without a sign of life before a subscriber is cold (30 to 3650, default 180).
|
|
1305
1394
|
|
|
1306
1395
|
Newsletter block prose:
|
|
1307
1396
|
Paragraph, quote, callout, list-item, and column bodies accept bare text or sanitized inline HTML. Use <p> and <br> for line breaks.
|
|
1308
1397
|
|
|
1309
1398
|
Newsletter feeds:
|
|
1310
|
-
--feed-entry-url <url> Link this
|
|
1399
|
+
--feed-entry-url <url> Link this newsletter's Atom feed title to an http(s) URL instead of its Shipmail archive.
|
|
1311
1400
|
|
|
1312
1401
|
Sign-in:
|
|
1313
1402
|
Run shipmail login once to sign in through the browser instead of using an API key.
|
|
@@ -1557,6 +1646,44 @@ async function replyToMessageParams(options) {
|
|
|
1557
1646
|
} : {}
|
|
1558
1647
|
};
|
|
1559
1648
|
}
|
|
1649
|
+
var DOMAIN_HEALTH_PROVIDER_LABELS = {
|
|
1650
|
+
google: "Gmail",
|
|
1651
|
+
microsoft: "Outlook",
|
|
1652
|
+
yahoo: "Yahoo",
|
|
1653
|
+
apple: "iCloud",
|
|
1654
|
+
other: "Other"
|
|
1655
|
+
};
|
|
1656
|
+
function formatRecipients(count) {
|
|
1657
|
+
return `${count} ${count === 1 ? "recipient" : "recipients"}`;
|
|
1658
|
+
}
|
|
1659
|
+
function formatDeliveryRate(rate) {
|
|
1660
|
+
return rate === null ? "no results yet" : `${rate}% delivered`;
|
|
1661
|
+
}
|
|
1662
|
+
function writeDomainHealth(out, health) {
|
|
1663
|
+
const { dns, blocklist, delivery } = health;
|
|
1664
|
+
const checks = [
|
|
1665
|
+
["MX", dns.mx],
|
|
1666
|
+
["SPF", dns.spf],
|
|
1667
|
+
["DKIM", dns.dkim],
|
|
1668
|
+
["DMARC", dns.dmarc]
|
|
1669
|
+
];
|
|
1670
|
+
const lines = [
|
|
1671
|
+
`${health.domain} (${health.domain_id})`,
|
|
1672
|
+
"",
|
|
1673
|
+
`DNS (checked ${dns.last_checked_at ?? "never"})`,
|
|
1674
|
+
...checks.map(([name, ok]) => ` ${name.padEnd(6)}${ok ? "pass" : "fail"}`),
|
|
1675
|
+
"",
|
|
1676
|
+
`Spamhaus DBL: ${blocklist.listed ? `listed since ${blocklist.listed_since ?? "unknown"}` : "not listed"}`,
|
|
1677
|
+
"",
|
|
1678
|
+
`Delivery, last 30 days: ${formatDeliveryRate(delivery.delivery_rate)}`,
|
|
1679
|
+
` ${formatRecipients(delivery.recipients)}: ${delivery.delivered} delivered, ${delivery.bounced} bounced, ${delivery.deferred} deferred, ${delivery.complained} complained`,
|
|
1680
|
+
...delivery.providers.map(
|
|
1681
|
+
(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`
|
|
1682
|
+
)
|
|
1683
|
+
];
|
|
1684
|
+
out.write(`${lines.join("\n")}
|
|
1685
|
+
`);
|
|
1686
|
+
}
|
|
1560
1687
|
async function runDomains(client, parsed, out) {
|
|
1561
1688
|
const subcommand = parsed.positional[1];
|
|
1562
1689
|
if (subcommand === "list") {
|
|
@@ -1594,6 +1721,14 @@ async function runDomains(client, parsed, out) {
|
|
|
1594
1721
|
);
|
|
1595
1722
|
return;
|
|
1596
1723
|
}
|
|
1724
|
+
if (subcommand === "health") {
|
|
1725
|
+
const health = await client.domains.getHealth(
|
|
1726
|
+
requirePositional(parsed.positional, 2, "domain id")
|
|
1727
|
+
);
|
|
1728
|
+
if (getBoolean(parsed.options, "json") === true) writeJson(out, health);
|
|
1729
|
+
else writeDomainHealth(out, health);
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1597
1732
|
if (subcommand === "verify") {
|
|
1598
1733
|
writeJson(
|
|
1599
1734
|
out,
|
|
@@ -1658,6 +1793,21 @@ async function runMailboxes(client, parsed, out) {
|
|
|
1658
1793
|
);
|
|
1659
1794
|
return;
|
|
1660
1795
|
}
|
|
1796
|
+
if (subcommand === "update") {
|
|
1797
|
+
const displayName = getString(parsed.options, "display-name");
|
|
1798
|
+
const agentSecurityHold = getBoolean(parsed.options, "agent-security-hold");
|
|
1799
|
+
if (displayName === void 0 && agentSecurityHold === void 0) {
|
|
1800
|
+
throw new CliUsageError("Provide --display-name or --agent-security-hold.");
|
|
1801
|
+
}
|
|
1802
|
+
writeJson(
|
|
1803
|
+
out,
|
|
1804
|
+
await client.mailboxes.update(requirePositional(parsed.positional, 2, "mailbox id"), {
|
|
1805
|
+
...displayName !== void 0 ? { display_name: displayName } : {},
|
|
1806
|
+
...agentSecurityHold !== void 0 ? { agent_security_hold: agentSecurityHold } : {}
|
|
1807
|
+
})
|
|
1808
|
+
);
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1661
1811
|
if (subcommand === "suspend") {
|
|
1662
1812
|
writeJson(
|
|
1663
1813
|
out,
|
|
@@ -1925,6 +2075,21 @@ async function runInbox(client, parsed, out) {
|
|
|
1925
2075
|
);
|
|
1926
2076
|
return;
|
|
1927
2077
|
}
|
|
2078
|
+
if (subcommand === "security-review") {
|
|
2079
|
+
writeJson(
|
|
2080
|
+
out,
|
|
2081
|
+
await client.mailboxes.releaseInboxMessageSecurity(
|
|
2082
|
+
requirePositional(parsed.positional, 2, "mailbox id"),
|
|
2083
|
+
requirePositional(parsed.positional, 3, "message id"),
|
|
2084
|
+
{
|
|
2085
|
+
decision: "release",
|
|
2086
|
+
reason: getRequiredString(parsed.options, "reason"),
|
|
2087
|
+
expected_content_sha256: getRequiredString(parsed.options, "expected-content-sha256")
|
|
2088
|
+
}
|
|
2089
|
+
)
|
|
2090
|
+
);
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
1928
2093
|
if (subcommand === "threads") {
|
|
1929
2094
|
const attentionState = getString(parsed.options, "attention-state");
|
|
1930
2095
|
const sortBy = getString(parsed.options, "sort-by");
|
|
@@ -2019,13 +2184,64 @@ async function runInbox(client, parsed, out) {
|
|
|
2019
2184
|
);
|
|
2020
2185
|
return;
|
|
2021
2186
|
}
|
|
2187
|
+
if (subcommand === "drafts") {
|
|
2188
|
+
writeJson(
|
|
2189
|
+
out,
|
|
2190
|
+
await client.mailboxes.listInboxReplyDrafts(
|
|
2191
|
+
requirePositional(parsed.positional, 2, "mailbox id"),
|
|
2192
|
+
requirePositional(parsed.positional, 3, "thread id")
|
|
2193
|
+
)
|
|
2194
|
+
);
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
if (subcommand === "get-draft") {
|
|
2198
|
+
writeJson(
|
|
2199
|
+
out,
|
|
2200
|
+
await client.mailboxes.getInboxReplyDraft(
|
|
2201
|
+
requirePositional(parsed.positional, 2, "mailbox id"),
|
|
2202
|
+
requirePositional(parsed.positional, 3, "thread id"),
|
|
2203
|
+
requirePositional(parsed.positional, 4, "draft id")
|
|
2204
|
+
)
|
|
2205
|
+
);
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
if (subcommand === "update-draft") {
|
|
2209
|
+
const text = await readTextInput(getString(parsed.options, "text"));
|
|
2210
|
+
const html = await readTextInput(getString(parsed.options, "html"));
|
|
2211
|
+
if (!text && !html) throw new CliUsageError("Provide --text or --html.");
|
|
2212
|
+
writeJson(
|
|
2213
|
+
out,
|
|
2214
|
+
await client.mailboxes.updateInboxReplyDraft(
|
|
2215
|
+
requirePositional(parsed.positional, 2, "mailbox id"),
|
|
2216
|
+
requirePositional(parsed.positional, 3, "thread id"),
|
|
2217
|
+
requirePositional(parsed.positional, 4, "draft id"),
|
|
2218
|
+
{
|
|
2219
|
+
...text ? { text } : {},
|
|
2220
|
+
...html ? { html } : {}
|
|
2221
|
+
}
|
|
2222
|
+
)
|
|
2223
|
+
);
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
if (subcommand === "delete-draft") {
|
|
2227
|
+
const draftId = requirePositional(parsed.positional, 4, "draft id");
|
|
2228
|
+
await client.mailboxes.deleteInboxReplyDraft(
|
|
2229
|
+
requirePositional(parsed.positional, 2, "mailbox id"),
|
|
2230
|
+
requirePositional(parsed.positional, 3, "thread id"),
|
|
2231
|
+
draftId
|
|
2232
|
+
);
|
|
2233
|
+
writeJson(out, { ok: true, id: draftId });
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2022
2236
|
if (subcommand === "send-draft") {
|
|
2237
|
+
const expectedContentSha256 = getString(parsed.options, "expected-content-sha256");
|
|
2023
2238
|
writeJson(
|
|
2024
2239
|
out,
|
|
2025
2240
|
await client.mailboxes.sendInboxReplyDraft(
|
|
2026
2241
|
requirePositional(parsed.positional, 2, "mailbox id"),
|
|
2027
2242
|
requirePositional(parsed.positional, 3, "thread id"),
|
|
2028
|
-
requirePositional(parsed.positional, 4, "draft id")
|
|
2243
|
+
requirePositional(parsed.positional, 4, "draft id"),
|
|
2244
|
+
expectedContentSha256 ? { expectedContentSha256 } : void 0
|
|
2029
2245
|
)
|
|
2030
2246
|
);
|
|
2031
2247
|
return;
|
|
@@ -2320,14 +2536,27 @@ async function runMembers(client, parsed, out) {
|
|
|
2320
2536
|
}
|
|
2321
2537
|
throw new CliUsageError("Unknown members command.");
|
|
2322
2538
|
}
|
|
2539
|
+
var SUPPRESSION_SCOPES = ["all", "marketing"];
|
|
2323
2540
|
async function runSuppressions(client, parsed, out) {
|
|
2324
2541
|
const subcommand = parsed.positional[1];
|
|
2542
|
+
if (subcommand === "add") {
|
|
2543
|
+
const scope = getEnum(parsed.options, "scope", SUPPRESSION_SCOPES);
|
|
2544
|
+
writeJson(
|
|
2545
|
+
out,
|
|
2546
|
+
await client.suppressions.create({
|
|
2547
|
+
email: requirePositional(parsed.positional, 2, "email"),
|
|
2548
|
+
...scope ? { scope } : {}
|
|
2549
|
+
})
|
|
2550
|
+
);
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2325
2553
|
if (subcommand === "list") {
|
|
2326
2554
|
writeJson(
|
|
2327
2555
|
out,
|
|
2328
2556
|
await client.suppressions.list({
|
|
2329
2557
|
cursor: getString(parsed.options, "cursor"),
|
|
2330
|
-
limit: getNumber(parsed.options, "limit")
|
|
2558
|
+
limit: getNumber(parsed.options, "limit"),
|
|
2559
|
+
scope: getEnum(parsed.options, "scope", SUPPRESSION_SCOPES)
|
|
2331
2560
|
})
|
|
2332
2561
|
);
|
|
2333
2562
|
return;
|
|
@@ -2347,7 +2576,8 @@ var SUBSCRIBER_STATUS_VALUES = [
|
|
|
2347
2576
|
"bounced",
|
|
2348
2577
|
"complained",
|
|
2349
2578
|
"suppressed",
|
|
2350
|
-
"transactional_only"
|
|
2579
|
+
"transactional_only",
|
|
2580
|
+
"dormant"
|
|
2351
2581
|
];
|
|
2352
2582
|
function subscriberListParams(options) {
|
|
2353
2583
|
const email = getString(options, "email");
|
|
@@ -2504,6 +2734,89 @@ async function runAudienceFeed(client, parsed, out) {
|
|
|
2504
2734
|
}
|
|
2505
2735
|
throw new CliUsageError("Unknown audiences feed command.");
|
|
2506
2736
|
}
|
|
2737
|
+
async function welcomeStepParams(options) {
|
|
2738
|
+
const subject = getString(options, "subject");
|
|
2739
|
+
const previewText = getString(options, "preview-text");
|
|
2740
|
+
const waitMinutes = getNumber(options, "wait-minutes");
|
|
2741
|
+
const senderIdentityId = getString(options, "sender-identity-id");
|
|
2742
|
+
const stylingMode = parseStylingMode(options);
|
|
2743
|
+
const trackEngagement = getBoolean(options, "track-engagement");
|
|
2744
|
+
const addUtmTags = getBoolean(options, "add-utm-tags");
|
|
2745
|
+
const blocksInput = getString(options, "blocks");
|
|
2746
|
+
const blocks = blocksInput ? await readNewsletterBlocks(blocksInput) : void 0;
|
|
2747
|
+
const contentMarkdown = await readNewsletterMarkdown(options);
|
|
2748
|
+
if (waitMinutes !== void 0 && (!Number.isInteger(waitMinutes) || waitMinutes < 0)) {
|
|
2749
|
+
throw new CliUsageError("--wait-minutes must be a whole number of minutes, 0 or more.");
|
|
2750
|
+
}
|
|
2751
|
+
return {
|
|
2752
|
+
...subject !== void 0 ? { subject } : {},
|
|
2753
|
+
...previewText !== void 0 ? { preview_text: previewText } : {},
|
|
2754
|
+
...blocks ? { blocks } : {},
|
|
2755
|
+
...contentMarkdown !== void 0 ? { content_markdown: contentMarkdown } : {},
|
|
2756
|
+
...waitMinutes !== void 0 ? { wait_minutes: waitMinutes } : {},
|
|
2757
|
+
...senderIdentityId !== void 0 ? { sender_identity_id: senderIdentityId } : {},
|
|
2758
|
+
...stylingMode !== void 0 ? { styling_mode: stylingMode } : {},
|
|
2759
|
+
...trackEngagement !== void 0 ? { track_engagement: trackEngagement } : {},
|
|
2760
|
+
...addUtmTags !== void 0 ? { add_utm_tags: addUtmTags } : {}
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
async function runAudienceWelcome(client, parsed, out) {
|
|
2764
|
+
const action = parsed.positional[2];
|
|
2765
|
+
const audienceId = requirePositional(parsed.positional, 3, "audience id");
|
|
2766
|
+
if (action === "get") {
|
|
2767
|
+
writeJson(out, await client.audiences.welcomeSequence.get(audienceId));
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
const idempotencyKey = getString(parsed.options, "idempotency-key");
|
|
2771
|
+
const methodOptions = idempotencyKey ? { idempotencyKey } : void 0;
|
|
2772
|
+
if (action === "enable" || action === "disable") {
|
|
2773
|
+
writeJson(
|
|
2774
|
+
out,
|
|
2775
|
+
await client.audiences.welcomeSequence.update(
|
|
2776
|
+
audienceId,
|
|
2777
|
+
{ enabled: action === "enable" },
|
|
2778
|
+
methodOptions
|
|
2779
|
+
)
|
|
2780
|
+
);
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
if (action === "add-step") {
|
|
2784
|
+
const params = await welcomeStepParams(parsed.options);
|
|
2785
|
+
if (!params.subject) throw new CliUsageError("--subject is required.");
|
|
2786
|
+
if (!params.blocks?.length && !params.content_markdown?.trim()) {
|
|
2787
|
+
throw new CliUsageError("Provide --blocks or --markdown.");
|
|
2788
|
+
}
|
|
2789
|
+
writeJson(
|
|
2790
|
+
out,
|
|
2791
|
+
await client.audiences.welcomeSequence.createStep(
|
|
2792
|
+
audienceId,
|
|
2793
|
+
{ ...params, subject: params.subject },
|
|
2794
|
+
methodOptions
|
|
2795
|
+
)
|
|
2796
|
+
);
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
const stepId = requirePositional(parsed.positional, 4, "step id");
|
|
2800
|
+
if (action === "update-step") {
|
|
2801
|
+
const params = await welcomeStepParams(parsed.options);
|
|
2802
|
+
if (Object.keys(params).length === 0) {
|
|
2803
|
+
throw new CliUsageError("Provide at least one field to update.");
|
|
2804
|
+
}
|
|
2805
|
+
writeJson(
|
|
2806
|
+
out,
|
|
2807
|
+
await client.audiences.welcomeSequence.updateStep(audienceId, stepId, params, methodOptions)
|
|
2808
|
+
);
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
if (action === "remove-step") {
|
|
2812
|
+
writeJson(
|
|
2813
|
+
out,
|
|
2814
|
+
await client.audiences.welcomeSequence.deleteStep(audienceId, stepId, methodOptions)
|
|
2815
|
+
);
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
throw new CliUsageError("Unknown audiences welcome command.");
|
|
2819
|
+
}
|
|
2507
2820
|
async function runAudiences(client, parsed, out) {
|
|
2508
2821
|
const subcommand = parsed.positional[1];
|
|
2509
2822
|
if (subcommand === "subscribers") {
|
|
@@ -2514,6 +2827,10 @@ async function runAudiences(client, parsed, out) {
|
|
|
2514
2827
|
await runAudienceFeed(client, parsed, out);
|
|
2515
2828
|
return;
|
|
2516
2829
|
}
|
|
2830
|
+
if (subcommand === "welcome") {
|
|
2831
|
+
await runAudienceWelcome(client, parsed, out);
|
|
2832
|
+
return;
|
|
2833
|
+
}
|
|
2517
2834
|
if (subcommand === "list") {
|
|
2518
2835
|
writeJson(
|
|
2519
2836
|
out,
|
|
@@ -2547,11 +2864,17 @@ async function runAudiences(client, parsed, out) {
|
|
|
2547
2864
|
const id = requirePositional(parsed.positional, 2, "audience id");
|
|
2548
2865
|
const name = getString(parsed.options, "name");
|
|
2549
2866
|
const description = getString(parsed.options, "description");
|
|
2867
|
+
const cleanup_enabled = getBoolean(parsed.options, "cleanup");
|
|
2868
|
+
const cleanup_ignored_issues = getNumber(parsed.options, "cleanup-ignored-issues");
|
|
2869
|
+
const cleanup_inactive_days = getNumber(parsed.options, "cleanup-inactive-days");
|
|
2550
2870
|
writeJson(
|
|
2551
2871
|
out,
|
|
2552
2872
|
await client.audiences.update(id, {
|
|
2553
2873
|
...name ? { name } : {},
|
|
2554
|
-
...description ? { description } : {}
|
|
2874
|
+
...description ? { description } : {},
|
|
2875
|
+
...cleanup_enabled !== void 0 ? { cleanup_enabled } : {},
|
|
2876
|
+
...cleanup_ignored_issues !== void 0 ? { cleanup_ignored_issues } : {},
|
|
2877
|
+
...cleanup_inactive_days !== void 0 ? { cleanup_inactive_days } : {}
|
|
2555
2878
|
})
|
|
2556
2879
|
);
|
|
2557
2880
|
return;
|
|
@@ -2630,14 +2953,27 @@ async function runNewsletters(client, parsed, out) {
|
|
|
2630
2953
|
return;
|
|
2631
2954
|
}
|
|
2632
2955
|
if (subcommand === "schedule") {
|
|
2956
|
+
const limit = getNumber(parsed.options, "limit");
|
|
2633
2957
|
writeJson(
|
|
2634
2958
|
out,
|
|
2635
2959
|
await client.newsletters.schedule(requirePositional(parsed.positional, 2, "newsletter id"), {
|
|
2636
|
-
scheduled_at: getRequiredString(parsed.options, "scheduled-at")
|
|
2960
|
+
scheduled_at: getRequiredString(parsed.options, "scheduled-at"),
|
|
2961
|
+
...limit !== void 0 ? { limit } : {}
|
|
2637
2962
|
})
|
|
2638
2963
|
);
|
|
2639
2964
|
return;
|
|
2640
2965
|
}
|
|
2966
|
+
if (subcommand === "send-more") {
|
|
2967
|
+
const count = getNumber(parsed.options, "count");
|
|
2968
|
+
writeJson(
|
|
2969
|
+
out,
|
|
2970
|
+
await client.newsletters.sendToMore(
|
|
2971
|
+
requirePositional(parsed.positional, 2, "newsletter id"),
|
|
2972
|
+
count !== void 0 ? { count } : {}
|
|
2973
|
+
)
|
|
2974
|
+
);
|
|
2975
|
+
return;
|
|
2976
|
+
}
|
|
2641
2977
|
if (subcommand === "cancel") {
|
|
2642
2978
|
writeJson(
|
|
2643
2979
|
out,
|
|
@@ -2957,6 +3293,93 @@ async function runCalendar(client, parsed, out) {
|
|
|
2957
3293
|
}
|
|
2958
3294
|
throw new CliUsageError("Unknown calendar command.");
|
|
2959
3295
|
}
|
|
3296
|
+
function parseBookingLinkRanges(options) {
|
|
3297
|
+
const values = getStringList(options, "range");
|
|
3298
|
+
if (values.length === 0) return void 0;
|
|
3299
|
+
if (values.length === 1 && values[0] === "none") return null;
|
|
3300
|
+
return values.map((value) => {
|
|
3301
|
+
const [start, end, extra] = value.split("/");
|
|
3302
|
+
if (!start || !end || extra !== void 0) {
|
|
3303
|
+
throw new CliUsageError("--range must be <start>/<end>, two ISO 8601 instants.");
|
|
3304
|
+
}
|
|
3305
|
+
return { start, end };
|
|
3306
|
+
});
|
|
3307
|
+
}
|
|
3308
|
+
function getMaxBookings(options) {
|
|
3309
|
+
if (getString(options, "max-bookings") === "unlimited") return null;
|
|
3310
|
+
return getNumber(options, "max-bookings");
|
|
3311
|
+
}
|
|
3312
|
+
function getNullableString(options, key) {
|
|
3313
|
+
const value = getString(options, key);
|
|
3314
|
+
return value === "none" ? null : value;
|
|
3315
|
+
}
|
|
3316
|
+
async function runBookingLinks(client, parsed, out) {
|
|
3317
|
+
const subcommand = parsed.positional[1];
|
|
3318
|
+
const options = parsed.options;
|
|
3319
|
+
if (subcommand === "list") {
|
|
3320
|
+
writeJson(
|
|
3321
|
+
out,
|
|
3322
|
+
await client.bookingLinks.list({
|
|
3323
|
+
cursor: getString(options, "cursor"),
|
|
3324
|
+
limit: getNumber(options, "limit"),
|
|
3325
|
+
status: getEnum(options, "status", BOOKING_LINK_STATUSES)
|
|
3326
|
+
})
|
|
3327
|
+
);
|
|
3328
|
+
return;
|
|
3329
|
+
}
|
|
3330
|
+
if (subcommand === "create") {
|
|
3331
|
+
const ranges = parseBookingLinkRanges(options);
|
|
3332
|
+
if (ranges === null) throw new CliUsageError("--range none only applies to update.");
|
|
3333
|
+
const params = {
|
|
3334
|
+
name: getRequiredString(options, "name"),
|
|
3335
|
+
mailbox: getRequiredString(options, "mailbox"),
|
|
3336
|
+
duration_minutes: requireNumber(options, "duration-minutes"),
|
|
3337
|
+
timezone: getRequiredString(options, "timezone"),
|
|
3338
|
+
offered_ranges: ranges,
|
|
3339
|
+
source_page_id: getString(options, "source-page"),
|
|
3340
|
+
description: getString(options, "description"),
|
|
3341
|
+
buffer_minutes: getNumber(options, "buffer-minutes"),
|
|
3342
|
+
minimum_notice_minutes: getNumber(options, "minimum-notice-minutes"),
|
|
3343
|
+
conferencing_provider: getConferencingProvider(options),
|
|
3344
|
+
max_bookings: getMaxBookings(options),
|
|
3345
|
+
expires_at: getString(options, "expires-at")
|
|
3346
|
+
};
|
|
3347
|
+
writeJson(out, await client.bookingLinks.create(params));
|
|
3348
|
+
return;
|
|
3349
|
+
}
|
|
3350
|
+
if (subcommand === "get") {
|
|
3351
|
+
writeJson(
|
|
3352
|
+
out,
|
|
3353
|
+
await client.bookingLinks.get(requirePositional(parsed.positional, 2, "booking link id"))
|
|
3354
|
+
);
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3357
|
+
if (subcommand === "update") {
|
|
3358
|
+
const id = requirePositional(parsed.positional, 2, "booking link id");
|
|
3359
|
+
const params = {
|
|
3360
|
+
name: getString(options, "name"),
|
|
3361
|
+
mailbox: getString(options, "mailbox"),
|
|
3362
|
+
duration_minutes: getNumber(options, "duration-minutes"),
|
|
3363
|
+
timezone: getString(options, "timezone"),
|
|
3364
|
+
offered_ranges: parseBookingLinkRanges(options),
|
|
3365
|
+
source_page_id: getNullableString(options, "source-page"),
|
|
3366
|
+
description: getString(options, "description"),
|
|
3367
|
+
buffer_minutes: getNumber(options, "buffer-minutes"),
|
|
3368
|
+
minimum_notice_minutes: getNumber(options, "minimum-notice-minutes"),
|
|
3369
|
+
conferencing_provider: getConferencingProvider(options),
|
|
3370
|
+
max_bookings: getMaxBookings(options),
|
|
3371
|
+
expires_at: getNullableString(options, "expires-at")
|
|
3372
|
+
};
|
|
3373
|
+
writeJson(out, await client.bookingLinks.update(id, params));
|
|
3374
|
+
return;
|
|
3375
|
+
}
|
|
3376
|
+
if (subcommand === "revoke") {
|
|
3377
|
+
const id = requirePositional(parsed.positional, 2, "booking link id");
|
|
3378
|
+
writeJson(out, await client.bookingLinks.revoke(id));
|
|
3379
|
+
return;
|
|
3380
|
+
}
|
|
3381
|
+
throw new CliUsageError("Unknown booking-links command.");
|
|
3382
|
+
}
|
|
2960
3383
|
async function runBookingPages(client, parsed, out) {
|
|
2961
3384
|
const subcommand = parsed.positional[1];
|
|
2962
3385
|
const options = parsed.options;
|
|
@@ -3097,6 +3520,11 @@ ${JSON.stringify(err.body, null, 2)}
|
|
|
3097
3520
|
if (shipmailError.actionUrl) {
|
|
3098
3521
|
const reason = shipmailError.reason ? ` (${shipmailError.reason})` : "";
|
|
3099
3522
|
errOut.write(`Ask the organization owner to open${reason}: ${shipmailError.actionUrl}
|
|
3523
|
+
`);
|
|
3524
|
+
}
|
|
3525
|
+
if (shipmailError.limit) {
|
|
3526
|
+
const { resource, used, limit, scope } = shipmailError.limit;
|
|
3527
|
+
errOut.write(`Limit: ${resource} ${used} of ${limit} (${scope})
|
|
3100
3528
|
`);
|
|
3101
3529
|
}
|
|
3102
3530
|
return;
|
|
@@ -3181,6 +3609,7 @@ async function runCli(argv, options = {}) {
|
|
|
3181
3609
|
else if (command === "audiences") await runAudiences(client, parsed, out);
|
|
3182
3610
|
else if (command === "calendar") await runCalendar(client, parsed, out);
|
|
3183
3611
|
else if (command === "booking-pages") await runBookingPages(client, parsed, out);
|
|
3612
|
+
else if (command === "booking-links") await runBookingLinks(client, parsed, out);
|
|
3184
3613
|
else throw new CliUsageError(`Unknown command: ${command}.`);
|
|
3185
3614
|
return 0;
|
|
3186
3615
|
} 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.5",
|
|
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.6.
|
|
44
|
+
"shipmail": "0.6.6",
|
|
45
45
|
"zod": "^4.6.5"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|