carlyemail 0.6.0 → 0.6.3

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 CHANGED
@@ -86,7 +86,7 @@ npm install -g carlyemail
86
86
  | `inboxes` | List inboxes |
87
87
  | `create --username <u>` | Create an inbox |
88
88
  | `delete <inbox> --yes` | Delete an inbox and its mail |
89
- | `send --from <i> --to <a> --subject <s> --text <t>` | Send an email |
89
+ | `send --from <i> --to <a> --subject <s> --text <t>` | Send an email (also `--cc`, `--bcc`, `--reply-to`, `--attach <file>`) |
90
90
  | `messages <inbox>` | List messages |
91
91
  | `read <inbox> <id>` | Read one message in full |
92
92
  | `search <inbox> <text>` | Search messages |
package/carlyemail.js CHANGED
@@ -14,11 +14,11 @@
14
14
 
15
15
  import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, realpathSync } from "node:fs";
16
16
  import { homedir } from "node:os";
17
- import { join } from "node:path";
17
+ import { basename, extname, join } from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import { randomBytes } from "node:crypto";
20
20
 
21
- export const VERSION = "0.6.0";
21
+ export const VERSION = "0.6.3";
22
22
 
23
23
  const DEFAULT_API = "https://api.carlyemail.com";
24
24
  const CONFIG_DIR = join(homedir(), ".carlyemail");
@@ -171,6 +171,11 @@ export async function request(
171
171
  export function parseArgs(argv) {
172
172
  const flags = {};
173
173
  const positional = [];
174
+ // A flag given twice keeps both values, as an array. Last-one-wins would
175
+ // silently drop the first `--attach`.
176
+ const set = (name, value) => {
177
+ flags[name] = Object.hasOwn(flags, name) ? [].concat(flags[name], value) : value;
178
+ };
174
179
  for (let i = 0; i < argv.length; i++) {
175
180
  const arg = argv[i];
176
181
  if (!arg.startsWith("--")) {
@@ -179,11 +184,11 @@ export function parseArgs(argv) {
179
184
  }
180
185
  const equals = arg.indexOf("=");
181
186
  if (equals !== -1) {
182
- flags[arg.slice(2, equals)] = arg.slice(equals + 1);
187
+ set(arg.slice(2, equals), arg.slice(equals + 1));
183
188
  } else if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
184
- flags[arg.slice(2)] = argv[++i];
189
+ set(arg.slice(2), argv[++i]);
185
190
  } else {
186
- flags[arg.slice(2)] = true;
191
+ set(arg.slice(2), true);
187
192
  }
188
193
  }
189
194
  return { flags, positional };
@@ -197,6 +202,106 @@ function required(flags, name) {
197
202
  return value;
198
203
  }
199
204
 
205
+ /**
206
+ * An address flag. Commas and repeats both work, so `--cc a@x.com,b@y.com` and
207
+ * `--cc a@x.com --cc b@y.com` send the same message.
208
+ */
209
+ function addresses(flags, name, { require = false } = {}) {
210
+ const value = flags[name];
211
+ if (value === undefined) {
212
+ if (require) throw new UsageError(`--${name} is required`);
213
+ return undefined;
214
+ }
215
+ const given = [].concat(value);
216
+ if (given.some((v) => typeof v !== "string")) throw new UsageError(`--${name} needs an address`);
217
+ const list = given.flatMap((v) => v.split(",")).map((s) => s.trim()).filter(Boolean);
218
+ if (!list.length) throw new UsageError(`--${name} needs an address`);
219
+ return list;
220
+ }
221
+
222
+ /**
223
+ * Enough types that a PDF or a photo arrives as one. Anything not listed goes
224
+ * without a type and the API sends it as application/octet-stream.
225
+ */
226
+ const CONTENT_TYPES = {
227
+ ".pdf": "application/pdf",
228
+ ".png": "image/png",
229
+ ".jpg": "image/jpeg",
230
+ ".jpeg": "image/jpeg",
231
+ ".gif": "image/gif",
232
+ ".webp": "image/webp",
233
+ ".svg": "image/svg+xml",
234
+ ".txt": "text/plain",
235
+ ".md": "text/markdown",
236
+ ".csv": "text/csv",
237
+ ".html": "text/html",
238
+ ".json": "application/json",
239
+ ".ics": "text/calendar",
240
+ ".zip": "application/zip",
241
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
242
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
243
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
244
+ };
245
+
246
+ /** Every `--attach <file>`, read and base64-encoded the way the API takes it. */
247
+ function attachments(flags) {
248
+ if (flags.attach === undefined) return undefined;
249
+ return [].concat(flags.attach).map((path) => {
250
+ if (typeof path !== "string" || !path) throw new UsageError("--attach needs a file");
251
+ let content;
252
+ try {
253
+ content = readFileSync(path);
254
+ } catch (error) {
255
+ throw new UsageError(`cannot read ${path} (${error.code ?? error.message})`);
256
+ }
257
+ return {
258
+ filename: basename(path),
259
+ content_type: CONTENT_TYPES[extname(path).toLowerCase()],
260
+ content: content.toString("base64"),
261
+ };
262
+ });
263
+ }
264
+
265
+ /**
266
+ * The flags each command reads, beyond the global --json, --help and --version.
267
+ *
268
+ * Anything else is refused before a request is made. `send` used to accept
269
+ * `--cc` and `--attach` and ignore them, so the message went out without its
270
+ * copy or its file and the command still printed "sent". A test checks this
271
+ * table against the flags each command's code actually reads.
272
+ */
273
+ const FLAGS = {
274
+ signup: ["human-email", "username", "display-name"],
275
+ signin: ["human-email"],
276
+ verify: ["code"],
277
+ create: ["username", "domain", "display-name"],
278
+ delete: ["yes"],
279
+ send: ["from", "to", "cc", "bcc", "reply-to", "subject", "text", "html", "attach"],
280
+ search: ["query"],
281
+ reply: ["last", "text", "html", "all"],
282
+ draft: ["to", "subject", "text"],
283
+ webhook: ["events"],
284
+ domain: ["zone", "check", "subdomains"],
285
+ upgrade: ["plan"],
286
+ };
287
+ const GLOBAL_FLAGS = ["json", "help", "version"];
288
+ const REPEATABLE = new Set(["to", "cc", "bcc", "reply-to", "attach"]);
289
+
290
+ /** Why these flags cannot be run with this command, or null when they can. */
291
+ export function refusedFlag(commandName, flags) {
292
+ const own = FLAGS[commandName] ?? [];
293
+ for (const [name, value] of Object.entries(flags)) {
294
+ if (!GLOBAL_FLAGS.includes(name) && !own.includes(name)) {
295
+ const takes = own.length ? own.map((f) => `--${f}`).join(", ") : "no options";
296
+ return `${commandName} has no --${name} option (it takes ${takes})`;
297
+ }
298
+ if (Array.isArray(value) && !REPEATABLE.has(name)) {
299
+ return `--${name} was given more than once`;
300
+ }
301
+ }
302
+ return null;
303
+ }
304
+
200
305
  // ---------------------------------------------------------------- commands
201
306
 
202
307
  const commands = {};
@@ -244,8 +349,8 @@ async function startSignin(ctx, human_email, { known = false } = {}) {
244
349
  ctx.print(
245
350
  arrow(
246
351
  known
247
- ? `a sign-in code is on its way to ${human_email} (good for 10 minutes), then:`
248
- : `if ${human_email} has an account, a code is on its way (good for 10 minutes), then:`
352
+ ? `a sign-in code is on its way to ${human_email} (good for 30 minutes), then:`
353
+ : `if ${human_email} has an account, a code is on its way (good for 30 minutes), then:`
249
354
  )
250
355
  );
251
356
  ctx.print(` carlyemail verify <code>`);
@@ -434,14 +539,18 @@ define("delete", "Delete an inbox and its mail", "carlyemail delete support@carl
434
539
  define(
435
540
  "send",
436
541
  "Send an email",
437
- 'carlyemail send --from me@carlyemail.com --to you@example.com --subject Hi --text "Hello"',
542
+ 'carlyemail send --from me@carlyemail.com --to you@example.com --subject Hi --text "Hello" [--cc a,b] [--bcc a] [--attach file]',
438
543
  async (ctx) => {
439
544
  const from = required(ctx.flags, "from");
440
545
  const body = {
441
- to: required(ctx.flags, "to").split(",").map((s) => s.trim()),
546
+ to: addresses(ctx.flags, "to", { require: true }),
547
+ cc: addresses(ctx.flags, "cc"),
548
+ bcc: addresses(ctx.flags, "bcc"),
549
+ reply_to: addresses(ctx.flags, "reply-to"),
442
550
  subject: typeof ctx.flags.subject === "string" ? ctx.flags.subject : undefined,
443
551
  text: typeof ctx.flags.text === "string" ? ctx.flags.text : undefined,
444
552
  html: typeof ctx.flags.html === "string" ? ctx.flags.html : undefined,
553
+ attachments: attachments(ctx.flags),
445
554
  };
446
555
  const sent = await request(
447
556
  ctx.config,
@@ -613,7 +722,7 @@ define(
613
722
  const inbox = ctx.positional[0];
614
723
  if (!inbox) throw new UsageError("carlyemail draft <inbox> --to ... --text ...");
615
724
  const body = {
616
- to: required(ctx.flags, "to").split(",").map((s) => s.trim()),
725
+ to: addresses(ctx.flags, "to", { require: true }),
617
726
  subject: typeof ctx.flags.subject === "string" ? ctx.flags.subject : undefined,
618
727
  text: typeof ctx.flags.text === "string" ? ctx.flags.text : undefined,
619
728
  };
@@ -922,6 +1031,14 @@ export async function main(
922
1031
  return 1;
923
1032
  }
924
1033
 
1034
+ // Before the config is read, and so before anything can be sent.
1035
+ const refused = refusedFlag(name, flags);
1036
+ if (refused) {
1037
+ print(`${paint(31, "✗")} ${refused}`);
1038
+ print(dim(` ${command.usage}`));
1039
+ return 1;
1040
+ }
1041
+
925
1042
  const config = loadConfig(env, configFile);
926
1043
  try {
927
1044
  await command.run({ config, flags, positional, print, ask, configFile, configDir });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carlyemail",
3
- "version": "0.6.0",
3
+ "version": "0.6.3",
4
4
  "description": "Real email inboxes your agent can send, receive and reply from. SDK and CLI.",
5
5
  "keywords": [
6
6
  "email",
@@ -13,11 +13,16 @@
13
13
  ],
14
14
  "homepage": "https://docs.carlyemail.com",
15
15
  "bugs": "https://docs.carlyemail.com/support",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/shirschfield/carlyemail.git",
19
+ "directory": "cli"
20
+ },
16
21
  "license": "MIT",
17
22
  "author": "SWH Labs LLC",
18
23
  "type": "module",
19
24
  "bin": {
20
- "carlyemail": "./carlyemail.js"
25
+ "carlyemail": "carlyemail.js"
21
26
  },
22
27
  "types": "./sdk.d.ts",
23
28
  "exports": {
package/sdk.d.ts CHANGED
@@ -16,6 +16,8 @@ export interface AgentSigninVerifyRequest {
16
16
  export interface AgentSigninVerifyResponse {
17
17
  organization_id: string;
18
18
  api_key: string;
19
+ inbox_domain?: string | null;
20
+ inboxes_on_signup_domain?: Array<string>;
19
21
  }
20
22
 
21
23
  export interface AgentSignupRequest {
@@ -39,6 +41,8 @@ export interface AgentVerifyRequest {
39
41
  export interface AgentVerifyResponse {
40
42
  organization_id: string;
41
43
  verified: boolean;
44
+ inbox_domain?: string | null;
45
+ inboxes_on_signup_domain?: Array<string>;
42
46
  }
43
47
 
44
48
  export interface ApiKeyOut {
@@ -50,6 +54,7 @@ export interface ApiKeyOut {
50
54
  used_at?: string | null;
51
55
  permissions?: Record<string, boolean> | null;
52
56
  created_at: string;
57
+ expires_at?: string | null;
53
58
  }
54
59
 
55
60
  export interface AttachmentOut {
@@ -97,6 +102,7 @@ export interface BatchUpdateMessagesResponse {
97
102
  export interface CreateApiKeyRequest {
98
103
  name?: string | null;
99
104
  permissions?: Record<string, boolean> | null;
105
+ expires_at?: string | null;
100
106
  }
101
107
 
102
108
  export interface CreateApiKeyResponse {
@@ -108,6 +114,7 @@ export interface CreateApiKeyResponse {
108
114
  inbox_id?: string | null;
109
115
  permissions?: Record<string, boolean> | null;
110
116
  created_at: string;
117
+ expires_at?: string | null;
111
118
  }
112
119
 
113
120
  export interface CreateDomainRequest {
@@ -204,6 +211,26 @@ export interface DraftOut {
204
211
  created_at: string;
205
212
  }
206
213
 
214
+ export interface EventOut {
215
+ event_id: string;
216
+ event_type: string;
217
+ organization_id: string;
218
+ pod_id?: string | null;
219
+ inbox_id?: string | null;
220
+ message_id?: string | null;
221
+ thread_id?: string | null;
222
+ payload?: Record<string, unknown>;
223
+ created_at: string;
224
+ }
225
+
226
+ export interface FeedbackRequest {
227
+ message: string;
228
+ }
229
+
230
+ export interface FeedbackResponse {
231
+ received: boolean;
232
+ }
233
+
207
234
  export interface ForwardMessageRequest {
208
235
  to?: Array<string> | string | null;
209
236
  cc?: Array<string> | string | null;
@@ -286,6 +313,13 @@ export interface ListEntryOut {
286
313
  created_at: string;
287
314
  }
288
315
 
316
+ export interface ListEventsResponse {
317
+ count: number;
318
+ limit?: number | null;
319
+ next_page_token?: string | null;
320
+ events: Array<EventOut>;
321
+ }
322
+
289
323
  export interface ListInboxEventsResponse {
290
324
  count: number;
291
325
  limit?: number | null;
@@ -328,6 +362,11 @@ export interface ListThreadsResponse {
328
362
  threads: Array<ThreadItem>;
329
363
  }
330
364
 
365
+ export interface ListWebhookAttemptsResponse {
366
+ count: number;
367
+ attempts: Array<WebhookAttemptOut>;
368
+ }
369
+
331
370
  export interface ListWebhooksResponse {
332
371
  count: number;
333
372
  limit?: number | null;
@@ -378,6 +417,7 @@ export interface MessageOut {
378
417
  in_reply_to?: string | null;
379
418
  references?: Array<string>;
380
419
  headers?: Record<string, unknown>;
420
+ authentication?: Record<string, string>;
381
421
  }
382
422
 
383
423
  export interface OrganizationOut {
@@ -587,6 +627,19 @@ export interface VerificationRecord {
587
627
  priority?: number | null;
588
628
  }
589
629
 
630
+ export interface WebhookAttemptOut {
631
+ attempt_id: string;
632
+ webhook_id: string;
633
+ event_id: string;
634
+ event_type: string;
635
+ url: string;
636
+ status_code?: number | null;
637
+ ok: boolean;
638
+ detail?: string | null;
639
+ duration_ms: number;
640
+ created_at: string;
641
+ }
642
+
590
643
  export interface WebhookHeadersResponse {
591
644
  header_names: Array<string>;
592
645
  }
@@ -635,6 +688,10 @@ export declare class Billing {
635
688
  current(): Promise<Record<string, unknown>>;
636
689
  }
637
690
 
691
+ export declare class Feedback {
692
+ send(body: FeedbackRequest): Promise<FeedbackResponse>;
693
+ }
694
+
638
695
  export declare class Inboxes {
639
696
  list(query?: { limit?: number | null; pageToken?: string | null; ascending?: boolean }): Promise<ListInboxesResponse>;
640
697
  create(body: CreateInboxRequest): Promise<InboxOut>;
@@ -717,6 +774,10 @@ export declare class ApiKeys {
717
774
  deleteInbox(inboxId: string, apiKeyId: string): Promise<void>;
718
775
  }
719
776
 
777
+ export declare class Events {
778
+ list(query?: { eventTypes?: Array<string> | null; inboxId?: string | null; start?: string | null; end?: string | null; limit?: number | null; pageToken?: string | null }): Promise<ListEventsResponse>;
779
+ }
780
+
720
781
  export declare class Metrics {
721
782
  orgEvents(query?: { eventTypes?: Array<string> | null; start?: string | null; end?: string | null; period?: number | null; limit?: number | null; descending?: boolean }): Promise<Record<string, unknown>>;
722
783
  orgUsage(query?: { usageTypes?: Array<string> | null; start?: string | null; end?: string | null; period?: number | null; limit?: number | null; descending?: boolean }): Promise<Record<string, unknown>>;
@@ -779,6 +840,8 @@ export declare class InboxEvents {
779
840
  export declare class Webhooks {
780
841
  list(query?: { limit?: number | null; pageToken?: string | null; ascending?: boolean }): Promise<ListWebhooksResponse>;
781
842
  create(body: CreateWebhookRequest): Promise<WebhookOut>;
843
+ listAllAttempts(query?: { limit?: number }): Promise<ListWebhookAttemptsResponse>;
844
+ listAttempts(webhookId: string, query?: { limit?: number }): Promise<ListWebhookAttemptsResponse>;
782
845
  get(webhookId: string): Promise<WebhookOut>;
783
846
  update(webhookId: string, body: UpdateWebhookRequest): Promise<WebhookOut>;
784
847
  delete(webhookId: string): Promise<void>;
@@ -798,6 +861,7 @@ export declare class CarlyEmail {
798
861
  agent: Agent;
799
862
  auth: Auth;
800
863
  billing: Billing;
864
+ feedback: Feedback;
801
865
  inboxes: Inboxes;
802
866
  messages: Messages;
803
867
  threads: Threads;
@@ -805,6 +869,7 @@ export declare class CarlyEmail {
805
869
  lists: Lists;
806
870
  domains: Domains;
807
871
  apiKeys: ApiKeys;
872
+ events: Events;
808
873
  metrics: Metrics;
809
874
  pods: Pods;
810
875
  organizations: Organizations;
package/sdk.js CHANGED
@@ -126,6 +126,17 @@ class Billing {
126
126
  }
127
127
  }
128
128
 
129
+ class Feedback {
130
+ constructor(transport) {
131
+ this.$ = transport;
132
+ }
133
+
134
+ /** Send Feedback */
135
+ send(body) {
136
+ return this.$.request("POST", `/v0/feedback`, { body });
137
+ }
138
+ }
139
+
129
140
  class Inboxes {
130
141
  constructor(transport) {
131
142
  this.$ = transport;
@@ -473,6 +484,17 @@ class ApiKeys {
473
484
  }
474
485
  }
475
486
 
487
+ class Events {
488
+ constructor(transport) {
489
+ this.$ = transport;
490
+ }
491
+
492
+ /** List Events */
493
+ list(query = {}) {
494
+ return this.$.request("GET", `/v0/events`, { query: { "event_types": query.eventTypes, "inbox_id": query.inboxId, "start": query.start, "end": query.end, "limit": query.limit, "page_token": query.pageToken } });
495
+ }
496
+ }
497
+
476
498
  class Metrics {
477
499
  constructor(transport) {
478
500
  this.$ = transport;
@@ -747,6 +769,16 @@ class Webhooks {
747
769
  return this.$.request("POST", `/v0/webhooks`, { body });
748
770
  }
749
771
 
772
+ /** List All Attempts */
773
+ listAllAttempts(query = {}) {
774
+ return this.$.request("GET", `/v0/webhooks/attempts`, { query: { "limit": query.limit } });
775
+ }
776
+
777
+ /** List Attempts */
778
+ listAttempts(webhookId, query = {}) {
779
+ return this.$.request("GET", `/v0/webhooks/${encode(webhookId)}/attempts`, { query: { "limit": query.limit } });
780
+ }
781
+
750
782
  /** Get Webhook */
751
783
  get(webhookId) {
752
784
  return this.$.request("GET", `/v0/webhooks/${encode(webhookId)}`);
@@ -814,6 +846,7 @@ export class CarlyEmail {
814
846
  this.agent = new Agent(transport);
815
847
  this.auth = new Auth(transport);
816
848
  this.billing = new Billing(transport);
849
+ this.feedback = new Feedback(transport);
817
850
  this.inboxes = new Inboxes(transport);
818
851
  this.messages = new Messages(transport);
819
852
  this.threads = new Threads(transport);
@@ -821,6 +854,7 @@ export class CarlyEmail {
821
854
  this.lists = new Lists(transport);
822
855
  this.domains = new Domains(transport);
823
856
  this.apiKeys = new ApiKeys(transport);
857
+ this.events = new Events(transport);
824
858
  this.metrics = new Metrics(transport);
825
859
  this.pods = new Pods(transport);
826
860
  this.organizations = new Organizations(transport);