shipmail-cli 0.6.3 → 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 +27 -6
- package/dist/cli.js +497 -24
- 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
|
|
@@ -98,12 +99,19 @@ shipmail mailboxes sandbox-inbound mbx_123 --from customer@example.com --subject
|
|
|
98
99
|
|
|
99
100
|
# Read inbound mail
|
|
100
101
|
shipmail inbox list 550e8400-e29b-41d4-a716-446655440000 --folder-role inbox --after 2025-07-20T00:00:00Z --limit 25
|
|
102
|
+
shipmail inbox list 550e8400-e29b-41d4-a716-446655440000 --from billing@vendor.example --subject invoice --has-attachment
|
|
101
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
|
|
102
109
|
shipmail inbox threads 550e8400-e29b-41d4-a716-446655440000 --attention-state needs_reply --after 2025-07-20T00:00:00Z --limit 50
|
|
103
110
|
shipmail inbox thread 550e8400-e29b-41d4-a716-446655440000 thr_123
|
|
104
111
|
shipmail inbox draft 550e8400-e29b-41d4-a716-446655440000 thr_123 --expected-version 7 --text "Thanks for the note"
|
|
105
|
-
#
|
|
106
|
-
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
|
|
107
115
|
# Save a new message to Drafts for a human to review and send; this never sends
|
|
108
116
|
shipmail inbox compose-draft 550e8400-e29b-41d4-a716-446655440000 --to user@example.com --subject "Kickoff" --text "Draft for review"
|
|
109
117
|
shipmail inbox thread-attention 550e8400-e29b-41d4-a716-446655440000 thr_123 --done true --expected-version 8
|
|
@@ -124,6 +132,8 @@ shipmail newsletters create \
|
|
|
124
132
|
--subject "What shipped in July" \
|
|
125
133
|
--blocks @blocks.json
|
|
126
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
|
|
127
137
|
# A concurrent update exits with conflict (409); fetch the latest newsletter before retrying.
|
|
128
138
|
shipmail newsletters preview nws_123
|
|
129
139
|
shipmail newsletters test-send nws_123 --recipient-email owner@example.com
|
|
@@ -134,6 +144,11 @@ shipmail newsletter-assets upload ./hero.png --content-type image/png
|
|
|
134
144
|
shipmail newsletter-assets upload ./demo.mp4 --content-type video/mp4 --thumbnail ./demo-poster.jpg
|
|
135
145
|
shipmail newsletter-assets register-url https://cdn.shipmail.to/newsletter-images/org_123/hero.png
|
|
136
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
|
+
|
|
137
152
|
# Manage webhooks
|
|
138
153
|
shipmail webhooks create \
|
|
139
154
|
--url https://example.com/shipmail/webhook \
|
|
@@ -150,6 +165,11 @@ Paragraph, quote, callout, list-item, and column bodies in `--blocks` accept bar
|
|
|
150
165
|
sanitized inline HTML. Use `<p>` and `<br>` for line breaks. Shipmail keeps `a`, `b`, `br`,
|
|
151
166
|
`code`, `em`, `i`, `p`, `s`, `span`, `strong`, and `u`, with safe attributes and URLs.
|
|
152
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
|
+
|
|
153
173
|
`newsletter-assets upload` uses a five-minute direct storage PUT, then calls the single-use
|
|
154
174
|
completion URL. Video uploads require a JPEG poster through `--thumbnail`. `newsletter-assets list`
|
|
155
175
|
includes current newsletter media storage usage and a `pagination.next_cursor` value in the JSON
|
|
@@ -170,17 +190,18 @@ Use repeatable `--header "Name: value"` flags for request headers. Use `--output
|
|
|
170
190
|
- `status`
|
|
171
191
|
- `api <METHOD> <PATH>`
|
|
172
192
|
- `domains list|search|create|get|verify|delete`
|
|
173
|
-
- `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`
|
|
174
194
|
- `members list|get`
|
|
175
|
-
- `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`
|
|
176
196
|
- `reply-scans create|status|results`
|
|
177
197
|
- `messages send|list|analytics|get|reply` (test keys also support `--sandbox-outcome`)
|
|
178
198
|
- `scheduled-messages list|get|update|cancel`
|
|
179
199
|
- `mailboxes sandbox-inbound`
|
|
180
200
|
- `webhooks list|create|update|test|deliveries|replay-delivery`
|
|
181
|
-
- `suppressions list|remove`
|
|
201
|
+
- `suppressions add|list|remove`
|
|
182
202
|
- `audiences list|create|get|update|delete`
|
|
183
203
|
- `audiences feed get|update|rotate|revoke`
|
|
204
|
+
- `audiences welcome get|enable|disable|add-step|update-step|remove-step`
|
|
184
205
|
- `audiences subscribers add|list|get|by-email|update|unsubscribe|resubscribe|remove`
|
|
185
206
|
- `newsletter-sender-identities list`
|
|
186
207
|
- `newsletter-domains list`
|
|
@@ -191,7 +212,7 @@ Member output uses the roles `owner`, `admin`, and `member`. Its `mailbox_access
|
|
|
191
212
|
`selected`; `mailbox_ids` is empty for `all`, while selected IDs may be limited to mailboxes the
|
|
192
213
|
API key can reach.
|
|
193
214
|
|
|
194
|
-
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:
|
|
195
216
|
|
|
196
217
|
```bash
|
|
197
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,15 +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");
|
|
1058
|
+
const add_utm_tags = getBoolean(options, "add-utm-tags");
|
|
1038
1059
|
const preview_text = getString(options, "preview-text");
|
|
1060
|
+
const subjectTest = subjectTestParams(options);
|
|
1039
1061
|
return {
|
|
1040
1062
|
audience_id,
|
|
1041
1063
|
sender_identity_id,
|
|
@@ -1043,13 +1065,32 @@ async function newsletterCreateParams(options) {
|
|
|
1043
1065
|
subject,
|
|
1044
1066
|
...preview_text !== void 0 ? { preview_text } : {},
|
|
1045
1067
|
...blocks ? { blocks } : {},
|
|
1068
|
+
...content_markdown !== void 0 ? { content_markdown } : {},
|
|
1046
1069
|
...body_html !== void 0 ? { body_html } : {},
|
|
1047
1070
|
...body_text !== void 0 ? { body_text } : {},
|
|
1048
1071
|
...send_window_hours !== void 0 ? { send_window_hours } : {},
|
|
1049
1072
|
...archive_visibility !== void 0 ? { archive_visibility } : {},
|
|
1073
|
+
...publish_target !== void 0 ? { publish_target } : {},
|
|
1050
1074
|
...feed_entry_url !== void 0 ? { feed_entry_url } : {},
|
|
1051
1075
|
...styling_mode !== void 0 ? { styling_mode } : {},
|
|
1052
|
-
...track_engagement !== void 0 ? { track_engagement } : {}
|
|
1076
|
+
...track_engagement !== void 0 ? { track_engagement } : {},
|
|
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 } : {}
|
|
1053
1094
|
};
|
|
1054
1095
|
}
|
|
1055
1096
|
async function newsletterUpdateParams(options) {
|
|
@@ -1060,15 +1101,18 @@ async function newsletterUpdateParams(options) {
|
|
|
1060
1101
|
const preview_text = getString(options, "preview-text");
|
|
1061
1102
|
const send_window_hours = getNumber(options, "send-window-hours");
|
|
1062
1103
|
const archive_visibility = parseArchiveVisibility(options);
|
|
1104
|
+
const publish_target = parsePublishTarget(options);
|
|
1063
1105
|
const feed_entry_url = getString(options, "feed-entry-url");
|
|
1064
1106
|
const styling_mode = parseStylingMode(options);
|
|
1065
1107
|
const track_engagement = getBoolean(options, "track-engagement");
|
|
1108
|
+
const add_utm_tags = getBoolean(options, "add-utm-tags");
|
|
1066
1109
|
const blocksInput = getString(options, "blocks");
|
|
1067
1110
|
const bodyHtmlInput = getString(options, "body-html");
|
|
1068
1111
|
const bodyTextInput = getString(options, "body-text");
|
|
1069
1112
|
const blocks = blocksInput ? await readNewsletterBlocks(blocksInput) : void 0;
|
|
1070
1113
|
const body_html = bodyHtmlInput ? await readTextInput(bodyHtmlInput) : void 0;
|
|
1071
1114
|
const body_text = bodyTextInput ? await readTextInput(bodyTextInput) : void 0;
|
|
1115
|
+
const content_markdown = await readNewsletterMarkdown(options);
|
|
1072
1116
|
const params = {
|
|
1073
1117
|
...audience_id ? { audience_id } : {},
|
|
1074
1118
|
...sender_identity_id ? { sender_identity_id } : {},
|
|
@@ -1076,13 +1120,17 @@ async function newsletterUpdateParams(options) {
|
|
|
1076
1120
|
...subject ? { subject } : {},
|
|
1077
1121
|
...preview_text !== void 0 ? { preview_text } : {},
|
|
1078
1122
|
...blocks ? { blocks } : {},
|
|
1123
|
+
...content_markdown !== void 0 ? { content_markdown } : {},
|
|
1079
1124
|
...body_html !== void 0 ? { body_html } : {},
|
|
1080
1125
|
...body_text !== void 0 ? { body_text } : {},
|
|
1081
1126
|
...send_window_hours !== void 0 ? { send_window_hours } : {},
|
|
1082
1127
|
...archive_visibility !== void 0 ? { archive_visibility } : {},
|
|
1128
|
+
...publish_target !== void 0 ? { publish_target } : {},
|
|
1083
1129
|
...feed_entry_url !== void 0 ? { feed_entry_url } : {},
|
|
1084
1130
|
...styling_mode !== void 0 ? { styling_mode } : {},
|
|
1085
|
-
...track_engagement !== void 0 ? { track_engagement } : {}
|
|
1131
|
+
...track_engagement !== void 0 ? { track_engagement } : {},
|
|
1132
|
+
...add_utm_tags !== void 0 ? { add_utm_tags } : {},
|
|
1133
|
+
...subjectTestParams(options)
|
|
1086
1134
|
};
|
|
1087
1135
|
if (Object.keys(params).length === 0) {
|
|
1088
1136
|
throw new CliUsageError("Provide at least one field to update.");
|
|
@@ -1175,11 +1223,13 @@ Usage:
|
|
|
1175
1223
|
shipmail domains create <name>
|
|
1176
1224
|
shipmail domains get <id>
|
|
1177
1225
|
shipmail domains dns-records <id>
|
|
1226
|
+
shipmail domains health <id> [--json]
|
|
1178
1227
|
shipmail domains verify <id>
|
|
1179
1228
|
shipmail domains delete <id>
|
|
1180
1229
|
shipmail mailboxes list [--domain-id dom] [--limit 25] [--cursor cur]
|
|
1181
1230
|
shipmail mailboxes create --domain-id dom --address hello (--password value | --password-file path | --generate-password) [--display-name "Hello"]
|
|
1182
1231
|
shipmail mailboxes get <id>
|
|
1232
|
+
shipmail mailboxes update <id> [--display-name "Ops"] [--agent-security-hold true|false]
|
|
1183
1233
|
shipmail mailboxes suspend <id>
|
|
1184
1234
|
shipmail mailboxes resume <id>
|
|
1185
1235
|
shipmail mailboxes export <id>
|
|
@@ -1202,14 +1252,19 @@ Usage:
|
|
|
1202
1252
|
shipmail mailboxes sandbox-inbound <id> --from sender@example.com --subject "Test" (--text "Hello" | --html "<p>Hello</p>") [--to support@example.com]
|
|
1203
1253
|
shipmail members list [--limit 25] [--cursor cur]
|
|
1204
1254
|
shipmail members get <id>
|
|
1205
|
-
shipmail inbox list <mailbox-id> [--folder-role inbox] [--search text] [--after ISO] [--before ISO] [--limit 25] [--cursor cur]
|
|
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]
|
|
1206
1256
|
shipmail inbox message <mailbox-id> <message-id>
|
|
1257
|
+
shipmail inbox security-review <mailbox-id> <message-id> --reason "Reviewed" --expected-content-sha256 HASH
|
|
1207
1258
|
shipmail inbox threads <mailbox-id> [--attention-state needs_reply] [--after ISO] [--before ISO] [--limit 25] [--cursor cur]
|
|
1208
1259
|
shipmail inbox thread <mailbox-id> <thread-id>
|
|
1209
1260
|
shipmail inbox thread-attention <mailbox-id> <thread-id> --done true --expected-version 3
|
|
1210
1261
|
shipmail inbox draft <mailbox-id> <thread-id> --expected-version 3 (--text "Hello" | --html "<p>Hello</p>") [--reply-mode reply_all]
|
|
1211
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)
|
|
1212
|
-
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]
|
|
1213
1268
|
shipmail inbox reply-message <mailbox-id> <message-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false]
|
|
1214
1269
|
shipmail inbox reply-thread <mailbox-id> <thread-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false]
|
|
1215
1270
|
shipmail inbox update <mailbox-id> <message-id> [--read true] [--starred false]
|
|
@@ -1227,23 +1282,25 @@ Usage:
|
|
|
1227
1282
|
shipmail reply-scans status <scan-id>
|
|
1228
1283
|
shipmail reply-scans results <scan-id> [--limit 25] [--cursor cur]
|
|
1229
1284
|
shipmail webhooks list
|
|
1230
|
-
shipmail webhooks create --url https://example.com/hook --events message.received,message.bounced [--authorization "Bearer key"] [--standard-webhooks-secret whsec_...]
|
|
1231
|
-
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]
|
|
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]
|
|
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]
|
|
1232
1287
|
shipmail webhooks replay-delivery <webhook-id> <delivery-id> [--idempotency-key key]
|
|
1233
|
-
shipmail suppressions
|
|
1288
|
+
shipmail suppressions add <email> [--scope all|marketing]
|
|
1289
|
+
shipmail suppressions list [--scope all|marketing] [--limit 25] [--cursor cur]
|
|
1234
1290
|
shipmail suppressions remove <email>
|
|
1235
1291
|
shipmail newsletters create \\
|
|
1236
1292
|
--audience-id aud_123 \\
|
|
1237
1293
|
--sender-identity-id nwsid_456 \\
|
|
1238
1294
|
--name "July changelog" \\
|
|
1239
1295
|
--subject "What shipped in July" \\
|
|
1240
|
-
--blocks @blocks.json \\
|
|
1241
|
-
[--feed-entry-url https://example.com/post] [--styling-mode styled|plain] [--track-engagement true|false]
|
|
1242
|
-
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]
|
|
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]
|
|
1243
1299
|
shipmail newsletters preview <id>
|
|
1244
1300
|
shipmail newsletters preflight <id>
|
|
1245
1301
|
shipmail newsletters test-send <id> --recipient-email owner@example.com
|
|
1246
|
-
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]
|
|
1247
1304
|
shipmail newsletters cancel <id>
|
|
1248
1305
|
shipmail newsletters resume <id>
|
|
1249
1306
|
shipmail newsletter-assets list [--kind image|video] [--q text] [--limit 25] [--cursor cur]
|
|
@@ -1254,12 +1311,17 @@ Usage:
|
|
|
1254
1311
|
shipmail audiences list [--limit 25] [--cursor cur]
|
|
1255
1312
|
shipmail audiences create <name> --consent-source "Signup form" [--description "..."]
|
|
1256
1313
|
shipmail audiences get <id>
|
|
1257
|
-
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]
|
|
1258
1315
|
shipmail audiences delete <id>
|
|
1259
1316
|
shipmail audiences feed get <audience-id>
|
|
1260
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]
|
|
1261
1318
|
shipmail audiences feed rotate <audience-id> [--idempotency-key key]
|
|
1262
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>
|
|
1263
1325
|
shipmail audiences subscribers add <audience-id> <email> [--name "Jane"] [--consent-source "..."]
|
|
1264
1326
|
shipmail audiences subscribers list <audience-id> [--status subscribed] [--email jane@x.com] [--limit 25]
|
|
1265
1327
|
shipmail audiences subscribers get <audience-id> <subscriber-id>
|
|
@@ -1279,11 +1341,27 @@ Usage:
|
|
|
1279
1341
|
shipmail booking-pages get <id>
|
|
1280
1342
|
shipmail booking-pages update <id> [--name "..."] [--conferencing-provider zoom|google_meet|none] [--active false]
|
|
1281
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>
|
|
1282
1349
|
|
|
1283
1350
|
Booking page conferencing:
|
|
1284
1351
|
--conferencing-provider <zoom|google_meet> Create a meeting for every booking.
|
|
1285
1352
|
--conferencing-provider none Stop creating meeting links on update.
|
|
1286
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
|
+
|
|
1287
1365
|
Conversation IDs:
|
|
1288
1366
|
<thread-id> accepts a conversation ID (thd_...) or the mail-server thread ID. Store the
|
|
1289
1367
|
conversation_id every response carries. thread_id is deprecated and unchanged.
|
|
@@ -1295,14 +1373,30 @@ Member access output:
|
|
|
1295
1373
|
Newsletter styling:
|
|
1296
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.
|
|
1297
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
|
+
|
|
1298
1379
|
Newsletter tracking:
|
|
1299
|
-
--track-engagement <true|false> Track opens and clicks for this
|
|
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).
|
|
1300
1394
|
|
|
1301
1395
|
Newsletter block prose:
|
|
1302
1396
|
Paragraph, quote, callout, list-item, and column bodies accept bare text or sanitized inline HTML. Use <p> and <br> for line breaks.
|
|
1303
1397
|
|
|
1304
1398
|
Newsletter feeds:
|
|
1305
|
-
--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.
|
|
1306
1400
|
|
|
1307
1401
|
Sign-in:
|
|
1308
1402
|
Run shipmail login once to sign in through the browser instead of using an API key.
|
|
@@ -1347,11 +1441,22 @@ function requirePositional(positional, index, label) {
|
|
|
1347
1441
|
if (!value) throw new CliUsageError(`Missing ${label}.`);
|
|
1348
1442
|
return value;
|
|
1349
1443
|
}
|
|
1444
|
+
function inboxTextFilters(options) {
|
|
1445
|
+
const from = getString(options, "from");
|
|
1446
|
+
const to = getString(options, "to");
|
|
1447
|
+
const subject = getString(options, "subject");
|
|
1448
|
+
return {
|
|
1449
|
+
...from ? { from } : {},
|
|
1450
|
+
...to ? { to } : {},
|
|
1451
|
+
...subject ? { subject } : {}
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1350
1454
|
function inboxListParams(options) {
|
|
1351
1455
|
const params = {};
|
|
1352
1456
|
const folderId = getString(options, "folder-id");
|
|
1353
1457
|
const folderRole = getString(options, "folder-role");
|
|
1354
1458
|
const searchText = getString(options, "search");
|
|
1459
|
+
const hasAttachment = getBoolean(options, "has-attachment");
|
|
1355
1460
|
const cursor = getString(options, "cursor");
|
|
1356
1461
|
const after = getString(options, "after");
|
|
1357
1462
|
const before = getString(options, "before");
|
|
@@ -1361,6 +1466,8 @@ function inboxListParams(options) {
|
|
|
1361
1466
|
if (folderId) params.folder_id = folderId;
|
|
1362
1467
|
if (folderRole) params.folder_role = folderRole;
|
|
1363
1468
|
if (searchText) params.search_text = searchText;
|
|
1469
|
+
Object.assign(params, inboxTextFilters(options));
|
|
1470
|
+
if (hasAttachment !== void 0) params.has_attachment = hasAttachment;
|
|
1364
1471
|
if (cursor) params.cursor = cursor;
|
|
1365
1472
|
if (after) params.after = after;
|
|
1366
1473
|
if (before) params.before = before;
|
|
@@ -1395,19 +1502,39 @@ function webhookWriteOnlyUpdate(options, name) {
|
|
|
1395
1502
|
}
|
|
1396
1503
|
return clear ? null : value;
|
|
1397
1504
|
}
|
|
1505
|
+
function webhookAllowedSenders(options) {
|
|
1506
|
+
const value = optionValue(options, "allowed-sender");
|
|
1507
|
+
if (value === void 0) return void 0;
|
|
1508
|
+
if (typeof value === "boolean") throw new CliUsageError("Missing value for --allowed-sender.");
|
|
1509
|
+
return getStringList(options, "allowed-sender");
|
|
1510
|
+
}
|
|
1511
|
+
function webhookAllowedSendersUpdate(options) {
|
|
1512
|
+
const senders = webhookAllowedSenders(options);
|
|
1513
|
+
const clear = getBoolean(options, "clear-allowed-senders") === true;
|
|
1514
|
+
if (senders !== void 0 && clear) {
|
|
1515
|
+
throw new CliUsageError("Use --allowed-sender or --clear-allowed-senders, not both.");
|
|
1516
|
+
}
|
|
1517
|
+
return clear ? null : senders;
|
|
1518
|
+
}
|
|
1398
1519
|
function webhookUpdateParams(options) {
|
|
1399
1520
|
const authorization = webhookWriteOnlyUpdate(options, "authorization");
|
|
1400
1521
|
const standardWebhooksSecret = webhookWriteOnlyUpdate(options, "standard-webhooks-secret");
|
|
1401
1522
|
const url = getString(options, "url");
|
|
1402
1523
|
const description = getString(options, "description");
|
|
1403
1524
|
const active = getBoolean(options, "active");
|
|
1525
|
+
const includeReplyText = getBoolean(options, "include-reply-text");
|
|
1526
|
+
const authenticatedSendersOnly = getBoolean(options, "authenticated-senders-only");
|
|
1527
|
+
const allowedSenders = webhookAllowedSendersUpdate(options);
|
|
1404
1528
|
const params = {
|
|
1405
1529
|
...url !== void 0 ? { url } : {},
|
|
1406
1530
|
...optionValue(options, "events") !== void 0 ? { events: webhookEvents(options) } : {},
|
|
1407
1531
|
...description !== void 0 ? { description } : {},
|
|
1408
1532
|
...active !== void 0 ? { active } : {},
|
|
1409
1533
|
...authorization !== void 0 ? { authorization } : {},
|
|
1410
|
-
...standardWebhooksSecret !== void 0 ? { standard_webhooks_secret: standardWebhooksSecret } : {}
|
|
1534
|
+
...standardWebhooksSecret !== void 0 ? { standard_webhooks_secret: standardWebhooksSecret } : {},
|
|
1535
|
+
...includeReplyText !== void 0 ? { include_reply_text: includeReplyText } : {},
|
|
1536
|
+
...authenticatedSendersOnly !== void 0 ? { authenticated_senders_only: authenticatedSendersOnly } : {},
|
|
1537
|
+
...allowedSenders !== void 0 ? { allowed_senders: allowedSenders } : {}
|
|
1411
1538
|
};
|
|
1412
1539
|
if (Object.keys(params).length === 0) {
|
|
1413
1540
|
throw new CliUsageError("Nothing to update. Pass at least one webhook option.");
|
|
@@ -1519,6 +1646,44 @@ async function replyToMessageParams(options) {
|
|
|
1519
1646
|
} : {}
|
|
1520
1647
|
};
|
|
1521
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
|
+
}
|
|
1522
1687
|
async function runDomains(client, parsed, out) {
|
|
1523
1688
|
const subcommand = parsed.positional[1];
|
|
1524
1689
|
if (subcommand === "list") {
|
|
@@ -1556,6 +1721,14 @@ async function runDomains(client, parsed, out) {
|
|
|
1556
1721
|
);
|
|
1557
1722
|
return;
|
|
1558
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
|
+
}
|
|
1559
1732
|
if (subcommand === "verify") {
|
|
1560
1733
|
writeJson(
|
|
1561
1734
|
out,
|
|
@@ -1620,6 +1793,21 @@ async function runMailboxes(client, parsed, out) {
|
|
|
1620
1793
|
);
|
|
1621
1794
|
return;
|
|
1622
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
|
+
}
|
|
1623
1811
|
if (subcommand === "suspend") {
|
|
1624
1812
|
writeJson(
|
|
1625
1813
|
out,
|
|
@@ -1887,6 +2075,21 @@ async function runInbox(client, parsed, out) {
|
|
|
1887
2075
|
);
|
|
1888
2076
|
return;
|
|
1889
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
|
+
}
|
|
1890
2093
|
if (subcommand === "threads") {
|
|
1891
2094
|
const attentionState = getString(parsed.options, "attention-state");
|
|
1892
2095
|
const sortBy = getString(parsed.options, "sort-by");
|
|
@@ -1981,13 +2184,64 @@ async function runInbox(client, parsed, out) {
|
|
|
1981
2184
|
);
|
|
1982
2185
|
return;
|
|
1983
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
|
+
}
|
|
1984
2236
|
if (subcommand === "send-draft") {
|
|
2237
|
+
const expectedContentSha256 = getString(parsed.options, "expected-content-sha256");
|
|
1985
2238
|
writeJson(
|
|
1986
2239
|
out,
|
|
1987
2240
|
await client.mailboxes.sendInboxReplyDraft(
|
|
1988
2241
|
requirePositional(parsed.positional, 2, "mailbox id"),
|
|
1989
2242
|
requirePositional(parsed.positional, 3, "thread id"),
|
|
1990
|
-
requirePositional(parsed.positional, 4, "draft id")
|
|
2243
|
+
requirePositional(parsed.positional, 4, "draft id"),
|
|
2244
|
+
expectedContentSha256 ? { expectedContentSha256 } : void 0
|
|
1991
2245
|
)
|
|
1992
2246
|
);
|
|
1993
2247
|
return;
|
|
@@ -2209,6 +2463,9 @@ async function runWebhooks(client, parsed, out) {
|
|
|
2209
2463
|
parsed.options,
|
|
2210
2464
|
"standard-webhooks-secret"
|
|
2211
2465
|
);
|
|
2466
|
+
const includeReplyText = getBoolean(parsed.options, "include-reply-text");
|
|
2467
|
+
const authenticatedSendersOnly = getBoolean(parsed.options, "authenticated-senders-only");
|
|
2468
|
+
const allowedSenders = webhookAllowedSenders(parsed.options);
|
|
2212
2469
|
writeJson(
|
|
2213
2470
|
out,
|
|
2214
2471
|
await client.webhooks.create({
|
|
@@ -2216,7 +2473,10 @@ async function runWebhooks(client, parsed, out) {
|
|
|
2216
2473
|
events: webhookEvents(parsed.options),
|
|
2217
2474
|
...description ? { description } : {},
|
|
2218
2475
|
...authorization !== void 0 ? { authorization } : {},
|
|
2219
|
-
...standardWebhooksSecret !== void 0 ? { standard_webhooks_secret: standardWebhooksSecret } : {}
|
|
2476
|
+
...standardWebhooksSecret !== void 0 ? { standard_webhooks_secret: standardWebhooksSecret } : {},
|
|
2477
|
+
...includeReplyText !== void 0 ? { include_reply_text: includeReplyText } : {},
|
|
2478
|
+
...authenticatedSendersOnly !== void 0 ? { authenticated_senders_only: authenticatedSendersOnly } : {},
|
|
2479
|
+
...allowedSenders !== void 0 ? { allowed_senders: allowedSenders } : {}
|
|
2220
2480
|
})
|
|
2221
2481
|
);
|
|
2222
2482
|
return;
|
|
@@ -2276,14 +2536,27 @@ async function runMembers(client, parsed, out) {
|
|
|
2276
2536
|
}
|
|
2277
2537
|
throw new CliUsageError("Unknown members command.");
|
|
2278
2538
|
}
|
|
2539
|
+
var SUPPRESSION_SCOPES = ["all", "marketing"];
|
|
2279
2540
|
async function runSuppressions(client, parsed, out) {
|
|
2280
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
|
+
}
|
|
2281
2553
|
if (subcommand === "list") {
|
|
2282
2554
|
writeJson(
|
|
2283
2555
|
out,
|
|
2284
2556
|
await client.suppressions.list({
|
|
2285
2557
|
cursor: getString(parsed.options, "cursor"),
|
|
2286
|
-
limit: getNumber(parsed.options, "limit")
|
|
2558
|
+
limit: getNumber(parsed.options, "limit"),
|
|
2559
|
+
scope: getEnum(parsed.options, "scope", SUPPRESSION_SCOPES)
|
|
2287
2560
|
})
|
|
2288
2561
|
);
|
|
2289
2562
|
return;
|
|
@@ -2303,7 +2576,8 @@ var SUBSCRIBER_STATUS_VALUES = [
|
|
|
2303
2576
|
"bounced",
|
|
2304
2577
|
"complained",
|
|
2305
2578
|
"suppressed",
|
|
2306
|
-
"transactional_only"
|
|
2579
|
+
"transactional_only",
|
|
2580
|
+
"dormant"
|
|
2307
2581
|
];
|
|
2308
2582
|
function subscriberListParams(options) {
|
|
2309
2583
|
const email = getString(options, "email");
|
|
@@ -2460,6 +2734,89 @@ async function runAudienceFeed(client, parsed, out) {
|
|
|
2460
2734
|
}
|
|
2461
2735
|
throw new CliUsageError("Unknown audiences feed command.");
|
|
2462
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
|
+
}
|
|
2463
2820
|
async function runAudiences(client, parsed, out) {
|
|
2464
2821
|
const subcommand = parsed.positional[1];
|
|
2465
2822
|
if (subcommand === "subscribers") {
|
|
@@ -2470,6 +2827,10 @@ async function runAudiences(client, parsed, out) {
|
|
|
2470
2827
|
await runAudienceFeed(client, parsed, out);
|
|
2471
2828
|
return;
|
|
2472
2829
|
}
|
|
2830
|
+
if (subcommand === "welcome") {
|
|
2831
|
+
await runAudienceWelcome(client, parsed, out);
|
|
2832
|
+
return;
|
|
2833
|
+
}
|
|
2473
2834
|
if (subcommand === "list") {
|
|
2474
2835
|
writeJson(
|
|
2475
2836
|
out,
|
|
@@ -2503,11 +2864,17 @@ async function runAudiences(client, parsed, out) {
|
|
|
2503
2864
|
const id = requirePositional(parsed.positional, 2, "audience id");
|
|
2504
2865
|
const name = getString(parsed.options, "name");
|
|
2505
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");
|
|
2506
2870
|
writeJson(
|
|
2507
2871
|
out,
|
|
2508
2872
|
await client.audiences.update(id, {
|
|
2509
2873
|
...name ? { name } : {},
|
|
2510
|
-
...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 } : {}
|
|
2511
2878
|
})
|
|
2512
2879
|
);
|
|
2513
2880
|
return;
|
|
@@ -2586,14 +2953,27 @@ async function runNewsletters(client, parsed, out) {
|
|
|
2586
2953
|
return;
|
|
2587
2954
|
}
|
|
2588
2955
|
if (subcommand === "schedule") {
|
|
2956
|
+
const limit = getNumber(parsed.options, "limit");
|
|
2589
2957
|
writeJson(
|
|
2590
2958
|
out,
|
|
2591
2959
|
await client.newsletters.schedule(requirePositional(parsed.positional, 2, "newsletter id"), {
|
|
2592
|
-
scheduled_at: getRequiredString(parsed.options, "scheduled-at")
|
|
2960
|
+
scheduled_at: getRequiredString(parsed.options, "scheduled-at"),
|
|
2961
|
+
...limit !== void 0 ? { limit } : {}
|
|
2593
2962
|
})
|
|
2594
2963
|
);
|
|
2595
2964
|
return;
|
|
2596
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
|
+
}
|
|
2597
2977
|
if (subcommand === "cancel") {
|
|
2598
2978
|
writeJson(
|
|
2599
2979
|
out,
|
|
@@ -2913,6 +3293,93 @@ async function runCalendar(client, parsed, out) {
|
|
|
2913
3293
|
}
|
|
2914
3294
|
throw new CliUsageError("Unknown calendar command.");
|
|
2915
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
|
+
}
|
|
2916
3383
|
async function runBookingPages(client, parsed, out) {
|
|
2917
3384
|
const subcommand = parsed.positional[1];
|
|
2918
3385
|
const options = parsed.options;
|
|
@@ -3053,6 +3520,11 @@ ${JSON.stringify(err.body, null, 2)}
|
|
|
3053
3520
|
if (shipmailError.actionUrl) {
|
|
3054
3521
|
const reason = shipmailError.reason ? ` (${shipmailError.reason})` : "";
|
|
3055
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})
|
|
3056
3528
|
`);
|
|
3057
3529
|
}
|
|
3058
3530
|
return;
|
|
@@ -3137,6 +3609,7 @@ async function runCli(argv, options = {}) {
|
|
|
3137
3609
|
else if (command === "audiences") await runAudiences(client, parsed, out);
|
|
3138
3610
|
else if (command === "calendar") await runCalendar(client, parsed, out);
|
|
3139
3611
|
else if (command === "booking-pages") await runBookingPages(client, parsed, out);
|
|
3612
|
+
else if (command === "booking-links") await runBookingLinks(client, parsed, out);
|
|
3140
3613
|
else throw new CliUsageError(`Unknown command: ${command}.`);
|
|
3141
3614
|
return 0;
|
|
3142
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": {
|