carlyemail 0.6.2 → 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 +1 -1
- package/carlyemail.js +125 -8
- package/package.json +1 -1
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.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
|
-
|
|
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"],
|
|
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 = {};
|
|
@@ -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:
|
|
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:
|
|
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 });
|