carlyemail 0.6.2 → 0.6.4
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 +1 -1
- package/carlyemail.js +139 -10
- package/package.json +1 -1
- package/sdk.d.ts +7 -2
- package/sdk.js +4 -4
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.
|
|
21
|
+
export const VERSION = "0.6.4";
|
|
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
|
-
|
|
187
|
+
set(arg.slice(2, equals), arg.slice(equals + 1));
|
|
183
188
|
} else if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
|
|
184
|
-
|
|
189
|
+
set(arg.slice(2), argv[++i]);
|
|
185
190
|
} else {
|
|
186
|
-
|
|
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", "force"],
|
|
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 = {};
|
|
@@ -419,7 +524,7 @@ define(
|
|
|
419
524
|
}
|
|
420
525
|
);
|
|
421
526
|
|
|
422
|
-
define("delete", "Delete an inbox and its mail", "carlyemail delete support@carlyemail.com", async (ctx) => {
|
|
527
|
+
define("delete", "Delete an inbox and its mail", "carlyemail delete support@carlyemail.com --yes", async (ctx) => {
|
|
423
528
|
const inbox = ctx.positional[0];
|
|
424
529
|
if (!inbox) throw new UsageError("which inbox? carlyemail delete <inbox>");
|
|
425
530
|
if (!ctx.flags.yes) {
|
|
@@ -427,21 +532,37 @@ define("delete", "Delete an inbox and its mail", "carlyemail delete support@carl
|
|
|
427
532
|
`this deletes ${inbox} and every message in it. Re-run with --yes to confirm.`
|
|
428
533
|
);
|
|
429
534
|
}
|
|
430
|
-
|
|
535
|
+
// An inbox that sent mail or whose key was used in the last 30 days is
|
|
536
|
+
// refused with `inbox_in_use`, and the API's fix names its own parameter.
|
|
537
|
+
// Here the answer is a flag, so say that instead.
|
|
538
|
+
const force = ctx.flags.force === true;
|
|
539
|
+
const path = `/v0/inboxes/${encodeURIComponent(inbox)}${force ? "?force=true" : ""}`;
|
|
540
|
+
try {
|
|
541
|
+
await request(ctx.config, "DELETE", path);
|
|
542
|
+
} catch (error) {
|
|
543
|
+
if (error instanceof ApiError && error.code === "inbox_in_use") {
|
|
544
|
+
error.fix = "Check nothing still sends from it. To delete it anyway, re-run with --force.";
|
|
545
|
+
}
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
431
548
|
ctx.print(ok(`deleted ${inbox}`));
|
|
432
549
|
});
|
|
433
550
|
|
|
434
551
|
define(
|
|
435
552
|
"send",
|
|
436
553
|
"Send an email",
|
|
437
|
-
'carlyemail send --from me@carlyemail.com --to you@example.com --subject Hi --text "Hello"',
|
|
554
|
+
'carlyemail send --from me@carlyemail.com --to you@example.com --subject Hi --text "Hello" [--cc a,b] [--bcc a] [--attach file]',
|
|
438
555
|
async (ctx) => {
|
|
439
556
|
const from = required(ctx.flags, "from");
|
|
440
557
|
const body = {
|
|
441
|
-
to:
|
|
558
|
+
to: addresses(ctx.flags, "to", { require: true }),
|
|
559
|
+
cc: addresses(ctx.flags, "cc"),
|
|
560
|
+
bcc: addresses(ctx.flags, "bcc"),
|
|
561
|
+
reply_to: addresses(ctx.flags, "reply-to"),
|
|
442
562
|
subject: typeof ctx.flags.subject === "string" ? ctx.flags.subject : undefined,
|
|
443
563
|
text: typeof ctx.flags.text === "string" ? ctx.flags.text : undefined,
|
|
444
564
|
html: typeof ctx.flags.html === "string" ? ctx.flags.html : undefined,
|
|
565
|
+
attachments: attachments(ctx.flags),
|
|
445
566
|
};
|
|
446
567
|
const sent = await request(
|
|
447
568
|
ctx.config,
|
|
@@ -613,7 +734,7 @@ define(
|
|
|
613
734
|
const inbox = ctx.positional[0];
|
|
614
735
|
if (!inbox) throw new UsageError("carlyemail draft <inbox> --to ... --text ...");
|
|
615
736
|
const body = {
|
|
616
|
-
to:
|
|
737
|
+
to: addresses(ctx.flags, "to", { require: true }),
|
|
617
738
|
subject: typeof ctx.flags.subject === "string" ? ctx.flags.subject : undefined,
|
|
618
739
|
text: typeof ctx.flags.text === "string" ? ctx.flags.text : undefined,
|
|
619
740
|
};
|
|
@@ -922,6 +1043,14 @@ export async function main(
|
|
|
922
1043
|
return 1;
|
|
923
1044
|
}
|
|
924
1045
|
|
|
1046
|
+
// Before the config is read, and so before anything can be sent.
|
|
1047
|
+
const refused = refusedFlag(name, flags);
|
|
1048
|
+
if (refused) {
|
|
1049
|
+
print(`${paint(31, "✗")} ${refused}`);
|
|
1050
|
+
print(dim(` ${command.usage}`));
|
|
1051
|
+
return 1;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
925
1054
|
const config = loadConfig(env, configFile);
|
|
926
1055
|
try {
|
|
927
1056
|
await command.run({ config, flags, positional, print, ask, configFile, configDir });
|
package/package.json
CHANGED
package/sdk.d.ts
CHANGED
|
@@ -576,6 +576,7 @@ export interface UpdateInboxRequest {
|
|
|
576
576
|
|
|
577
577
|
export interface UpdateInboxWebhookRequest {
|
|
578
578
|
event_types?: Array<string> | null;
|
|
579
|
+
enabled?: boolean | null;
|
|
579
580
|
}
|
|
580
581
|
|
|
581
582
|
export interface UpdateMessageRequest {
|
|
@@ -605,6 +606,7 @@ export interface UpdateWebhookHeadersRequest {
|
|
|
605
606
|
|
|
606
607
|
export interface UpdateWebhookRequest {
|
|
607
608
|
event_types?: Array<string> | null;
|
|
609
|
+
enabled?: boolean | null;
|
|
608
610
|
add_inbox_ids?: Array<string> | null;
|
|
609
611
|
remove_inbox_ids?: Array<string> | null;
|
|
610
612
|
add_pod_ids?: Array<string> | null;
|
|
@@ -652,6 +654,9 @@ export interface WebhookOut {
|
|
|
652
654
|
inbox_ids?: Array<string>;
|
|
653
655
|
secret?: string | null;
|
|
654
656
|
enabled?: boolean;
|
|
657
|
+
disabled_reason?: string | null;
|
|
658
|
+
disabled_at?: string | null;
|
|
659
|
+
failing_since?: string | null;
|
|
655
660
|
client_id?: string | null;
|
|
656
661
|
updated_at: string;
|
|
657
662
|
created_at: string;
|
|
@@ -697,7 +702,7 @@ export declare class Inboxes {
|
|
|
697
702
|
create(body: CreateInboxRequest): Promise<InboxOut>;
|
|
698
703
|
get(inboxId: string): Promise<InboxOut>;
|
|
699
704
|
update(inboxId: string, body: UpdateInboxRequest): Promise<InboxOut>;
|
|
700
|
-
delete(inboxId: string): Promise<void>;
|
|
705
|
+
delete(inboxId: string, query?: { force?: boolean }): Promise<void>;
|
|
701
706
|
}
|
|
702
707
|
|
|
703
708
|
export declare class Messages {
|
|
@@ -794,7 +799,7 @@ export declare class Pods {
|
|
|
794
799
|
createInbox(podId: string, body: CreateInboxRequest): Promise<InboxOut>;
|
|
795
800
|
getInbox(inboxId: string, podId: string): Promise<InboxOut>;
|
|
796
801
|
updateInbox(inboxId: string, podId: string, body: UpdateInboxRequest): Promise<InboxOut>;
|
|
797
|
-
deleteInbox(inboxId: string, podId: string): Promise<void>;
|
|
802
|
+
deleteInbox(inboxId: string, podId: string, query?: { force?: boolean }): Promise<void>;
|
|
798
803
|
listThreads(podId: string, query?: { limit?: number | null; pageToken?: string | null; labels?: Array<string> | null; before?: string | null; after?: string | null; ascending?: boolean; includeSpam?: boolean; includeBlocked?: boolean; includeUnauthenticated?: boolean; includeTrash?: boolean; senders?: Array<string> | null; recipients?: Array<string> | null; subject?: string | null }): Promise<ListThreadsResponse>;
|
|
799
804
|
searchThreads(podId: string, query?: { q?: string; limit?: number | null; pageToken?: string | null; before?: string | null; after?: string | null }): Promise<ListThreadsResponse>;
|
|
800
805
|
getThread(threadId: string, podId: string): Promise<ThreadOut>;
|
package/sdk.js
CHANGED
|
@@ -163,8 +163,8 @@ class Inboxes {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
/** Delete Inbox */
|
|
166
|
-
delete(inboxId) {
|
|
167
|
-
return this.$.request("DELETE", `/v0/inboxes/${encode(inboxId)}
|
|
166
|
+
delete(inboxId, query = {}) {
|
|
167
|
+
return this.$.request("DELETE", `/v0/inboxes/${encode(inboxId)}`, { query: { "force": query.force } });
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
@@ -567,8 +567,8 @@ class Pods {
|
|
|
567
567
|
}
|
|
568
568
|
|
|
569
569
|
/** Delete Inbox */
|
|
570
|
-
deleteInbox(inboxId, podId) {
|
|
571
|
-
return this.$.request("DELETE", `/v0/pods/${encode(podId)}/inboxes/${encode(inboxId)}
|
|
570
|
+
deleteInbox(inboxId, podId, query = {}) {
|
|
571
|
+
return this.$.request("DELETE", `/v0/pods/${encode(podId)}/inboxes/${encode(inboxId)}`, { query: { "force": query.force } });
|
|
572
572
|
}
|
|
573
573
|
|
|
574
574
|
/** List Threads */
|