@develemail/cli 0.1.6 → 0.1.7
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 +43 -0
- package/dist/index.js +387 -42
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @develemail/cli
|
|
2
|
+
|
|
3
|
+
The `devel` command-line tool for DevelEmail: manage projects, domains and API
|
|
4
|
+
keys, publish DNS, and send test emails.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install -g @develemail/cli
|
|
8
|
+
devel --version
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Onboarding
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
devel login # opens a browser; saves an owner session
|
|
15
|
+
devel domains add example.com # prints the domain ID
|
|
16
|
+
devel domains records <id> # DNS records to publish (--format bind)
|
|
17
|
+
devel domains publish-dns <id> # or publish them to Cloudflare for you
|
|
18
|
+
devel domains verify <id> --wait # poll until DNS verifies
|
|
19
|
+
devel init --project my-app --from noreply@example.com
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`devel login` defaults to `--url https://develemail.com/api`. The session is
|
|
23
|
+
saved to `~/.config/develemail/auth.json`.
|
|
24
|
+
|
|
25
|
+
`publish-dns` needs a Cloudflare token connected first:
|
|
26
|
+
`devel cloudflare connect --token-env CLOUDFLARE_API_TOKEN`.
|
|
27
|
+
|
|
28
|
+
## Receiving mail
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
devel inbound add example.com privacy --if-missing # inbox-only mailbox
|
|
32
|
+
devel inbound list example.com
|
|
33
|
+
devel inbound check example.com # does the MX reach us?
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`devel init` creates the project and an API key and writes
|
|
37
|
+
`DEVELEMAIL_BASE_URL` and `DEVELEMAIL_API_KEY` to your env files. After that,
|
|
38
|
+
`devel send`, `devel logs` and `devel tail` use that API key.
|
|
39
|
+
|
|
40
|
+
## Reference
|
|
41
|
+
|
|
42
|
+
Every command and option is documented in the
|
|
43
|
+
[CLI reference](https://docs.develemail.com/cli-reference) on the DevelEmail docs site.
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/
|
|
4
|
-
import { Command as
|
|
3
|
+
// src/program.ts
|
|
4
|
+
import { Command as Command13 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/send.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -210,15 +210,46 @@ function openBrowser(url, deps = {}) {
|
|
|
210
210
|
return true;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
// src/resolve-web-app-url.ts
|
|
214
|
+
var DISCOVERY_TIMEOUT_MS = 3e3;
|
|
215
|
+
function trimSlash(url) {
|
|
216
|
+
return url.replace(/\/+$/, "");
|
|
217
|
+
}
|
|
218
|
+
function legacyWebAppUrl(apiUrl) {
|
|
219
|
+
return trimSlash(apiUrl).replace(/\/api$/, "");
|
|
220
|
+
}
|
|
221
|
+
async function resolveWebAppUrl(apiUrl, fetchImpl = fetch, options = {}) {
|
|
222
|
+
if (options.appUrl?.trim()) return trimSlash(options.appUrl.trim());
|
|
223
|
+
const base = trimSlash(apiUrl);
|
|
224
|
+
try {
|
|
225
|
+
const res = await fetchImpl(`${base}/meta/public-urls`, {
|
|
226
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? DISCOVERY_TIMEOUT_MS)
|
|
227
|
+
});
|
|
228
|
+
if (!res.ok) return legacyWebAppUrl(base);
|
|
229
|
+
const body = await res.json();
|
|
230
|
+
return typeof body.webAppUrl === "string" && body.webAppUrl.trim() ? trimSlash(body.webAppUrl.trim()) : legacyWebAppUrl(base);
|
|
231
|
+
} catch {
|
|
232
|
+
return legacyWebAppUrl(base);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
213
236
|
// src/commands/login.ts
|
|
214
|
-
var
|
|
215
|
-
|
|
216
|
-
"
|
|
217
|
-
"
|
|
237
|
+
var DEFAULT_API_URL = "https://develemail.com/api";
|
|
238
|
+
var loginCommand = new Command4("login").description("Authenticate with your DevelEmail instance via browser").option("--url <url>", "Base URL of your DevelEmail API", DEFAULT_API_URL).option(
|
|
239
|
+
"--app-url <url>",
|
|
240
|
+
"Dashboard URL to open for sign-in (discovered from the API by default)"
|
|
218
241
|
).action(async (opts) => {
|
|
242
|
+
if (!isHttpUrl(opts.url) || opts.appUrl && !isHttpUrl(opts.appUrl)) {
|
|
243
|
+
console.error("--url must be an http(s) URL");
|
|
244
|
+
process.exit(1);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
219
247
|
const apiUrl = opts.url.replace(/\/$/, "");
|
|
220
|
-
const
|
|
248
|
+
const webAppUrl = await resolveWebAppUrl(apiUrl, fetch, {
|
|
249
|
+
appUrl: opts.appUrl
|
|
250
|
+
});
|
|
221
251
|
await new Promise((resolve2, reject) => {
|
|
252
|
+
let timeout;
|
|
222
253
|
const server = createServer(
|
|
223
254
|
(req, res) => {
|
|
224
255
|
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
@@ -240,6 +271,7 @@ var loginCommand = new Command4("login").description("Authenticate with your Dev
|
|
|
240
271
|
);
|
|
241
272
|
console.log("Authenticated successfully.");
|
|
242
273
|
console.log("Session saved to ~/.config/develemail/auth.json");
|
|
274
|
+
clearTimeout(timeout);
|
|
243
275
|
server.close();
|
|
244
276
|
resolve2();
|
|
245
277
|
}
|
|
@@ -251,13 +283,16 @@ var loginCommand = new Command4("login").description("Authenticate with your Dev
|
|
|
251
283
|
return;
|
|
252
284
|
}
|
|
253
285
|
const port = addr.port;
|
|
254
|
-
const authUrl = `${
|
|
286
|
+
const authUrl = `${webAppUrl}/cli-auth?port=${port}`;
|
|
255
287
|
console.log("Opening browser for authentication...");
|
|
256
288
|
console.log(`If the browser doesn't open, visit: ${authUrl}`);
|
|
257
289
|
openBrowser(authUrl);
|
|
258
290
|
});
|
|
259
|
-
server.on("error",
|
|
260
|
-
|
|
291
|
+
server.on("error", (err) => {
|
|
292
|
+
clearTimeout(timeout);
|
|
293
|
+
reject(err);
|
|
294
|
+
});
|
|
295
|
+
timeout = setTimeout(
|
|
261
296
|
() => {
|
|
262
297
|
server.close();
|
|
263
298
|
reject(new Error("Authentication timed out after 5 minutes"));
|
|
@@ -294,6 +329,21 @@ var COL_ENV = 12;
|
|
|
294
329
|
function col2(s, w) {
|
|
295
330
|
return (s ?? "").slice(0, w).padEnd(w);
|
|
296
331
|
}
|
|
332
|
+
function formatCooldown(minutes) {
|
|
333
|
+
if (minutes == null) return "platform default";
|
|
334
|
+
return minutes === 0 ? "disabled" : `${minutes} min`;
|
|
335
|
+
}
|
|
336
|
+
function parseCooldownMinutes(raw) {
|
|
337
|
+
if (raw === "default") return null;
|
|
338
|
+
const n = Number(raw);
|
|
339
|
+
if (!Number.isInteger(n) || n < 0 || n > 1440) {
|
|
340
|
+
console.error(
|
|
341
|
+
'Error: --recipient-cooldown must be 0-1440 minutes, or "default"'
|
|
342
|
+
);
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
return n;
|
|
346
|
+
}
|
|
297
347
|
var listCommand = new Command5("list").description("List all projects").action(async () => {
|
|
298
348
|
const { url, token } = loadOwnerSession();
|
|
299
349
|
const projects = await apiRequest(`${url}/projects`, token);
|
|
@@ -320,6 +370,9 @@ var getCommand = new Command5("get").description("Get project details").argument
|
|
|
320
370
|
console.log(
|
|
321
371
|
`Rate Limit: ${project.rateLimitPerDay ?? "Unlimited"} per day`
|
|
322
372
|
);
|
|
373
|
+
console.log(
|
|
374
|
+
`Recipient Cooldown: ${formatCooldown(project.recipientCooldownMinutes)}`
|
|
375
|
+
);
|
|
323
376
|
if (project.domainId) {
|
|
324
377
|
console.log(`Domain ID: ${project.domainId}`);
|
|
325
378
|
}
|
|
@@ -346,10 +399,33 @@ var createCommand = new Command5("create").description("Create a new project").r
|
|
|
346
399
|
console.log(`Created project: ${project.name} (${project.id})`);
|
|
347
400
|
}
|
|
348
401
|
);
|
|
349
|
-
var
|
|
402
|
+
var updateCommand = new Command5("update").description("Update project settings").argument("<id>", "Project ID").option(
|
|
403
|
+
"--recipient-cooldown <minutes>",
|
|
404
|
+
'Minutes before the same subject can be re-sent to a recipient (0 disables, "default" restores the platform default)'
|
|
405
|
+
).action(async (id, opts) => {
|
|
406
|
+
const body = {};
|
|
407
|
+
if (opts.recipientCooldown !== void 0) {
|
|
408
|
+
body.recipientCooldownMinutes = parseCooldownMinutes(
|
|
409
|
+
opts.recipientCooldown
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
if (Object.keys(body).length === 0) {
|
|
413
|
+
console.error("Error: nothing to update; pass --recipient-cooldown");
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
const { url, token } = loadOwnerSession();
|
|
417
|
+
const project = await apiRequest(`${url}/projects/${id}`, token, {
|
|
418
|
+
method: "PATCH",
|
|
419
|
+
body: JSON.stringify(body)
|
|
420
|
+
});
|
|
421
|
+
console.log(
|
|
422
|
+
`Updated project: ${project.name} (recipient cooldown: ${formatCooldown(project.recipientCooldownMinutes)})`
|
|
423
|
+
);
|
|
424
|
+
});
|
|
425
|
+
var projectCommand = new Command5("project").description("Manage projects").addCommand(listCommand).addCommand(getCommand).addCommand(createCommand).addCommand(updateCommand);
|
|
350
426
|
|
|
351
427
|
// src/commands/domains.ts
|
|
352
|
-
import { Command as
|
|
428
|
+
import { Command as Command8 } from "commander";
|
|
353
429
|
|
|
354
430
|
// src/owner-api.ts
|
|
355
431
|
var OwnerApiError = class extends Error {
|
|
@@ -365,7 +441,7 @@ async function ownerRequest(path, init) {
|
|
|
365
441
|
...init,
|
|
366
442
|
headers: {
|
|
367
443
|
Cookie: `session=${token}`,
|
|
368
|
-
"Content-Type": "application/json",
|
|
444
|
+
...init?.body != null ? { "Content-Type": "application/json" } : {},
|
|
369
445
|
...init?.headers ?? {}
|
|
370
446
|
}
|
|
371
447
|
});
|
|
@@ -379,6 +455,7 @@ async function ownerRequest(path, init) {
|
|
|
379
455
|
const body = await res.json().catch(() => ({}));
|
|
380
456
|
throw new OwnerApiError(body.message ?? res.statusText, res.status);
|
|
381
457
|
}
|
|
458
|
+
if (res.status === 204) return void 0;
|
|
382
459
|
return res.json();
|
|
383
460
|
}
|
|
384
461
|
function exitOnError(fn) {
|
|
@@ -481,6 +558,63 @@ var publishDnsCommand = new Command6("publish-dns").description("Publish a domai
|
|
|
481
558
|
})
|
|
482
559
|
);
|
|
483
560
|
|
|
561
|
+
// src/commands/domains-warmup.ts
|
|
562
|
+
import { Command as Command7 } from "commander";
|
|
563
|
+
var FULLY_WARMED_STAGE = 7;
|
|
564
|
+
var STAGE_LIMITS = {
|
|
565
|
+
1: "50",
|
|
566
|
+
2: "100",
|
|
567
|
+
3: "200",
|
|
568
|
+
4: "400",
|
|
569
|
+
5: "800",
|
|
570
|
+
6: "1,600",
|
|
571
|
+
7: "Unlimited"
|
|
572
|
+
};
|
|
573
|
+
function printWarmupStatus(d) {
|
|
574
|
+
console.log(`Domain: ${d.name}`);
|
|
575
|
+
if (d.warmupStage === null) {
|
|
576
|
+
console.log("Warmup: not started");
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
console.log(
|
|
580
|
+
d.warmupStage >= FULLY_WARMED_STAGE ? "Stage: Fully warmed" : `Stage: ${d.warmupStage} of ${FULLY_WARMED_STAGE}`
|
|
581
|
+
);
|
|
582
|
+
console.log(`Daily limit: ${STAGE_LIMITS[d.warmupStage] ?? "Unknown"}`);
|
|
583
|
+
if (d.warmupPausedAt) {
|
|
584
|
+
console.log(`Paused since ${new Date(d.warmupPausedAt).toLocaleString()}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
async function runWarmup(id, opts) {
|
|
588
|
+
if (opts.pause && opts.resume) {
|
|
589
|
+
throw new Error("Use either --pause or --resume, not both.");
|
|
590
|
+
}
|
|
591
|
+
const patch = (warmupPausedAt) => ownerRequest(`/domains/${id}`, {
|
|
592
|
+
method: "PATCH",
|
|
593
|
+
body: JSON.stringify({ warmupPausedAt })
|
|
594
|
+
});
|
|
595
|
+
let domain;
|
|
596
|
+
if (opts.pause) {
|
|
597
|
+
domain = await patch((/* @__PURE__ */ new Date()).toISOString());
|
|
598
|
+
} else if (opts.resume) {
|
|
599
|
+
domain = await patch(null);
|
|
600
|
+
} else {
|
|
601
|
+
domain = await ownerRequest(`/domains/${id}`);
|
|
602
|
+
if (domain.warmupStage === null) {
|
|
603
|
+
domain = await ownerRequest(
|
|
604
|
+
`/domains/${id}/warmup/enable`,
|
|
605
|
+
{
|
|
606
|
+
method: "POST"
|
|
607
|
+
}
|
|
608
|
+
);
|
|
609
|
+
} else {
|
|
610
|
+
console.log("Warmup already in progress");
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (opts.json) console.log(JSON.stringify(domain, null, 2));
|
|
614
|
+
else printWarmupStatus(domain);
|
|
615
|
+
}
|
|
616
|
+
var warmupCommand = new Command7("warmup").description("Start, pause or resume domain warmup").argument("<id>", "Domain ID").option("--pause", "Pause the warmup ramp").option("--resume", "Resume a paused warmup ramp").option("--json", "Output the domain as JSON").action(exitOnError(runWarmup));
|
|
617
|
+
|
|
484
618
|
// src/commands/domains.ts
|
|
485
619
|
var COL_ID3 = 40;
|
|
486
620
|
var COL_NAME2 = 30;
|
|
@@ -493,7 +627,7 @@ function printStatuses(d) {
|
|
|
493
627
|
console.log(`DKIM Status: ${d.dkimStatus ?? "\u2014"}`);
|
|
494
628
|
console.log(`DMARC Status: ${d.dmarcStatus ?? "\u2014"}`);
|
|
495
629
|
}
|
|
496
|
-
var listCommand2 = new
|
|
630
|
+
var listCommand2 = new Command8("list").description("List all domains").action(
|
|
497
631
|
exitOnError(async () => {
|
|
498
632
|
const domains = await ownerRequest("/domains");
|
|
499
633
|
if (domains.length === 0) {
|
|
@@ -511,7 +645,7 @@ var listCommand2 = new Command7("list").description("List all domains").action(
|
|
|
511
645
|
}
|
|
512
646
|
})
|
|
513
647
|
);
|
|
514
|
-
var getCommand2 = new
|
|
648
|
+
var getCommand2 = new Command8("get").description("Get domain details").argument("<id>", "Domain ID").action(
|
|
515
649
|
exitOnError(async (id) => {
|
|
516
650
|
const domain = await ownerRequest(`/domains/${id}`);
|
|
517
651
|
console.log(`ID: ${domain.id}`);
|
|
@@ -527,7 +661,7 @@ ${domain.dkimPublicKey}`);
|
|
|
527
661
|
console.log(`Created At: ${new Date(domain.createdAt).toISOString()}`);
|
|
528
662
|
})
|
|
529
663
|
);
|
|
530
|
-
var addCommand = new
|
|
664
|
+
var addCommand = new Command8("add").description("Create a domain and print its required DNS records").argument("<name>", "Domain name, e.g. example.com").option("--dkim-selector <selector>", "DKIM selector").option("--json", "Output JSON").action(
|
|
531
665
|
exitOnError(
|
|
532
666
|
async (name, opts) => {
|
|
533
667
|
const domain = await ownerRequest("/domains", {
|
|
@@ -547,7 +681,7 @@ var addCommand = new Command7("add").description("Create a domain and print its
|
|
|
547
681
|
}
|
|
548
682
|
)
|
|
549
683
|
);
|
|
550
|
-
var recordsCommand = new
|
|
684
|
+
var recordsCommand = new Command8("records").description("Print the DNS records a domain requires").argument("<id>", "Domain ID").option("--json", "Output JSON").option("--format <format>", "table | bind", "table").action(
|
|
551
685
|
exitOnError(
|
|
552
686
|
async (id, opts) => {
|
|
553
687
|
const records = await ownerRequest(
|
|
@@ -559,7 +693,7 @@ var recordsCommand = new Command7("records").description("Print the DNS records
|
|
|
559
693
|
);
|
|
560
694
|
var POLL_INTERVAL_MS2 = 1e4;
|
|
561
695
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
562
|
-
var verifyCommand = new
|
|
696
|
+
var verifyCommand = new Command8("verify").description("Trigger domain verification").argument("<id>", "Domain ID").option("--wait", "Poll until verified or timed out").option("--timeout <seconds>", "Max seconds to wait with --wait", "600").action(
|
|
563
697
|
exitOnError(
|
|
564
698
|
async (id, opts) => {
|
|
565
699
|
const deadline = Date.now() + Number(opts.timeout) * 1e3;
|
|
@@ -582,11 +716,209 @@ var verifyCommand = new Command7("verify").description("Trigger domain verificat
|
|
|
582
716
|
}
|
|
583
717
|
)
|
|
584
718
|
);
|
|
585
|
-
var domainsCommand = new
|
|
719
|
+
var domainsCommand = new Command8("domains").description("Manage domains").addCommand(listCommand2).addCommand(getCommand2).addCommand(addCommand).addCommand(recordsCommand).addCommand(publishDnsCommand).addCommand(verifyCommand).addCommand(warmupCommand);
|
|
720
|
+
|
|
721
|
+
// src/commands/inbound.ts
|
|
722
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
723
|
+
import { Command as Command9 } from "commander";
|
|
724
|
+
|
|
725
|
+
// src/commands/inbound-check.ts
|
|
726
|
+
import { resolve4, resolveMx } from "dns/promises";
|
|
727
|
+
var INBOUND_HOST = "mail.develemail.com";
|
|
728
|
+
async function checkMx(domain) {
|
|
729
|
+
const [mx, inbound] = await Promise.all([
|
|
730
|
+
resolveMx(domain),
|
|
731
|
+
resolve4(INBOUND_HOST)
|
|
732
|
+
]);
|
|
733
|
+
const checks = await Promise.all(
|
|
734
|
+
mx.map(async (r) => {
|
|
735
|
+
const addresses = await resolve4(r.exchange).catch(() => []);
|
|
736
|
+
return {
|
|
737
|
+
exchange: r.exchange,
|
|
738
|
+
priority: r.priority,
|
|
739
|
+
addresses,
|
|
740
|
+
pointsAtInbound: addresses.some((a) => inbound.includes(a))
|
|
741
|
+
};
|
|
742
|
+
})
|
|
743
|
+
);
|
|
744
|
+
return checks.sort((a, b) => a.priority - b.priority);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// src/commands/inbound.ts
|
|
748
|
+
var COL_ADDRESS = 32;
|
|
749
|
+
var COL_ACTIVE = 8;
|
|
750
|
+
var COL_WEBHOOK = 36;
|
|
751
|
+
var COL_NOTIFY = 30;
|
|
752
|
+
function col4(s, w) {
|
|
753
|
+
return s.slice(0, w).padEnd(w);
|
|
754
|
+
}
|
|
755
|
+
function toAddress(localPart) {
|
|
756
|
+
return localPart === "*" ? null : localPart;
|
|
757
|
+
}
|
|
758
|
+
function label(address) {
|
|
759
|
+
return address ?? "*";
|
|
760
|
+
}
|
|
761
|
+
async function resolveDomain(nameOrId) {
|
|
762
|
+
const domains = await ownerRequest("/domains");
|
|
763
|
+
const match = domains.find(
|
|
764
|
+
(d) => d.id === nameOrId || d.name === nameOrId.toLowerCase()
|
|
765
|
+
);
|
|
766
|
+
if (!match) throw new Error(`Domain not found: ${nameOrId}`);
|
|
767
|
+
return match;
|
|
768
|
+
}
|
|
769
|
+
async function findRoute(domain, localPart) {
|
|
770
|
+
const address = toAddress(localPart.toLowerCase());
|
|
771
|
+
const routes = await ownerRequest(
|
|
772
|
+
`/domains/${domain.id}/inbound-routes`
|
|
773
|
+
);
|
|
774
|
+
const route = routes.find((r) => r.address === address);
|
|
775
|
+
if (!route) {
|
|
776
|
+
throw new Error(`No inbound route for ${label(address)}@${domain.name}`);
|
|
777
|
+
}
|
|
778
|
+
return route;
|
|
779
|
+
}
|
|
780
|
+
function noneToNull(value) {
|
|
781
|
+
return value === "none" ? null : value;
|
|
782
|
+
}
|
|
783
|
+
async function confirm2(question) {
|
|
784
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
785
|
+
try {
|
|
786
|
+
return /^y(es)?$/i.test((await rl.question(question)).trim());
|
|
787
|
+
} finally {
|
|
788
|
+
rl.close();
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
var listCommand3 = new Command9("list").description("List inbound routes for a domain").argument("<domain>", "Domain name or ID").option("--json", "Output JSON").action(
|
|
792
|
+
exitOnError(async (domainArg, opts) => {
|
|
793
|
+
const domain = await resolveDomain(domainArg);
|
|
794
|
+
const routes = await ownerRequest(
|
|
795
|
+
`/domains/${domain.id}/inbound-routes`
|
|
796
|
+
);
|
|
797
|
+
if (opts.json) {
|
|
798
|
+
console.log(JSON.stringify(routes, null, 2));
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (routes.length === 0) {
|
|
802
|
+
console.log(`No inbound routes for ${domain.name}.`);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const header = `${col4("ADDRESS", COL_ADDRESS)} ${col4("ACTIVE", COL_ACTIVE)} ${col4("WEBHOOK", COL_WEBHOOK)} ${col4("NOTIFY", COL_NOTIFY)} CREATED`;
|
|
806
|
+
console.log(header);
|
|
807
|
+
console.log("-".repeat(header.length + 10));
|
|
808
|
+
for (const r of routes) {
|
|
809
|
+
console.log(
|
|
810
|
+
`${col4(`${label(r.address)}@${domain.name}`, COL_ADDRESS)} ${col4(r.active ? "yes" : "no", COL_ACTIVE)} ${col4(r.webhookUrl ?? "-", COL_WEBHOOK)} ${col4(r.notifyEmail ?? "-", COL_NOTIFY)} ${new Date(r.createdAt).toLocaleDateString()}`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
})
|
|
814
|
+
);
|
|
815
|
+
var addCommand2 = new Command9("add").description("Create an inbound route (use * for a catch-all)").argument("<domain>", "Domain name or ID").argument("<local-part>", "Local part, or * for catch-all").option("--webhook <url>", "Deliver received mail to this webhook").option("--notify <email>", "Send an arrival notification to this address").option("--if-missing", "Exit 0 if the route already exists").action(
|
|
816
|
+
exitOnError(
|
|
817
|
+
async (domainArg, localPart, opts) => {
|
|
818
|
+
const domain = await resolveDomain(domainArg);
|
|
819
|
+
const address = toAddress(localPart);
|
|
820
|
+
try {
|
|
821
|
+
const route = await ownerRequest(
|
|
822
|
+
`/domains/${domain.id}/inbound-routes`,
|
|
823
|
+
{
|
|
824
|
+
method: "POST",
|
|
825
|
+
body: JSON.stringify({
|
|
826
|
+
address,
|
|
827
|
+
webhookUrl: opts.webhook ?? null,
|
|
828
|
+
notifyEmail: opts.notify ?? null
|
|
829
|
+
})
|
|
830
|
+
}
|
|
831
|
+
);
|
|
832
|
+
console.log(
|
|
833
|
+
`Created inbound route ${label(address)}@${domain.name} (${route.id})`
|
|
834
|
+
);
|
|
835
|
+
} catch (err) {
|
|
836
|
+
if (opts.ifMissing && err instanceof OwnerApiError && err.status === 409) {
|
|
837
|
+
console.log(
|
|
838
|
+
`Inbound route ${label(address)}@${domain.name} already exists`
|
|
839
|
+
);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
throw err;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
)
|
|
846
|
+
);
|
|
847
|
+
var updateCommand2 = new Command9("update").description("Update an inbound route").argument("<domain>", "Domain name or ID").argument("<local-part>", "Local part, or * for catch-all").option("--webhook <url|none>", "Set or clear the webhook").option("--notify <email|none>", "Set or clear the notify email").option("--active", "Enable the route").option("--inactive", "Disable the route").action(
|
|
848
|
+
exitOnError(
|
|
849
|
+
async (domainArg, localPart, opts) => {
|
|
850
|
+
if (opts.active && opts.inactive) {
|
|
851
|
+
throw new Error("Pass only one of --active and --inactive");
|
|
852
|
+
}
|
|
853
|
+
const domain = await resolveDomain(domainArg);
|
|
854
|
+
const route = await findRoute(domain, localPart);
|
|
855
|
+
const body = {
|
|
856
|
+
webhookUrl: noneToNull(opts.webhook),
|
|
857
|
+
notifyEmail: noneToNull(opts.notify),
|
|
858
|
+
active: opts.active ? true : opts.inactive ? false : void 0
|
|
859
|
+
};
|
|
860
|
+
await ownerRequest(
|
|
861
|
+
`/domains/${domain.id}/inbound-routes/${route.id}`,
|
|
862
|
+
{ method: "PATCH", body: JSON.stringify(body) }
|
|
863
|
+
);
|
|
864
|
+
console.log(`Updated ${label(route.address)}@${domain.name}`);
|
|
865
|
+
}
|
|
866
|
+
)
|
|
867
|
+
);
|
|
868
|
+
var removeCommand = new Command9("remove").description("Delete an inbound route").argument("<domain>", "Domain name or ID").argument("<local-part>", "Local part, or * for catch-all").option("--yes", "Delete without prompting").action(
|
|
869
|
+
exitOnError(
|
|
870
|
+
async (domainArg, localPart, opts) => {
|
|
871
|
+
const domain = await resolveDomain(domainArg);
|
|
872
|
+
const route = await findRoute(domain, localPart);
|
|
873
|
+
const target = `${label(route.address)}@${domain.name}`;
|
|
874
|
+
const ok = opts.yes || process.stdin.isTTY === true && await confirm2(`Delete inbound route ${target}? [y/N] `);
|
|
875
|
+
if (!ok) {
|
|
876
|
+
console.log("Nothing deleted. Re-run with --yes to delete.");
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
await ownerRequest(
|
|
880
|
+
`/domains/${domain.id}/inbound-routes/${route.id}`,
|
|
881
|
+
{ method: "DELETE" }
|
|
882
|
+
);
|
|
883
|
+
console.log(`Deleted ${target}`);
|
|
884
|
+
}
|
|
885
|
+
)
|
|
886
|
+
);
|
|
887
|
+
var checkCommand = new Command9("check").description("Show whether a domain MX points at the inbound server").argument("<domain>", "Domain name or ID").option("--json", "Output JSON").action(
|
|
888
|
+
exitOnError(async (domainArg, opts) => {
|
|
889
|
+
const ref = await resolveDomain(domainArg);
|
|
890
|
+
const [domain, mx] = await Promise.all([
|
|
891
|
+
ownerRequest(`/domains/${ref.id}`),
|
|
892
|
+
checkMx(ref.name)
|
|
893
|
+
]);
|
|
894
|
+
const ok = mx.some((r) => r.pointsAtInbound);
|
|
895
|
+
if (opts.json) {
|
|
896
|
+
console.log(
|
|
897
|
+
JSON.stringify(
|
|
898
|
+
{ mxStatus: domain.mxStatus ?? null, ok, mx },
|
|
899
|
+
null,
|
|
900
|
+
2
|
|
901
|
+
)
|
|
902
|
+
);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
console.log(`MX Status: ${domain.mxStatus ?? "\u2014"}`);
|
|
906
|
+
if (mx.length === 0) console.log("No MX records found.");
|
|
907
|
+
for (const r of mx) {
|
|
908
|
+
console.log(
|
|
909
|
+
`MX ${r.priority} ${r.exchange} -> ${r.addresses.join(", ") || "unresolved"} ${r.pointsAtInbound ? "(inbound server)" : "(not the inbound server)"}`
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
console.log(
|
|
913
|
+
ok ? `OK: mail for ${ref.name} reaches ${INBOUND_HOST}.` : `Mail for ${ref.name} does not reach ${INBOUND_HOST}; point its MX at ${INBOUND_HOST}.`
|
|
914
|
+
);
|
|
915
|
+
})
|
|
916
|
+
);
|
|
917
|
+
var inboundCommand = new Command9("inbound").description("Manage inbound mailboxes").addCommand(listCommand3).addCommand(addCommand2).addCommand(updateCommand2).addCommand(removeCommand).addCommand(checkCommand);
|
|
586
918
|
|
|
587
919
|
// src/commands/cloudflare.ts
|
|
588
|
-
import { Command as
|
|
589
|
-
var connectCommand = new
|
|
920
|
+
import { Command as Command10 } from "commander";
|
|
921
|
+
var connectCommand = new Command10("connect").description("Connect a Cloudflare API token to your organization").requiredOption(
|
|
590
922
|
"--token-env <VAR>",
|
|
591
923
|
"Environment variable holding the Cloudflare API token"
|
|
592
924
|
).action(
|
|
@@ -602,7 +934,7 @@ var connectCommand = new Command8("connect").description("Connect a Cloudflare A
|
|
|
602
934
|
console.log("Cloudflare connected.");
|
|
603
935
|
})
|
|
604
936
|
);
|
|
605
|
-
var statusCommand = new
|
|
937
|
+
var statusCommand = new Command10("status").description("Show whether Cloudflare is connected").action(
|
|
606
938
|
exitOnError(async () => {
|
|
607
939
|
const { connected } = await ownerRequest(
|
|
608
940
|
"/cloudflare/status"
|
|
@@ -610,7 +942,7 @@ var statusCommand = new Command8("status").description("Show whether Cloudflare
|
|
|
610
942
|
console.log(connected ? "Connected" : "Not connected");
|
|
611
943
|
})
|
|
612
944
|
);
|
|
613
|
-
var zonesCommand = new
|
|
945
|
+
var zonesCommand = new Command10("zones").description("List Cloudflare zones").option("--json", "Output JSON").action(
|
|
614
946
|
exitOnError(async (opts) => {
|
|
615
947
|
const zones = await ownerRequest("/cloudflare/zones");
|
|
616
948
|
if (opts.json) {
|
|
@@ -620,10 +952,10 @@ var zonesCommand = new Command8("zones").description("List Cloudflare zones").op
|
|
|
620
952
|
for (const z of zones) console.log(`${z.id} ${z.name} ${z.status}`);
|
|
621
953
|
})
|
|
622
954
|
);
|
|
623
|
-
var cloudflareCommand = new
|
|
955
|
+
var cloudflareCommand = new Command10("cloudflare").description("Manage the Cloudflare integration").addCommand(connectCommand).addCommand(statusCommand).addCommand(zonesCommand);
|
|
624
956
|
|
|
625
957
|
// src/commands/key.ts
|
|
626
|
-
import { Command as
|
|
958
|
+
import { Command as Command11 } from "commander";
|
|
627
959
|
function ownerHeaders2(token) {
|
|
628
960
|
return { Cookie: `session=${token}`, "Content-Type": "application/json" };
|
|
629
961
|
}
|
|
@@ -639,7 +971,7 @@ async function apiRequest2(url, token, init) {
|
|
|
639
971
|
}
|
|
640
972
|
return res.json();
|
|
641
973
|
}
|
|
642
|
-
var
|
|
974
|
+
var listCommand4 = new Command11("list").description("List API keys for a project").requiredOption("--project <id>", "Project ID").action(async (opts) => {
|
|
643
975
|
const { url, token } = loadOwnerSession();
|
|
644
976
|
const keys = await apiRequest2(
|
|
645
977
|
`${url}/projects/${opts.project}/api-keys`,
|
|
@@ -654,7 +986,7 @@ var listCommand3 = new Command9("list").description("List API keys for a project
|
|
|
654
986
|
console.log(`${status} ${k.keyPrefix}\u2026 ${k.name} (${k.id})`);
|
|
655
987
|
}
|
|
656
988
|
});
|
|
657
|
-
var createCommand2 = new
|
|
989
|
+
var createCommand2 = new Command11("create").description("Create a new API key for a project").requiredOption("--project <id>", "Project ID").requiredOption("--name <name>", "Key name (e.g. production, ci)").option("--expires <date>", "Expiry date (ISO-8601, e.g. 2027-01-01)").option("--rate-limit <n>", "Max requests per minute").action(
|
|
658
990
|
async (opts) => {
|
|
659
991
|
const { url, token } = loadOwnerSession();
|
|
660
992
|
const result = await apiRequest2(
|
|
@@ -676,10 +1008,10 @@ var createCommand2 = new Command9("create").description("Create a new API key fo
|
|
|
676
1008
|
console.log("Store the secret now \u2014 it will not be shown again.");
|
|
677
1009
|
}
|
|
678
1010
|
);
|
|
679
|
-
var keyCommand = new
|
|
1011
|
+
var keyCommand = new Command11("key").description("Manage API keys").addCommand(listCommand4).addCommand(createCommand2);
|
|
680
1012
|
|
|
681
1013
|
// src/commands/init.ts
|
|
682
|
-
import { Command as
|
|
1014
|
+
import { Command as Command12 } from "commander";
|
|
683
1015
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
684
1016
|
import { resolve } from "path";
|
|
685
1017
|
function ownerHeaders3(token) {
|
|
@@ -709,7 +1041,7 @@ function upsertEnvFile(filePath, vars) {
|
|
|
709
1041
|
const content = lines.join("\n").trimEnd() + (lines.some((l) => l.trim()) ? "\n" : "") + appended + "\n";
|
|
710
1042
|
writeFileSync2(filePath, content, "utf-8");
|
|
711
1043
|
}
|
|
712
|
-
var initCommand = new
|
|
1044
|
+
var initCommand = new Command12("init").description(
|
|
713
1045
|
"Provision a project + API key and write credentials to an env file"
|
|
714
1046
|
).requiredOption(
|
|
715
1047
|
"--project <name>",
|
|
@@ -762,16 +1094,29 @@ var initCommand = new Command10("init").description(
|
|
|
762
1094
|
}
|
|
763
1095
|
);
|
|
764
1096
|
|
|
1097
|
+
// src/version.ts
|
|
1098
|
+
import { createRequire } from "module";
|
|
1099
|
+
var require2 = createRequire(import.meta.url);
|
|
1100
|
+
function readCliVersion() {
|
|
1101
|
+
return require2("../package.json").version;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// src/program.ts
|
|
1105
|
+
function buildProgram() {
|
|
1106
|
+
const program = new Command13().name("devel").description("DevelEmail CLI \u2014 interact with your DevelEmail instance").version(readCliVersion());
|
|
1107
|
+
program.addCommand(loginCommand);
|
|
1108
|
+
program.addCommand(logoutCommand);
|
|
1109
|
+
program.addCommand(projectCommand);
|
|
1110
|
+
program.addCommand(domainsCommand);
|
|
1111
|
+
program.addCommand(inboundCommand);
|
|
1112
|
+
program.addCommand(cloudflareCommand);
|
|
1113
|
+
program.addCommand(keyCommand);
|
|
1114
|
+
program.addCommand(initCommand);
|
|
1115
|
+
program.addCommand(sendCommand);
|
|
1116
|
+
program.addCommand(logsCommand);
|
|
1117
|
+
program.addCommand(tailCommand);
|
|
1118
|
+
return program;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
765
1121
|
// src/index.ts
|
|
766
|
-
|
|
767
|
-
program.addCommand(loginCommand);
|
|
768
|
-
program.addCommand(logoutCommand);
|
|
769
|
-
program.addCommand(projectCommand);
|
|
770
|
-
program.addCommand(domainsCommand);
|
|
771
|
-
program.addCommand(cloudflareCommand);
|
|
772
|
-
program.addCommand(keyCommand);
|
|
773
|
-
program.addCommand(initCommand);
|
|
774
|
-
program.addCommand(sendCommand);
|
|
775
|
-
program.addCommand(logsCommand);
|
|
776
|
-
program.addCommand(tailCommand);
|
|
777
|
-
program.parse(process.argv);
|
|
1122
|
+
buildProgram().parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@develemail/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"devel": "dist/index.js"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"commander": "^12.1.0",
|
|
18
18
|
"dotenv": "^16.4.5",
|
|
19
|
-
"@develemail/sdk": "0.4.
|
|
19
|
+
"@develemail/sdk": "0.4.1"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"tsup": "^8.3.5",
|