groundmail-cli 0.32.3 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +203 -51
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -75,6 +75,7 @@ groundmail domains list
75
75
  groundmail domains verify example.com
76
76
  groundmail webhooks list
77
77
  groundmail emails list
78
+ groundmail senders list # saved senders; send with --sender billing instead of --from
78
79
  SENDGRID_API_KEY=… groundmail migrate sendgrid --dry-run # import from SendGrid, Postmark, Mailgun, Resend or Amazon SES (beta)
79
80
  ```
80
81
 
package/dist/cli.js CHANGED
@@ -533,6 +533,7 @@ function parseSendCommandArgs(args) {
533
533
  const category = readFlag(args, "--category");
534
534
  return {
535
535
  from: readFlag(args, "--from"),
536
+ sender: readFlag(args, "--sender"),
536
537
  to: readFlag(args, "--to"),
537
538
  subject: readFlag(args, "--subject"),
538
539
  html: readFlag(args, "--html"),
@@ -564,7 +565,7 @@ async function runSendCommand(args, deps) {
564
565
  return 1;
565
566
  }
566
567
  if (options.fromDefaulted) {
567
- stderr(`No --from: sending from the sandbox sender ${SANDBOX_FROM}, which only sends to your account email. Pass --from to send from your own domain.`);
568
+ stderr(`No --from: sending from the sandbox sender ${SANDBOX_FROM}, which only sends to your account email. Pass --from, or --sender with a saved sender, to send from your own domain.`);
568
569
  }
569
570
  const startedAt = now();
570
571
  const recipientDomain2 = options.to.split(",")[0]?.split("@")[1]?.replace(/>$/, "").trim();
@@ -572,7 +573,8 @@ async function runSendCommand(args, deps) {
572
573
  let result;
573
574
  try {
574
575
  result = await deps.sendEmail({
575
- from: options.from,
576
+ ...options.from ? { from: options.from } : {},
577
+ ...options.sender ? { sender: options.sender } : {},
576
578
  to: options.to,
577
579
  subject: options.subject,
578
580
  html: options.html,
@@ -637,9 +639,11 @@ function parseSendCommandOptions(args, env) {
637
639
  if (!parsed.html && !parsed.text) {
638
640
  return { error: "Provide --html or --text" };
639
641
  }
642
+ const sender = parsed.sender?.trim() || void 0;
640
643
  return {
641
- from: parsed.from || SANDBOX_FROM,
642
- fromDefaulted: !parsed.from,
644
+ from: parsed.from || (sender ? void 0 : SANDBOX_FROM),
645
+ ...sender ? { sender } : {},
646
+ fromDefaulted: !parsed.from && !sender,
643
647
  to: parsed.to,
644
648
  subject: parsed.subject,
645
649
  html: parsed.html,
@@ -2051,7 +2055,7 @@ function trimUrl(value) {
2051
2055
  }
2052
2056
 
2053
2057
  // package.json
2054
- var version = "0.32.3";
2058
+ var version = "0.33.0";
2055
2059
 
2056
2060
  // src/user-agent.ts
2057
2061
  function buildUserAgent(own) {
@@ -2135,15 +2139,15 @@ function parseArgs(argv, spec) {
2135
2139
  const booleanFlags = new Set(spec.booleans ?? []);
2136
2140
  const values = {};
2137
2141
  const booleans = {};
2138
- const positionals2 = [];
2142
+ const positionals3 = [];
2139
2143
  for (let i = 0; i < argv.length; i++) {
2140
2144
  const arg = argv[i];
2141
2145
  if (arg === "--") {
2142
- positionals2.push(...argv.slice(i + 1));
2146
+ positionals3.push(...argv.slice(i + 1));
2143
2147
  break;
2144
2148
  }
2145
2149
  if (!arg.startsWith("--") || arg === "-") {
2146
- positionals2.push(arg);
2150
+ positionals3.push(arg);
2147
2151
  continue;
2148
2152
  }
2149
2153
  const eq = arg.indexOf("=");
@@ -2169,7 +2173,7 @@ function parseArgs(argv, spec) {
2169
2173
  const known = [...valueFlags, ...booleanFlags].sort().join(", ");
2170
2174
  return { ok: false, error: `Unknown flag ${name}.${known ? ` This command takes: ${known}.` : ""}` };
2171
2175
  }
2172
- return { ok: true, args: { values, booleans, positionals: positionals2 } };
2176
+ return { ok: true, args: { values, booleans, positionals: positionals3 } };
2173
2177
  }
2174
2178
  function wantsJson(args, stdout = process.stdout) {
2175
2179
  if (args.booleans["--json"] !== void 0) return args.booleans["--json"];
@@ -2477,10 +2481,10 @@ async function resolveAuth(flags, env = process.env, config = loadConfig(env)) {
2477
2481
  return fromSaved(current.workspace);
2478
2482
  }
2479
2483
  async function applySavedSession(argv, env = process.env, config) {
2480
- const hasFlag2 = (flag) => argv.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
2484
+ const hasFlag3 = (flag) => argv.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
2481
2485
  let workspace = null;
2482
2486
  let notice;
2483
- if (!hasFlag2("--api-key") && !envApiKey(env)) {
2487
+ if (!hasFlag3("--api-key") && !envApiKey(env)) {
2484
2488
  const current = currentSavedWorkspace(config ?? loadConfig(env), env);
2485
2489
  if (current.ok) {
2486
2490
  workspace = current.workspace;
@@ -2491,7 +2495,7 @@ async function applySavedSession(argv, env = process.env, config) {
2491
2495
  notice = current.error;
2492
2496
  }
2493
2497
  }
2494
- if (hasFlag2("--base-url")) return { argv, notice };
2498
+ if (hasFlag3("--base-url")) return { argv, notice };
2495
2499
  const url = explicitApiUrl(env) || trimUrl(workspace?.apiUrl) || trimUrl(env.GROUNDMAIL_DEFAULT_API_URL);
2496
2500
  return { argv: url ? [...argv, "--base-url", url] : argv, notice };
2497
2501
  }
@@ -3483,6 +3487,7 @@ async function runCreateCommand3(args, env = process.env) {
3483
3487
  const alias = readFlag20(args, "--alias");
3484
3488
  const category = readFlag20(args, "--category");
3485
3489
  const folder = readFlag20(args, "--folder");
3490
+ const sender = readFlag20(args, "--sender");
3486
3491
  const unsubscribeEnabled = args.includes("--unsubscribe-enabled");
3487
3492
  if (!name) {
3488
3493
  return { exitCode: 1, error: 'Missing name. Pass --name "Welcome"' };
@@ -3503,7 +3508,8 @@ async function runCreateCommand3(args, env = process.env) {
3503
3508
  ...alias ? { alias } : {},
3504
3509
  ...category ? { category } : {},
3505
3510
  ...unsubscribeEnabled ? { unsubscribe_enabled: true } : {},
3506
- ...folder?.trim() ? { folder } : {}
3511
+ ...folder?.trim() ? { folder } : {},
3512
+ ...sender?.trim() ? { sender_id: sender.trim() } : {}
3507
3513
  });
3508
3514
  console.log(JSON.stringify(result));
3509
3515
  return { exitCode: 0 };
@@ -3524,7 +3530,8 @@ var POSITIONAL_FLAGS11 = [
3524
3530
  "--html",
3525
3531
  "--text",
3526
3532
  "--variable",
3527
- "--folder"
3533
+ "--folder",
3534
+ "--sender"
3528
3535
  ];
3529
3536
  async function runUpdateCommand4(args, env = process.env) {
3530
3537
  const options = parseTemplatesCommonArgs(args);
@@ -3544,12 +3551,13 @@ async function runUpdateCommand4(args, env = process.env) {
3544
3551
  const variables = readFlags3(args, "--variable");
3545
3552
  const category = readFlag20(args, "--category");
3546
3553
  const folder = readFlag20(args, "--folder");
3554
+ const sender = readFlag20(args, "--sender");
3547
3555
  const unsubscribeEnabled = args.includes("--unsubscribe-enabled");
3548
3556
  const unsubscribeDisabled = args.includes("--no-unsubscribe-enabled");
3549
- if (!name && !subject && !html && !text && !alias && variables.length === 0 && !category && folder === void 0 && !unsubscribeEnabled && !unsubscribeDisabled) {
3557
+ if (!name && !subject && !html && !text && !alias && variables.length === 0 && !category && folder === void 0 && sender === void 0 && !unsubscribeEnabled && !unsubscribeDisabled) {
3550
3558
  return {
3551
3559
  exitCode: 1,
3552
- error: "Nothing to update. Pass --name, --subject, --html, --text, --set-alias, --category, --variable, --folder, or unsubscribe flags."
3560
+ error: "Nothing to update. Pass --name, --subject, --html, --text, --set-alias, --category, --variable, --folder, --sender, or unsubscribe flags."
3553
3561
  };
3554
3562
  }
3555
3563
  try {
@@ -3564,7 +3572,8 @@ async function runUpdateCommand4(args, env = process.env) {
3564
3572
  ...category ? { category } : {},
3565
3573
  ...unsubscribeEnabled ? { unsubscribe_enabled: true } : {},
3566
3574
  ...unsubscribeDisabled ? { unsubscribe_enabled: false } : {},
3567
- ...folder !== void 0 ? { folder } : {}
3575
+ ...folder !== void 0 ? { folder } : {},
3576
+ ...sender !== void 0 ? { sender_id: sender === "none" || !sender.trim() ? null : sender.trim() } : {}
3568
3577
  });
3569
3578
  console.log(JSON.stringify(result));
3570
3579
  return { exitCode: 0 };
@@ -3641,7 +3650,7 @@ async function runListStartersCommand(args, env = process.env) {
3641
3650
  }
3642
3651
 
3643
3652
  // src/commands/templates/versions.ts
3644
- var POSITIONAL_FLAGS13 = ["--api-key", "--base-url", "--id", "--alias", "--version", "--from", "--to"];
3653
+ var POSITIONAL_FLAGS13 = ["--api-key", "--base-url", "--id", "--alias", "--version", "--from", "--sender", "--to"];
3645
3654
  function readFirstPositional14(args) {
3646
3655
  for (let i = 0; i < args.length; i += 1) {
3647
3656
  const arg = args[i];
@@ -3702,16 +3711,21 @@ async function runTestSendCommand(args, env = process.env) {
3702
3711
  }
3703
3712
  const idOrAlias = readFlag20(args, "--id") || readFlag20(args, "--alias") || readFirstPositional14(args);
3704
3713
  const from = readFlag20(args, "--from");
3714
+ const sender = readFlag20(args, "--sender");
3705
3715
  const to = readFlag20(args, "--to");
3706
- if (!idOrAlias || !from || !to) {
3716
+ if (!idOrAlias || !to) {
3707
3717
  return {
3708
3718
  exitCode: 1,
3709
- error: "Usage: groundmail templates test-send --id <id|alias> --from <email> --to <email>"
3719
+ error: "Usage: groundmail templates test-send --id <id|alias> --to <email> [--from <email> | --sender <handle>] (neither: the template's sender, else your default sender)"
3710
3720
  };
3711
3721
  }
3712
3722
  try {
3713
3723
  const client2 = await createTemplatesClient(apiKey, options.baseUrl);
3714
- console.log(JSON.stringify(await client2.templates.testSend(idOrAlias, { from, to })));
3724
+ console.log(
3725
+ JSON.stringify(
3726
+ await client2.templates.testSend(idOrAlias, { to, ...from ? { from } : {}, ...sender ? { sender } : {} })
3727
+ )
3728
+ );
3715
3729
  return { exitCode: 0 };
3716
3730
  } catch (error) {
3717
3731
  return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
@@ -3925,6 +3939,131 @@ async function runDomainsCommand(args, env = process.env) {
3925
3939
  };
3926
3940
  }
3927
3941
 
3942
+ // src/commands/senders/shared.ts
3943
+ var SENDER_VALUE_FLAGS = ["--api-key", "--base-url", "--id", "--name", "--address", "--reply-to", "--handle"];
3944
+ function readFlag22(args, flag) {
3945
+ const index = args.indexOf(flag);
3946
+ if (index === -1) return void 0;
3947
+ return args[index + 1];
3948
+ }
3949
+ function hasFlag2(args, flag) {
3950
+ return args.includes(flag);
3951
+ }
3952
+ function positionals2(args, valueFlags = SENDER_VALUE_FLAGS) {
3953
+ const out = [];
3954
+ for (let i = 0; i < args.length; i += 1) {
3955
+ const arg = args[i];
3956
+ if (arg.startsWith("--")) {
3957
+ if (valueFlags.includes(arg)) i += 1;
3958
+ continue;
3959
+ }
3960
+ out.push(arg);
3961
+ }
3962
+ return out;
3963
+ }
3964
+ function parseSendersCommonArgs(args) {
3965
+ return {
3966
+ apiKey: readFlag22(args, "--api-key"),
3967
+ baseUrl: readFlag22(args, "--base-url")
3968
+ };
3969
+ }
3970
+ function resolveApiKey10(options, env) {
3971
+ return options.apiKey || env.GROUNDMAIL_API_KEY || null;
3972
+ }
3973
+ async function createSendersClient(apiKey, baseUrl) {
3974
+ const { Groundmail } = await import("groundmail");
3975
+ return new Groundmail(apiKey, baseUrl ? { baseUrl } : void 0);
3976
+ }
3977
+ var MISSING_KEY2 = "Missing API key. Set GROUNDMAIL_API_KEY or pass --api-key.";
3978
+ function failure2(error) {
3979
+ const message = error instanceof Error ? error.message : String(error);
3980
+ const hint = error?.hint;
3981
+ return { exitCode: 1, error: typeof hint === "string" && hint ? `${message}
3982
+ ${hint}` : message };
3983
+ }
3984
+ function senderRef(args) {
3985
+ return readFlag22(args, "--id") || positionals2(args)[0];
3986
+ }
3987
+ function parseSenderFields(args) {
3988
+ const name = readFlag22(args, "--name");
3989
+ const address = readFlag22(args, "--address");
3990
+ const replyTo = readFlag22(args, "--reply-to");
3991
+ const handle = readFlag22(args, "--handle");
3992
+ const makeDefault = hasFlag2(args, "--default");
3993
+ const notDefault = hasFlag2(args, "--no-default");
3994
+ if (makeDefault && notDefault) return { error: "Pass --default or --no-default, not both" };
3995
+ return {
3996
+ value: {
3997
+ ...name !== void 0 ? { name } : {},
3998
+ ...address !== void 0 ? { address } : {},
3999
+ ...replyTo !== void 0 ? { reply_to: replyTo === "none" || replyTo === "" ? null : replyTo } : {},
4000
+ ...handle !== void 0 ? { handle } : {},
4001
+ ...makeDefault ? { is_default: true } : notDefault ? { is_default: false } : {}
4002
+ }
4003
+ };
4004
+ }
4005
+
4006
+ // src/commands/senders/commands.ts
4007
+ async function withClient2(args, env, run) {
4008
+ const options = parseSendersCommonArgs(args);
4009
+ const apiKey = resolveApiKey10(options, env);
4010
+ if (!apiKey) return { exitCode: 1, error: MISSING_KEY2 };
4011
+ try {
4012
+ const client2 = await createSendersClient(apiKey, options.baseUrl);
4013
+ console.log(JSON.stringify(await run(client2)));
4014
+ return { exitCode: 0 };
4015
+ } catch (error) {
4016
+ return failure2(error);
4017
+ }
4018
+ }
4019
+ function missingSender(usage) {
4020
+ return { exitCode: 1, error: `Missing sender (id or handle). Usage: ${usage}` };
4021
+ }
4022
+ async function runListCommand9(args, env = process.env) {
4023
+ return withClient2(args, env, (client2) => client2.senders.list());
4024
+ }
4025
+ async function runCreateCommand5(args, env = process.env) {
4026
+ const fields = parseSenderFields(args);
4027
+ if ("error" in fields) return { exitCode: 1, error: fields.error };
4028
+ const { name, address } = fields.value;
4029
+ if (!name || !address) {
4030
+ return {
4031
+ exitCode: 1,
4032
+ error: 'Required: --name and --address. Usage: groundmail senders create --name "Acme Billing" --address billing@mail.acme.com [--reply-to \u2026] [--handle billing] [--default]'
4033
+ };
4034
+ }
4035
+ return withClient2(args, env, (client2) => client2.senders.create({ ...fields.value, name, address }));
4036
+ }
4037
+ async function runGetCommand8(args, env = process.env) {
4038
+ const sender = senderRef(args);
4039
+ if (!sender) return missingSender("groundmail senders get <id|handle>");
4040
+ return withClient2(args, env, (client2) => client2.senders.get(sender));
4041
+ }
4042
+ async function runUpdateCommand6(args, env = process.env) {
4043
+ const sender = senderRef(args);
4044
+ if (!sender) return missingSender("groundmail senders update <id|handle> [--name \u2026] [--address \u2026] [--reply-to \u2026|none] [--handle \u2026] [--default]");
4045
+ const fields = parseSenderFields(args);
4046
+ if ("error" in fields) return { exitCode: 1, error: fields.error };
4047
+ if (Object.keys(fields.value).length === 0) {
4048
+ return {
4049
+ exitCode: 1,
4050
+ error: "Nothing to update. Pass --name, --address, --reply-to, --handle, --default or --no-default."
4051
+ };
4052
+ }
4053
+ return withClient2(args, env, (client2) => client2.senders.update(sender, fields.value));
4054
+ }
4055
+ async function runDeleteCommand6(args, env = process.env) {
4056
+ const sender = senderRef(args);
4057
+ if (!sender) return missingSender("groundmail senders delete <id|handle> --yes");
4058
+ if (!hasFlag2(args, "--yes")) {
4059
+ return {
4060
+ exitCode: 1,
4061
+ error: `This deletes sender ${sender}. Sends that name it will fail. Run again with --yes to confirm.`
4062
+ };
4063
+ }
4064
+ return withClient2(args, env, (client2) => client2.senders.remove(sender));
4065
+ }
4066
+
3928
4067
  // src/commands/auth.ts
3929
4068
  async function runAuthCommand(subcommand, argv, deps = {}) {
3930
4069
  const env = deps.env ?? process.env;
@@ -4088,7 +4227,7 @@ function sesParams(args, env) {
4088
4227
  "Use a key with the read-only import policy (https://docs.groundmail.dev/migrations#amazon-ses-iam-policy). It is used for this import only and never stored."
4089
4228
  ].join("\n");
4090
4229
  }
4091
- const region = readFlag22(args, "--region") || env.AWS_REGION || env.AWS_DEFAULT_REGION;
4230
+ const region = readFlag23(args, "--region") || env.AWS_REGION || env.AWS_DEFAULT_REGION;
4092
4231
  if (!region) return "Set the SES region with --region (for example --region us-east-1) or AWS_REGION. One region per import.";
4093
4232
  if (!AWS_REGION.test(region)) return `--region must be an AWS region such as us-east-1, not ${region}`;
4094
4233
  return {
@@ -4100,7 +4239,7 @@ function sesParams(args, env) {
4100
4239
  };
4101
4240
  }
4102
4241
  var VALUE_FLAGS3 = /* @__PURE__ */ new Set(["--api-key", "--base-url"]);
4103
- function readFlag22(args, flag) {
4242
+ function readFlag23(args, flag) {
4104
4243
  const index = args.indexOf(flag);
4105
4244
  if (index === -1) return void 0;
4106
4245
  return args[index + 1];
@@ -4175,10 +4314,10 @@ function formatMigrationSummary(m) {
4175
4314
  return lines.join("\n");
4176
4315
  }
4177
4316
  async function client(args, env) {
4178
- const apiKey = readFlag22(args, "--api-key") || env.GROUNDMAIL_API_KEY;
4317
+ const apiKey = readFlag23(args, "--api-key") || env.GROUNDMAIL_API_KEY;
4179
4318
  if (!apiKey) return null;
4180
4319
  const { Groundmail } = await import("groundmail");
4181
- const baseUrl = readFlag22(args, "--base-url");
4320
+ const baseUrl = readFlag23(args, "--base-url");
4182
4321
  return new Groundmail(apiKey, baseUrl ? { baseUrl } : void 0);
4183
4322
  }
4184
4323
  async function runStoredCommand(sub, args, env) {
@@ -4215,7 +4354,7 @@ async function runMigrateCommand(provider, args, env = process.env) {
4215
4354
  };
4216
4355
  }
4217
4356
  const id = provider;
4218
- const apiKey = readFlag22(args, "--api-key") || env.GROUNDMAIL_API_KEY;
4357
+ const apiKey = readFlag23(args, "--api-key") || env.GROUNDMAIL_API_KEY;
4219
4358
  if (!apiKey) {
4220
4359
  return { exitCode: 1, error: "Not signed in. Run `groundmail login`, or set GROUNDMAIL_API_KEY." };
4221
4360
  }
@@ -4225,7 +4364,7 @@ async function runMigrateCommand(provider, args, env = process.env) {
4225
4364
  if (typeof ses === "string") return { exitCode: 1, error: ses };
4226
4365
  params = ses;
4227
4366
  } else {
4228
- const providerKey = readFlag22(args, "--key") || env[KEY_ENV[id]];
4367
+ const providerKey = readFlag23(args, "--key") || env[KEY_ENV[id]];
4229
4368
  if (!providerKey) {
4230
4369
  const resend = id === "resend" ? " Use a key with Full access (Resend \u2192 API Keys); a Sending access key cannot read your account." : "";
4231
4370
  return {
@@ -4235,15 +4374,15 @@ async function runMigrateCommand(provider, args, env = process.env) {
4235
4374
  }
4236
4375
  params = { provider: id, api_key: providerKey };
4237
4376
  if (id === "postmark") {
4238
- const accountToken = readFlag22(args, "--account-token") || env.POSTMARK_ACCOUNT_TOKEN;
4377
+ const accountToken = readFlag23(args, "--account-token") || env.POSTMARK_ACCOUNT_TOKEN;
4239
4378
  if (accountToken) params.account_api_key = accountToken;
4240
4379
  } else if (id === "sendgrid" || id === "mailgun") {
4241
- const region = readFlag22(args, "--region");
4380
+ const region = readFlag23(args, "--region");
4242
4381
  if (region === "us" || region === "eu") params.region = region;
4243
4382
  else if (region) return { exitCode: 1, error: "--region must be us or eu" };
4244
4383
  }
4245
4384
  }
4246
- const only = readFlag22(args, "--only");
4385
+ const only = readFlag23(args, "--only");
4247
4386
  if (only) {
4248
4387
  const list = only.split(",").map((s) => s.trim()).filter(Boolean);
4249
4388
  const bad = list.filter((r) => !RESOURCES.includes(r));
@@ -4255,7 +4394,7 @@ async function runMigrateCommand(provider, args, env = process.env) {
4255
4394
  if (args.includes("--dry-run")) params.dry_run = true;
4256
4395
  try {
4257
4396
  const { Groundmail } = await import("groundmail");
4258
- const baseUrl = readFlag22(args, "--base-url");
4397
+ const baseUrl = readFlag23(args, "--base-url");
4259
4398
  const client2 = new Groundmail(apiKey, baseUrl ? { baseUrl } : void 0);
4260
4399
  console.error(`${params.dry_run ? "Previewing" : "Importing"} from ${LABELS[id]}${params.provider === "ses" ? ` (${params.aws_region})` : ""} (Beta)\u2026`);
4261
4400
  const report = await client2.migrations.run(params, {
@@ -4299,7 +4438,7 @@ async function runValidateCommand(args, env = process.env) {
4299
4438
 
4300
4439
  // src/commands/dmarc/dmarc.ts
4301
4440
  var DMARC_APPLY_PROVIDERS = ["cloudflare", "vercel", "godaddy", "netlify", "digitalocean", "dnsimple"];
4302
- function readFlag23(args, flag) {
4441
+ function readFlag24(args, flag) {
4303
4442
  const index = args.indexOf(flag);
4304
4443
  if (index === -1) return void 0;
4305
4444
  return args[index + 1];
@@ -4356,23 +4495,23 @@ function formatDmarcStatus(dmarc) {
4356
4495
  return lines.join("\n");
4357
4496
  }
4358
4497
  async function runDmarcCommand(subcommand, args, env = process.env) {
4359
- const apiKey = readFlag23(args, "--api-key") || env.GROUNDMAIL_API_KEY;
4498
+ const apiKey = readFlag24(args, "--api-key") || env.GROUNDMAIL_API_KEY;
4360
4499
  if (!apiKey) return { exitCode: 1, error: "Missing API key. Run groundmail login, set GROUNDMAIL_API_KEY or pass --api-key." };
4361
- const domain = readFlag23(args, "--domain") || readFirstPositional15(args);
4500
+ const domain = readFlag24(args, "--domain") || readFirstPositional15(args);
4362
4501
  if (!subcommand || !["status", "enable", "disable", "digest"].includes(subcommand)) {
4363
4502
  return { exitCode: 1, error: "Usage: groundmail dmarc status|enable|disable|digest <domain>" };
4364
4503
  }
4365
4504
  if (!domain) return { exitCode: 1, error: `Missing domain. Usage: groundmail dmarc ${subcommand} example.com` };
4366
4505
  const json = args.includes("--json");
4367
4506
  try {
4368
- const client2 = await createDmarcClient(apiKey, readFlag23(args, "--base-url"));
4507
+ const client2 = await createDmarcClient(apiKey, readFlag24(args, "--base-url"));
4369
4508
  if (subcommand === "status") {
4370
4509
  const result2 = await client2.dmarc.get(domain);
4371
4510
  console.log(json ? JSON.stringify(result2) : formatDmarcStatus(result2.dmarc));
4372
4511
  return { exitCode: 0 };
4373
4512
  }
4374
4513
  if (subcommand === "enable") {
4375
- const apply = readFlag23(args, "--apply");
4514
+ const apply = readFlag24(args, "--apply");
4376
4515
  if (apply && !DMARC_APPLY_PROVIDERS.includes(apply)) {
4377
4516
  return { exitCode: 1, error: `--apply must be one of: ${DMARC_APPLY_PROVIDERS.join(", ")}` };
4378
4517
  }
@@ -4402,7 +4541,7 @@ Updated the DMARC record with ${result2.applied.provider}.`);
4402
4541
  }
4403
4542
 
4404
4543
  // src/commands/test-inbox/test-inbox.ts
4405
- function readFlag24(args, flag) {
4544
+ function readFlag25(args, flag) {
4406
4545
  const index = args.indexOf(flag);
4407
4546
  if (index === -1) return void 0;
4408
4547
  return args[index + 1];
@@ -4430,20 +4569,20 @@ function formatTestEmail(email) {
4430
4569
  var USAGE = "Usage: groundmail test-inbox address [--prefix signup] | wait --to <address> [--timeout 30] [--since <iso>] [--json] | list [--to <address>] [--json]";
4431
4570
  async function runTestInboxCommand(subcommand, args, env = process.env) {
4432
4571
  if (!subcommand || !["address", "wait", "list"].includes(subcommand)) return { exitCode: 1, error: USAGE };
4433
- const apiKey = readFlag24(args, "--api-key") || env.GROUNDMAIL_API_KEY;
4572
+ const apiKey = readFlag25(args, "--api-key") || env.GROUNDMAIL_API_KEY;
4434
4573
  if (!apiKey) return { exitCode: 1, error: "Missing API key. Run groundmail login, set GROUNDMAIL_API_KEY or pass --api-key." };
4435
4574
  const json = args.includes("--json");
4436
4575
  try {
4437
- const client2 = await createTestInboxClient(apiKey, readFlag24(args, "--base-url"));
4576
+ const client2 = await createTestInboxClient(apiKey, readFlag25(args, "--base-url"));
4438
4577
  if (subcommand === "address") {
4439
- console.log(await client2.testInbox.address({ prefix: readFlag24(args, "--prefix") }));
4578
+ console.log(await client2.testInbox.address({ prefix: readFlag25(args, "--prefix") }));
4440
4579
  return { exitCode: 0 };
4441
4580
  }
4442
4581
  if (subcommand === "list") {
4443
4582
  const result = await client2.testInbox.list({
4444
- to: readFlag24(args, "--to"),
4445
- since: readFlag24(args, "--since"),
4446
- limit: readFlag24(args, "--limit") ? Number(readFlag24(args, "--limit")) : void 0
4583
+ to: readFlag25(args, "--to"),
4584
+ since: readFlag25(args, "--since"),
4585
+ limit: readFlag25(args, "--limit") ? Number(readFlag25(args, "--limit")) : void 0
4447
4586
  });
4448
4587
  if (json) console.log(JSON.stringify(result));
4449
4588
  else if (!result.emails.length) console.log("No test emails yet.");
@@ -4454,13 +4593,13 @@ async function runTestInboxCommand(subcommand, args, env = process.env) {
4454
4593
  }
4455
4594
  return { exitCode: 0 };
4456
4595
  }
4457
- const to = readFlag24(args, "--to");
4596
+ const to = readFlag25(args, "--to");
4458
4597
  if (!to) return { exitCode: 1, error: "Missing --to. Usage: groundmail test-inbox wait --to signup-k3m9x2pq@acme.test.groundmail.dev" };
4459
- const timeoutRaw = readFlag24(args, "--timeout");
4598
+ const timeoutRaw = readFlag25(args, "--timeout");
4460
4599
  const seconds = timeoutRaw === void 0 ? 30 : Number(timeoutRaw);
4461
4600
  if (!Number.isFinite(seconds) || seconds < 0) return { exitCode: 1, error: "--timeout must be a number of seconds" };
4462
4601
  try {
4463
- const email = await client2.testInbox.waitFor({ to, timeout: seconds * 1e3, since: readFlag24(args, "--since") });
4602
+ const email = await client2.testInbox.waitFor({ to, timeout: seconds * 1e3, since: readFlag25(args, "--since") });
4464
4603
  console.log(json ? JSON.stringify({ email }) : formatTestEmail(email));
4465
4604
  return { exitCode: 0 };
4466
4605
  } catch (error) {
@@ -4501,7 +4640,7 @@ var USAGE_LINES = [
4501
4640
  " groundmail test-inbox address [--prefix signup] a fresh address on your team's test domain (mail there is caught, never delivered)",
4502
4641
  " groundmail test-inbox wait --to <address> [--timeout 30] [--since <iso>] [--json] wait for an email; prints its code and links (exit 2 on timeout)",
4503
4642
  " groundmail test-inbox list [--to <address>] [--since <iso>] [--json]",
4504
- ' groundmail emails send [--from you@domain.com] --to user@example.com --subject "Hello" --html "<p>Hi</p>" [--category transactional|product|newsletter] [--unsubscribe] [--tenant <tenant>] [--watch [--timeout 60] [--json]] no --from: the sandbox sender (to your account email only)',
4643
+ ` groundmail emails send [--from you@domain.com | --sender billing] --to user@example.com --subject "Hello" --html "<p>Hi</p>" [--category transactional|product|newsletter] [--unsubscribe] [--tenant <tenant>] [--watch [--timeout 60] [--json]] --sender: a saved sender's handle; no --from or --sender: the sandbox sender (to your account email only)`,
4505
4644
  ' groundmail emails send --to you@example.com --subject "Hello" --text "Hi" --watch watch it land: the receiving server, its reply and the time',
4506
4645
  " groundmail emails batch --file ./emails.json [--tenant <tenant>]",
4507
4646
  " groundmail emails list [--limit 25] [--cursor \u2026] [--test | --live] [--tenant <tenant>] [--stream transactional|product|newsletter]",
@@ -4534,14 +4673,14 @@ var USAGE_LINES = [
4534
4673
  " groundmail templates list [--status draft|published]",
4535
4674
  " groundmail templates starters",
4536
4675
  " groundmail templates from-starter password-reset",
4537
- ' groundmail templates create --name Welcome --subject "Hi" --html "<p>Hi</p>" [--category transactional|product] [--unsubscribe-enabled] [--folder Onboarding]',
4676
+ ' groundmail templates create --name Welcome --subject "Hi" --html "<p>Hi</p>" [--category transactional|product] [--unsubscribe-enabled] [--folder Onboarding] [--sender billing]',
4538
4677
  " groundmail templates get --id tpl_\u2026",
4539
- " groundmail templates update --id tpl_\u2026 [--name \u2026] [--subject \u2026] [--html \u2026] [--text \u2026] [--set-alias \u2026] [--category transactional|product] [--unsubscribe-enabled|--no-unsubscribe-enabled] [--folder \u2026]",
4678
+ " groundmail templates update --id tpl_\u2026 [--name \u2026] [--subject \u2026] [--html \u2026] [--text \u2026] [--set-alias \u2026] [--category transactional|product] [--unsubscribe-enabled|--no-unsubscribe-enabled] [--folder \u2026] [--sender billing|none]",
4540
4679
  " groundmail templates publish --id tpl_\u2026",
4541
4680
  ` groundmail templates push ./emails/welcome.tsx --alias welcome [--subject "Welcome"] [--props '{"name":"Ada"}'] [--publish]`,
4542
4681
  " groundmail templates versions --id tpl_\u2026",
4543
4682
  " groundmail templates restore --id tpl_\u2026 --version 2",
4544
- " groundmail templates test-send --id tpl_\u2026 --from you@domain.com --to you@domain.com",
4683
+ " groundmail templates test-send --id tpl_\u2026 --to you@domain.com [--from you@domain.com | --sender billing] neither: the template's sender, else your default sender",
4545
4684
  ' groundmail templates duplicate --id tpl_\u2026 [--name "Copy"]',
4546
4685
  " groundmail templates delete --id tpl_\u2026",
4547
4686
  " groundmail tenants list [--status active|paused] [--limit 25] [--cursor \u2026] your customers, each with its own domains, suppressions and limits",
@@ -4555,6 +4694,11 @@ var USAGE_LINES = [
4555
4694
  " groundmail tenants domains list <tenant>",
4556
4695
  " groundmail tenants domains add <tenant> example.com only this tenant can then send from it",
4557
4696
  " groundmail tenants domains remove <tenant> example.com gives the domain back to the account",
4697
+ " groundmail senders list the names and addresses your email goes out as",
4698
+ ' groundmail senders create --name "Acme Billing" --address billing@mail.acme.com [--reply-to billing@acme.com] [--handle billing] [--default] address on a verified domain',
4699
+ " groundmail senders get <id|handle>",
4700
+ " groundmail senders update <id|handle> [--name \u2026] [--address \u2026] [--reply-to \u2026|none] [--handle \u2026] [--default|--no-default]",
4701
+ " groundmail senders delete <id|handle> --yes refused while an automation or template uses it",
4558
4702
  " groundmail keys list",
4559
4703
  " groundmail keys create [--name Default] [--scopes emails:send,domains:read] [--allowed-domains example.com] [--test] [--tenant <tenant>] --test: a gm_test_ key, sends are simulated; --tenant: a key limited to one tenant",
4560
4704
  " groundmail keys delete --id <api-key-id>",
@@ -4597,7 +4741,8 @@ var ALIASES = {
4597
4741
  automations: { add: "create", remove: "delete" },
4598
4742
  suppressions: { create: "add", delete: "remove" },
4599
4743
  templates: { add: "create", remove: "delete" },
4600
- tenants: { add: "create", remove: "delete" }
4744
+ tenants: { add: "create", remove: "delete" },
4745
+ senders: { add: "create", remove: "delete" }
4601
4746
  };
4602
4747
  function words(line) {
4603
4748
  return line.trim().split(/\s+/);
@@ -4935,6 +5080,13 @@ async function main() {
4935
5080
  if (subcommand === "stats") await finish(await runStatsCommand(rest));
4936
5081
  if (subcommand === "domains") await finish(await runDomainsCommand(rest));
4937
5082
  }
5083
+ if (command === "senders") {
5084
+ if (subcommand === "list") await finish(await runListCommand9(rest));
5085
+ if (subcommand === "create" || subcommand === "add") await finish(await runCreateCommand5(rest));
5086
+ if (subcommand === "get") await finish(await runGetCommand8(rest));
5087
+ if (subcommand === "update") await finish(await runUpdateCommand6(rest));
5088
+ if (subcommand === "delete" || subcommand === "remove") await finish(await runDeleteCommand6(rest));
5089
+ }
4938
5090
  if (command === "keys") {
4939
5091
  if (subcommand === "list") {
4940
5092
  const result = await runListKeysCommandWithClient(rest);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groundmail-cli",
3
- "version": "0.32.3",
3
+ "version": "0.33.0",
4
4
  "description": "Groundmail CLI — send transactional email from the terminal",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -38,7 +38,7 @@
38
38
  "prepublishOnly": "npm run build"
39
39
  },
40
40
  "dependencies": {
41
- "groundmail": "0.33.0"
41
+ "groundmail": "0.34.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "tsup": "^8.5.0",