@btcemail/cli 0.1.0 → 0.3.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.
- package/README.md +110 -41
- package/dist/index.js +794 -136
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import pc11 from "picocolors";
|
|
6
6
|
|
|
7
7
|
// src/commands/login.ts
|
|
8
8
|
import { randomUUID } from "crypto";
|
|
@@ -200,14 +200,14 @@ async function logoutCommand() {
|
|
|
200
200
|
|
|
201
201
|
// src/commands/whoami.ts
|
|
202
202
|
import pc3 from "picocolors";
|
|
203
|
+
import ora2 from "ora";
|
|
203
204
|
|
|
204
205
|
// src/api.ts
|
|
205
206
|
async function apiRequest(endpoint, options = {}) {
|
|
206
207
|
const token = getToken();
|
|
207
208
|
const baseUrl = getApiBaseUrl();
|
|
208
209
|
const headers = {
|
|
209
|
-
"Content-Type": "application/json"
|
|
210
|
-
...options.headers
|
|
210
|
+
"Content-Type": "application/json"
|
|
211
211
|
};
|
|
212
212
|
if (token) {
|
|
213
213
|
headers.Authorization = `Bearer ${token}`;
|
|
@@ -285,15 +285,140 @@ async function sendEmail(options) {
|
|
|
285
285
|
});
|
|
286
286
|
}
|
|
287
287
|
async function getL402Invoice(options) {
|
|
288
|
-
|
|
288
|
+
const v1BaseUrl = getApiBaseUrl();
|
|
289
|
+
const rootApiUrl = v1BaseUrl.replace("/api/v1", "/api");
|
|
290
|
+
const token = getToken();
|
|
291
|
+
const headers = {
|
|
292
|
+
"Content-Type": "application/json"
|
|
293
|
+
};
|
|
294
|
+
if (token) {
|
|
295
|
+
headers.Authorization = `Bearer ${token}`;
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
const response = await fetch(`${rootApiUrl}/l402/invoice`, {
|
|
299
|
+
method: "POST",
|
|
300
|
+
headers,
|
|
301
|
+
body: JSON.stringify({
|
|
302
|
+
sender_email: options.fromEmail,
|
|
303
|
+
recipient_emails: options.toEmails,
|
|
304
|
+
amount_sats: options.amountSats
|
|
305
|
+
})
|
|
306
|
+
});
|
|
307
|
+
const json = await response.json();
|
|
308
|
+
if (!response.ok || json.success === false) {
|
|
309
|
+
return {
|
|
310
|
+
ok: false,
|
|
311
|
+
error: json.error || {
|
|
312
|
+
code: "UNKNOWN_ERROR",
|
|
313
|
+
message: `Request failed with status ${response.status}`
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return { ok: true, data: json.data };
|
|
318
|
+
} catch (error) {
|
|
319
|
+
return {
|
|
320
|
+
ok: false,
|
|
321
|
+
error: {
|
|
322
|
+
code: "NETWORK_ERROR",
|
|
323
|
+
message: error instanceof Error ? error.message : "Network request failed"
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async function getDeliveredEmails(options = {}) {
|
|
329
|
+
const params = new URLSearchParams();
|
|
330
|
+
if (options.limit) params.set("limit", String(options.limit));
|
|
331
|
+
if (options.offset) params.set("offset", String(options.offset));
|
|
332
|
+
return apiRequest(`/inbound/delivered?${params}`);
|
|
333
|
+
}
|
|
334
|
+
async function getCreditTransactions(options = {}) {
|
|
335
|
+
const params = new URLSearchParams();
|
|
336
|
+
if (options.limit) params.set("limit", String(options.limit));
|
|
337
|
+
if (options.offset) params.set("offset", String(options.offset));
|
|
338
|
+
if (options.type) params.set("type", options.type);
|
|
339
|
+
return apiRequest(`/credits/transactions?${params}`);
|
|
340
|
+
}
|
|
341
|
+
async function purchaseCredits(optionIndex) {
|
|
342
|
+
return apiRequest("/credits/purchase", {
|
|
289
343
|
method: "POST",
|
|
290
|
-
body: JSON.stringify({
|
|
291
|
-
sender_email: options.fromEmail,
|
|
292
|
-
recipient_emails: options.toEmails,
|
|
293
|
-
amount_sats: options.amountSats
|
|
294
|
-
})
|
|
344
|
+
body: JSON.stringify({ optionIndex })
|
|
295
345
|
});
|
|
296
346
|
}
|
|
347
|
+
async function checkPaymentStatus(paymentHash) {
|
|
348
|
+
const v1BaseUrl = getApiBaseUrl();
|
|
349
|
+
const rootApiUrl = v1BaseUrl.replace("/api/v1", "/api");
|
|
350
|
+
const token = getToken();
|
|
351
|
+
const headers = {
|
|
352
|
+
"Content-Type": "application/json"
|
|
353
|
+
};
|
|
354
|
+
if (token) {
|
|
355
|
+
headers.Authorization = `Bearer ${token}`;
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
const response = await fetch(`${rootApiUrl}/lightning/webhook?payment_hash=${paymentHash}`, {
|
|
359
|
+
headers
|
|
360
|
+
});
|
|
361
|
+
const json = await response.json();
|
|
362
|
+
if (!response.ok || json.success === false) {
|
|
363
|
+
return {
|
|
364
|
+
ok: false,
|
|
365
|
+
error: json.error || {
|
|
366
|
+
code: "UNKNOWN_ERROR",
|
|
367
|
+
message: `Request failed with status ${response.status}`
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
ok: true,
|
|
373
|
+
data: {
|
|
374
|
+
paid: json.paid || false,
|
|
375
|
+
delivered: json.delivered,
|
|
376
|
+
status: json.status || json.state,
|
|
377
|
+
pendingEmailId: json.pendingEmailId
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
} catch (error) {
|
|
381
|
+
return {
|
|
382
|
+
ok: false,
|
|
383
|
+
error: {
|
|
384
|
+
code: "NETWORK_ERROR",
|
|
385
|
+
message: error instanceof Error ? error.message : "Network request failed"
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function getCreditPurchaseOptions() {
|
|
391
|
+
return [
|
|
392
|
+
{
|
|
393
|
+
amountSats: 1e3,
|
|
394
|
+
priceSats: 1e3,
|
|
395
|
+
bonusSats: 0,
|
|
396
|
+
label: "1,000 sats",
|
|
397
|
+
description: "Good for ~10 emails"
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
amountSats: 5e3,
|
|
401
|
+
priceSats: 4500,
|
|
402
|
+
bonusSats: 500,
|
|
403
|
+
label: "5,000 sats",
|
|
404
|
+
description: "Good for ~50 emails (+10% bonus)"
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
amountSats: 1e4,
|
|
408
|
+
priceSats: 8500,
|
|
409
|
+
bonusSats: 1500,
|
|
410
|
+
label: "10,000 sats",
|
|
411
|
+
description: "Good for ~100 emails (+15% bonus)"
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
amountSats: 5e4,
|
|
415
|
+
priceSats: 4e4,
|
|
416
|
+
bonusSats: 1e4,
|
|
417
|
+
label: "50,000 sats",
|
|
418
|
+
description: "Good for ~500 emails (+20% bonus)"
|
|
419
|
+
}
|
|
420
|
+
];
|
|
421
|
+
}
|
|
297
422
|
|
|
298
423
|
// src/commands/whoami.ts
|
|
299
424
|
async function whoamiCommand() {
|
|
@@ -307,14 +432,16 @@ async function whoamiCommand() {
|
|
|
307
432
|
console.log(pc3.yellow("Not logged in."));
|
|
308
433
|
process.exit(1);
|
|
309
434
|
}
|
|
435
|
+
const spinner = ora2("Fetching account info...").start();
|
|
310
436
|
const result = await getWhoami();
|
|
437
|
+
spinner.stop();
|
|
311
438
|
if (result.ok) {
|
|
312
439
|
console.log();
|
|
313
440
|
console.log(pc3.bold("Current User"));
|
|
314
441
|
console.log();
|
|
315
442
|
console.log(` ${pc3.dim("Email:")} ${result.data.email}`);
|
|
316
443
|
if (result.data.username) {
|
|
317
|
-
console.log(` ${pc3.dim("Username:")} ${result.data.username}`);
|
|
444
|
+
console.log(` ${pc3.dim("Username:")} ${result.data.username}@btc.email`);
|
|
318
445
|
}
|
|
319
446
|
console.log(` ${pc3.dim("User ID:")} ${result.data.id}`);
|
|
320
447
|
const expiryInfo = getTokenExpiryInfo();
|
|
@@ -329,7 +456,7 @@ async function whoamiCommand() {
|
|
|
329
456
|
}
|
|
330
457
|
} else {
|
|
331
458
|
console.log();
|
|
332
|
-
console.log(pc3.bold("Current User (cached)"));
|
|
459
|
+
console.log(pc3.bold("Current User") + pc3.dim(" (cached)"));
|
|
333
460
|
console.log();
|
|
334
461
|
console.log(` ${pc3.dim("Email:")} ${auth.email}`);
|
|
335
462
|
console.log(` ${pc3.dim("User ID:")} ${auth.userId}`);
|
|
@@ -342,29 +469,78 @@ async function whoamiCommand() {
|
|
|
342
469
|
|
|
343
470
|
// src/commands/list.ts
|
|
344
471
|
import pc4 from "picocolors";
|
|
472
|
+
import ora3 from "ora";
|
|
473
|
+
|
|
474
|
+
// src/utils/cache.ts
|
|
475
|
+
var CACHE_KEY = "emailCache";
|
|
476
|
+
var CACHE_TTL_MS = 30 * 60 * 1e3;
|
|
477
|
+
function cacheEmailList(folder, emails) {
|
|
478
|
+
config.set(CACHE_KEY, {
|
|
479
|
+
folder,
|
|
480
|
+
emails,
|
|
481
|
+
timestamp: Date.now()
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
function getCachedEmailList(folder) {
|
|
485
|
+
const cache = config.get(CACHE_KEY);
|
|
486
|
+
if (!cache) return null;
|
|
487
|
+
if (Date.now() - cache.timestamp > CACHE_TTL_MS) {
|
|
488
|
+
config.delete(CACHE_KEY);
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
if (folder && cache.folder !== folder) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
return cache;
|
|
495
|
+
}
|
|
496
|
+
function getEmailIdByIndex(index) {
|
|
497
|
+
const cache = getCachedEmailList();
|
|
498
|
+
if (!cache) return null;
|
|
499
|
+
const idx = index - 1;
|
|
500
|
+
if (idx < 0 || idx >= cache.emails.length) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
return cache.emails[idx].id;
|
|
504
|
+
}
|
|
505
|
+
function isNumericId(id) {
|
|
506
|
+
return /^\d+$/.test(id);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/commands/list.ts
|
|
345
510
|
async function listCommand(options) {
|
|
346
511
|
if (!isAuthenticated()) {
|
|
347
512
|
console.log(pc4.yellow("Not logged in."));
|
|
348
513
|
console.log(pc4.dim("Run `btcemail login` to authenticate."));
|
|
349
514
|
process.exit(1);
|
|
350
515
|
}
|
|
516
|
+
const folder = options.folder || "inbox";
|
|
517
|
+
const limit = options.limit || 20;
|
|
518
|
+
const page = options.page || 1;
|
|
519
|
+
const offset = (page - 1) * limit;
|
|
520
|
+
const spinner = ora3("Loading emails...").start();
|
|
351
521
|
const result = await getEmails({
|
|
352
|
-
folder
|
|
353
|
-
limit
|
|
522
|
+
folder,
|
|
523
|
+
limit,
|
|
524
|
+
offset
|
|
354
525
|
});
|
|
355
526
|
if (!result.ok) {
|
|
356
|
-
|
|
527
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
357
528
|
process.exit(1);
|
|
358
529
|
}
|
|
359
530
|
const { data: emails, pagination } = result.data;
|
|
531
|
+
spinner.stop();
|
|
532
|
+
const cachedEmails = emails.map((e) => ({
|
|
533
|
+
id: e.id,
|
|
534
|
+
subject: e.subject,
|
|
535
|
+
from: e.from.name || e.from.email
|
|
536
|
+
}));
|
|
537
|
+
cacheEmailList(folder, cachedEmails);
|
|
360
538
|
if (options.json) {
|
|
361
539
|
console.log(JSON.stringify({ emails, pagination }, null, 2));
|
|
362
540
|
return;
|
|
363
541
|
}
|
|
364
542
|
console.log();
|
|
365
|
-
console.log(
|
|
366
|
-
pc4.bold(`${options.folder || "Inbox"} (${pagination.total} emails)`)
|
|
367
|
-
);
|
|
543
|
+
console.log(pc4.bold(`${capitalize(folder)} (${pagination.total} emails)`));
|
|
368
544
|
console.log();
|
|
369
545
|
if (emails.length === 0) {
|
|
370
546
|
console.log(pc4.dim(" No emails found."));
|
|
@@ -372,26 +548,34 @@ async function listCommand(options) {
|
|
|
372
548
|
return;
|
|
373
549
|
}
|
|
374
550
|
console.log(
|
|
375
|
-
` ${pc4.dim("
|
|
551
|
+
` ${pc4.dim("#".padEnd(4))} ${pc4.dim("From".padEnd(25))} ${pc4.dim("Subject".padEnd(45))} ${pc4.dim("Date")}`
|
|
376
552
|
);
|
|
377
|
-
console.log(pc4.dim(" " + "-".repeat(
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
const
|
|
553
|
+
console.log(pc4.dim(" " + "-".repeat(85)));
|
|
554
|
+
emails.forEach((email, index) => {
|
|
555
|
+
const num = pc4.cyan(String(index + 1).padEnd(4));
|
|
556
|
+
const unreadMarker = email.unread ? pc4.cyan("\u25CF") : " ";
|
|
381
557
|
const from = truncate(email.from.name || email.from.email, 23).padEnd(25);
|
|
382
|
-
const subject = truncate(email.subject,
|
|
558
|
+
const subject = truncate(email.subject, 43).padEnd(45);
|
|
383
559
|
const date = formatDate(email.date);
|
|
384
|
-
console.log(`${unreadMarker} ${
|
|
385
|
-
}
|
|
560
|
+
console.log(`${unreadMarker} ${num}${from} ${subject} ${pc4.dim(date)}`);
|
|
561
|
+
});
|
|
386
562
|
console.log();
|
|
387
|
-
|
|
563
|
+
const totalPages = Math.ceil(pagination.total / limit);
|
|
564
|
+
const currentPage = page;
|
|
565
|
+
if (totalPages > 1) {
|
|
388
566
|
console.log(
|
|
389
|
-
pc4.dim(
|
|
390
|
-
` Showing ${emails.length} of ${pagination.total}. Use --limit to see more.`
|
|
391
|
-
)
|
|
567
|
+
pc4.dim(` Page ${currentPage} of ${totalPages} (${emails.length} of ${pagination.total})`)
|
|
392
568
|
);
|
|
569
|
+
if (currentPage < totalPages) {
|
|
570
|
+
console.log(pc4.dim(` Next page: btcemail list --page ${currentPage + 1}`));
|
|
571
|
+
}
|
|
393
572
|
console.log();
|
|
394
573
|
}
|
|
574
|
+
console.log(pc4.dim(" Read email: btcemail read <#> or btcemail read <id>"));
|
|
575
|
+
console.log();
|
|
576
|
+
}
|
|
577
|
+
function capitalize(str) {
|
|
578
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
395
579
|
}
|
|
396
580
|
function truncate(str, maxLength) {
|
|
397
581
|
if (str.length <= maxLength) return str;
|
|
@@ -419,21 +603,42 @@ function formatDate(dateString) {
|
|
|
419
603
|
|
|
420
604
|
// src/commands/read.ts
|
|
421
605
|
import pc5 from "picocolors";
|
|
422
|
-
|
|
606
|
+
import ora4 from "ora";
|
|
607
|
+
async function readCommand(idOrNumber, options) {
|
|
423
608
|
if (!isAuthenticated()) {
|
|
424
609
|
console.log(pc5.yellow("Not logged in."));
|
|
425
610
|
console.log(pc5.dim("Run `btcemail login` to authenticate."));
|
|
426
611
|
process.exit(1);
|
|
427
612
|
}
|
|
428
|
-
|
|
613
|
+
let emailId = idOrNumber;
|
|
614
|
+
if (isNumericId(idOrNumber)) {
|
|
615
|
+
const index = parseInt(idOrNumber, 10);
|
|
616
|
+
const cachedId = getEmailIdByIndex(index);
|
|
617
|
+
if (cachedId) {
|
|
618
|
+
emailId = cachedId;
|
|
619
|
+
} else {
|
|
620
|
+
const cache = getCachedEmailList();
|
|
621
|
+
if (cache) {
|
|
622
|
+
console.error(pc5.red(`Email #${index} not found in list.`));
|
|
623
|
+
console.log(pc5.dim(`List has ${cache.emails.length} emails. Run 'btcemail list' to refresh.`));
|
|
624
|
+
} else {
|
|
625
|
+
console.error(pc5.red(`No email list cached.`));
|
|
626
|
+
console.log(pc5.dim(`Run 'btcemail list' first, then use 'btcemail read <#>'.`));
|
|
627
|
+
}
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const spinner = ora4("Loading email...").start();
|
|
632
|
+
const result = await getEmail(emailId);
|
|
429
633
|
if (!result.ok) {
|
|
430
634
|
if (result.error.code === "NOT_FOUND") {
|
|
431
|
-
|
|
635
|
+
spinner.fail(`Email not found: ${emailId}`);
|
|
432
636
|
} else {
|
|
433
|
-
|
|
637
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
434
638
|
}
|
|
435
639
|
process.exit(1);
|
|
436
640
|
}
|
|
641
|
+
spinner.stop();
|
|
437
642
|
const email = result.data;
|
|
438
643
|
if (options.json) {
|
|
439
644
|
console.log(JSON.stringify(email, null, 2));
|
|
@@ -482,46 +687,349 @@ function formatFullDate(dateString) {
|
|
|
482
687
|
});
|
|
483
688
|
}
|
|
484
689
|
|
|
485
|
-
// src/commands/
|
|
690
|
+
// src/commands/search.ts
|
|
486
691
|
import pc6 from "picocolors";
|
|
487
|
-
|
|
692
|
+
import ora5 from "ora";
|
|
693
|
+
async function searchCommand(query, options) {
|
|
488
694
|
if (!isAuthenticated()) {
|
|
489
695
|
console.log(pc6.yellow("Not logged in."));
|
|
490
696
|
console.log(pc6.dim("Run `btcemail login` to authenticate."));
|
|
491
697
|
process.exit(1);
|
|
492
698
|
}
|
|
699
|
+
if (!query || query.trim().length === 0) {
|
|
700
|
+
console.error(pc6.red("Error: Search query is required"));
|
|
701
|
+
process.exit(1);
|
|
702
|
+
}
|
|
703
|
+
const spinner = ora5(`Searching for "${query}"...`).start();
|
|
704
|
+
const result = await getEmails({
|
|
705
|
+
search: query.trim(),
|
|
706
|
+
limit: options.limit || 20
|
|
707
|
+
});
|
|
708
|
+
if (!result.ok) {
|
|
709
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
const { data: emails, pagination } = result.data;
|
|
713
|
+
spinner.stop();
|
|
714
|
+
const cachedEmails = emails.map((e) => ({
|
|
715
|
+
id: e.id,
|
|
716
|
+
subject: e.subject,
|
|
717
|
+
from: e.from.name || e.from.email
|
|
718
|
+
}));
|
|
719
|
+
cacheEmailList("search", cachedEmails);
|
|
720
|
+
if (options.json) {
|
|
721
|
+
console.log(JSON.stringify({ query, emails, pagination }, null, 2));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
console.log();
|
|
725
|
+
console.log(pc6.bold(`Search: "${query}" (${pagination.total} found)`));
|
|
726
|
+
console.log();
|
|
727
|
+
if (emails.length === 0) {
|
|
728
|
+
console.log(pc6.dim(" No emails found matching your search."));
|
|
729
|
+
console.log();
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
console.log(
|
|
733
|
+
` ${pc6.dim("#".padEnd(4))} ${pc6.dim("From".padEnd(25))} ${pc6.dim("Subject".padEnd(45))} ${pc6.dim("Date")}`
|
|
734
|
+
);
|
|
735
|
+
console.log(pc6.dim(" " + "-".repeat(85)));
|
|
736
|
+
emails.forEach((email, index) => {
|
|
737
|
+
const num = pc6.cyan(String(index + 1).padEnd(4));
|
|
738
|
+
const unreadMarker = email.unread ? pc6.cyan("\u25CF") : " ";
|
|
739
|
+
const from = truncate2(email.from.name || email.from.email, 23).padEnd(25);
|
|
740
|
+
const subject = truncate2(email.subject, 43).padEnd(45);
|
|
741
|
+
const date = formatDate2(email.date);
|
|
742
|
+
console.log(`${unreadMarker} ${num}${from} ${subject} ${pc6.dim(date)}`);
|
|
743
|
+
});
|
|
744
|
+
console.log();
|
|
745
|
+
if (pagination.hasMore) {
|
|
746
|
+
console.log(
|
|
747
|
+
pc6.dim(
|
|
748
|
+
` Showing ${emails.length} of ${pagination.total}. Use --limit to see more.`
|
|
749
|
+
)
|
|
750
|
+
);
|
|
751
|
+
console.log();
|
|
752
|
+
}
|
|
753
|
+
console.log(pc6.dim(" Read email: btcemail read <#> or btcemail read <id>"));
|
|
754
|
+
console.log();
|
|
755
|
+
}
|
|
756
|
+
function truncate2(str, maxLength) {
|
|
757
|
+
if (str.length <= maxLength) return str;
|
|
758
|
+
return str.slice(0, maxLength - 1) + "\u2026";
|
|
759
|
+
}
|
|
760
|
+
function formatDate2(dateString) {
|
|
761
|
+
const date = new Date(dateString);
|
|
762
|
+
const now = /* @__PURE__ */ new Date();
|
|
763
|
+
const diffMs = now.getTime() - date.getTime();
|
|
764
|
+
const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
|
|
765
|
+
if (diffDays === 0) {
|
|
766
|
+
return date.toLocaleTimeString("en-US", {
|
|
767
|
+
hour: "numeric",
|
|
768
|
+
minute: "2-digit"
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
if (diffDays < 7) {
|
|
772
|
+
return date.toLocaleDateString("en-US", { weekday: "short" });
|
|
773
|
+
}
|
|
774
|
+
return date.toLocaleDateString("en-US", {
|
|
775
|
+
month: "short",
|
|
776
|
+
day: "numeric"
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/commands/credits.ts
|
|
781
|
+
import pc8 from "picocolors";
|
|
782
|
+
import ora6 from "ora";
|
|
783
|
+
import qrcode from "qrcode-terminal";
|
|
784
|
+
|
|
785
|
+
// src/utils/payment-polling.ts
|
|
786
|
+
import pc7 from "picocolors";
|
|
787
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
788
|
+
async function pollForPayment(options) {
|
|
789
|
+
const { paymentHash, checkFn, maxAttempts = 120, intervalMs = 2500, onPaid } = options;
|
|
790
|
+
let frame = 0;
|
|
791
|
+
let attempts = 0;
|
|
792
|
+
process.stdout.write(`${pc7.cyan(SPINNER_FRAMES[0])} Waiting for payment...`);
|
|
793
|
+
while (attempts < maxAttempts) {
|
|
794
|
+
try {
|
|
795
|
+
const status = await checkFn(paymentHash);
|
|
796
|
+
if (status.paid) {
|
|
797
|
+
process.stdout.write("\r" + " ".repeat(50) + "\r");
|
|
798
|
+
console.log(pc7.green("\u2713") + " Payment received!");
|
|
799
|
+
if (onPaid) onPaid();
|
|
800
|
+
return status;
|
|
801
|
+
}
|
|
802
|
+
} catch {
|
|
803
|
+
}
|
|
804
|
+
frame = (frame + 1) % SPINNER_FRAMES.length;
|
|
805
|
+
const timeLeft = Math.ceil((maxAttempts - attempts) * intervalMs / 1e3 / 60);
|
|
806
|
+
process.stdout.write(
|
|
807
|
+
`\r${pc7.cyan(SPINNER_FRAMES[frame])} Waiting for payment... (${timeLeft}m remaining)`
|
|
808
|
+
);
|
|
809
|
+
await sleep2(intervalMs);
|
|
810
|
+
attempts++;
|
|
811
|
+
}
|
|
812
|
+
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
813
|
+
console.log(pc7.yellow("\u26A0") + " Timed out waiting for payment.");
|
|
814
|
+
return { paid: false };
|
|
815
|
+
}
|
|
816
|
+
function sleep2(ms) {
|
|
817
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// src/commands/credits.ts
|
|
821
|
+
async function creditsBalanceCommand() {
|
|
822
|
+
if (!isAuthenticated()) {
|
|
823
|
+
console.log(pc8.yellow("Not logged in."));
|
|
824
|
+
console.log(pc8.dim("Run `btcemail login` to authenticate."));
|
|
825
|
+
process.exit(1);
|
|
826
|
+
}
|
|
827
|
+
const spinner = ora6("Fetching balance...").start();
|
|
493
828
|
const result = await getCreditsBalance();
|
|
494
829
|
if (!result.ok) {
|
|
495
|
-
|
|
830
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
496
831
|
process.exit(1);
|
|
497
832
|
}
|
|
498
|
-
|
|
833
|
+
spinner.stop();
|
|
834
|
+
const { balanceSats, lifetimePurchasedSats, lifetimeSpentSats } = result.data;
|
|
499
835
|
console.log();
|
|
500
|
-
console.log(
|
|
836
|
+
console.log(pc8.bold("Credit Balance"));
|
|
501
837
|
console.log();
|
|
502
|
-
console.log(` ${
|
|
838
|
+
console.log(` ${pc8.green(formatSats(balanceSats))} sats`);
|
|
503
839
|
console.log();
|
|
840
|
+
if (lifetimePurchasedSats > 0 || lifetimeSpentSats > 0) {
|
|
841
|
+
console.log(pc8.dim(` Purchased: ${formatSats(lifetimePurchasedSats)} sats`));
|
|
842
|
+
console.log(pc8.dim(` Spent: ${formatSats(lifetimeSpentSats)} sats`));
|
|
843
|
+
console.log();
|
|
844
|
+
}
|
|
504
845
|
if (balanceSats === 0) {
|
|
505
|
-
console.log(
|
|
506
|
-
console.log(
|
|
846
|
+
console.log(pc8.dim(" No credits available."));
|
|
847
|
+
console.log(pc8.dim(" Run `btcemail credits purchase` to buy credits."));
|
|
848
|
+
console.log();
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
async function creditsHistoryCommand(options) {
|
|
852
|
+
if (!isAuthenticated()) {
|
|
853
|
+
console.log(pc8.yellow("Not logged in."));
|
|
854
|
+
console.log(pc8.dim("Run `btcemail login` to authenticate."));
|
|
855
|
+
process.exit(1);
|
|
856
|
+
}
|
|
857
|
+
const spinner = ora6("Loading transaction history...").start();
|
|
858
|
+
const result = await getCreditTransactions({
|
|
859
|
+
limit: options.limit || 20,
|
|
860
|
+
type: options.type
|
|
861
|
+
});
|
|
862
|
+
if (!result.ok) {
|
|
863
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
864
|
+
process.exit(1);
|
|
865
|
+
}
|
|
866
|
+
const { data: transactions, pagination } = result.data;
|
|
867
|
+
spinner.stop();
|
|
868
|
+
if (options.json) {
|
|
869
|
+
console.log(JSON.stringify({ transactions, pagination }, null, 2));
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
console.log();
|
|
873
|
+
console.log(pc8.bold("Transaction History"));
|
|
874
|
+
console.log();
|
|
875
|
+
if (transactions.length === 0) {
|
|
876
|
+
console.log(pc8.dim(" No transactions found."));
|
|
877
|
+
console.log();
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
console.log(
|
|
881
|
+
` ${pc8.dim("Date".padEnd(12))} ${pc8.dim("Type".padEnd(15))} ${pc8.dim("Amount".padEnd(12))} ${pc8.dim("Balance")}`
|
|
882
|
+
);
|
|
883
|
+
console.log(pc8.dim(" " + "-".repeat(55)));
|
|
884
|
+
for (const tx of transactions) {
|
|
885
|
+
const date = formatDate3(tx.createdAt);
|
|
886
|
+
const type = formatTransactionType(tx.transactionType).padEnd(15);
|
|
887
|
+
const amount = formatAmount(tx.amountSats).padEnd(12);
|
|
888
|
+
const balance = formatSats(tx.balanceAfter);
|
|
889
|
+
console.log(` ${date.padEnd(12)} ${type} ${amount} ${pc8.dim(balance)}`);
|
|
890
|
+
}
|
|
891
|
+
console.log();
|
|
892
|
+
if (pagination.hasMore) {
|
|
893
|
+
console.log(pc8.dim(` Use --limit to see more transactions.`));
|
|
894
|
+
console.log();
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
async function creditsPurchaseCommand(options) {
|
|
898
|
+
if (!isAuthenticated()) {
|
|
899
|
+
console.log(pc8.yellow("Not logged in."));
|
|
900
|
+
console.log(pc8.dim("Run `btcemail login` to authenticate."));
|
|
901
|
+
process.exit(1);
|
|
902
|
+
}
|
|
903
|
+
const purchaseOptions = getCreditPurchaseOptions();
|
|
904
|
+
if (options.option === void 0) {
|
|
905
|
+
console.log();
|
|
906
|
+
console.log(pc8.bold("Purchase Options"));
|
|
907
|
+
console.log();
|
|
908
|
+
purchaseOptions.forEach((opt, index) => {
|
|
909
|
+
console.log(` ${pc8.cyan(`[${index}]`)} ${opt.label}`);
|
|
910
|
+
console.log(` ${pc8.dim(opt.description)}`);
|
|
911
|
+
console.log(` ${pc8.dim(`Price: ${formatSats(opt.priceSats)} sats`)}`);
|
|
912
|
+
if (opt.bonusSats > 0) {
|
|
913
|
+
console.log(` ${pc8.green(`+${formatSats(opt.bonusSats)} bonus sats`)}`);
|
|
914
|
+
}
|
|
915
|
+
console.log();
|
|
916
|
+
});
|
|
917
|
+
console.log(pc8.dim(" Usage: btcemail credits purchase --option <index>"));
|
|
918
|
+
console.log(pc8.dim(" Add --wait to wait for payment confirmation."));
|
|
919
|
+
console.log();
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (options.option < 0 || options.option >= purchaseOptions.length) {
|
|
923
|
+
console.error(pc8.red(`Error: Invalid option. Choose 0-${purchaseOptions.length - 1}.`));
|
|
924
|
+
process.exit(1);
|
|
925
|
+
}
|
|
926
|
+
const selectedOption = purchaseOptions[options.option];
|
|
927
|
+
const spinner = ora6("Creating invoice...").start();
|
|
928
|
+
const result = await purchaseCredits(options.option);
|
|
929
|
+
if (!result.ok) {
|
|
930
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
931
|
+
process.exit(1);
|
|
932
|
+
}
|
|
933
|
+
spinner.stop();
|
|
934
|
+
const purchase = result.data;
|
|
935
|
+
if (options.json) {
|
|
936
|
+
console.log(JSON.stringify(purchase, null, 2));
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
console.log();
|
|
940
|
+
console.log(pc8.bold("Lightning Invoice"));
|
|
941
|
+
console.log();
|
|
942
|
+
console.log(` ${pc8.dim("Amount:")} ${formatSats(purchase.amountSats)} sats`);
|
|
943
|
+
console.log(` ${pc8.dim("Credits:")} ${formatSats(purchase.creditsToReceive)} sats`);
|
|
944
|
+
if (purchase.bonusSats > 0) {
|
|
945
|
+
console.log(` ${pc8.dim("Bonus:")} ${pc8.green(`+${formatSats(purchase.bonusSats)} sats`)}`);
|
|
946
|
+
}
|
|
947
|
+
console.log();
|
|
948
|
+
console.log(pc8.bold("Scan to pay:"));
|
|
949
|
+
console.log();
|
|
950
|
+
qrcode.generate(purchase.invoice, { small: true }, (qr) => {
|
|
951
|
+
const indentedQr = qr.split("\n").map((line) => " " + line).join("\n");
|
|
952
|
+
console.log(indentedQr);
|
|
953
|
+
});
|
|
954
|
+
console.log();
|
|
955
|
+
console.log(pc8.bold("Invoice:"));
|
|
956
|
+
console.log();
|
|
957
|
+
console.log(` ${purchase.invoice}`);
|
|
958
|
+
console.log();
|
|
959
|
+
console.log(pc8.dim(` Payment hash: ${purchase.paymentHash}`));
|
|
960
|
+
console.log(pc8.dim(` Expires: ${new Date(purchase.expiresAt).toLocaleString()}`));
|
|
961
|
+
console.log();
|
|
962
|
+
if (options.wait) {
|
|
963
|
+
console.log();
|
|
964
|
+
const status = await pollForPayment({
|
|
965
|
+
paymentHash: purchase.paymentHash,
|
|
966
|
+
checkFn: async (hash) => {
|
|
967
|
+
const result2 = await checkPaymentStatus(hash);
|
|
968
|
+
if (result2.ok) {
|
|
969
|
+
return { paid: result2.data.paid };
|
|
970
|
+
}
|
|
971
|
+
return { paid: false };
|
|
972
|
+
},
|
|
973
|
+
onPaid: async () => {
|
|
974
|
+
const balanceResult = await getCreditsBalance();
|
|
975
|
+
if (balanceResult.ok) {
|
|
976
|
+
console.log();
|
|
977
|
+
console.log(`${pc8.dim("New balance:")} ${pc8.green(formatSats(balanceResult.data.balanceSats))} sats`);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
if (!status.paid) {
|
|
982
|
+
console.log(pc8.dim(" You can still pay the invoice and credits will be added."));
|
|
983
|
+
}
|
|
984
|
+
console.log();
|
|
985
|
+
} else {
|
|
986
|
+
console.log(pc8.dim(" Scan the QR code or copy the invoice above to pay."));
|
|
987
|
+
console.log(pc8.dim(" Use --wait to wait for payment confirmation."));
|
|
507
988
|
console.log();
|
|
508
989
|
}
|
|
509
990
|
}
|
|
510
991
|
function formatSats(sats) {
|
|
511
992
|
return sats.toLocaleString();
|
|
512
993
|
}
|
|
994
|
+
function formatAmount(sats) {
|
|
995
|
+
if (sats >= 0) {
|
|
996
|
+
return pc8.green(`+${formatSats(sats)}`);
|
|
997
|
+
}
|
|
998
|
+
return pc8.red(formatSats(sats));
|
|
999
|
+
}
|
|
1000
|
+
function formatTransactionType(type) {
|
|
1001
|
+
const types = {
|
|
1002
|
+
topup: "Top-up",
|
|
1003
|
+
email_sent: "Email Sent",
|
|
1004
|
+
email_received: "Email Received",
|
|
1005
|
+
refund: "Refund",
|
|
1006
|
+
bonus: "Bonus"
|
|
1007
|
+
};
|
|
1008
|
+
return types[type] || type;
|
|
1009
|
+
}
|
|
1010
|
+
function formatDate3(dateString) {
|
|
1011
|
+
const date = new Date(dateString);
|
|
1012
|
+
return date.toLocaleDateString("en-US", {
|
|
1013
|
+
month: "short",
|
|
1014
|
+
day: "numeric"
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
513
1017
|
|
|
514
1018
|
// src/commands/send.ts
|
|
515
|
-
import
|
|
1019
|
+
import pc9 from "picocolors";
|
|
1020
|
+
import ora7 from "ora";
|
|
1021
|
+
import qrcode2 from "qrcode-terminal";
|
|
516
1022
|
async function sendCommand(options) {
|
|
517
1023
|
if (!isAuthenticated()) {
|
|
518
|
-
console.log(
|
|
519
|
-
console.log(
|
|
1024
|
+
console.log(pc9.yellow("Not logged in."));
|
|
1025
|
+
console.log(pc9.dim("Run `btcemail login` to authenticate."));
|
|
520
1026
|
process.exit(1);
|
|
521
1027
|
}
|
|
522
1028
|
let fromEmail = options.fromEmail;
|
|
523
1029
|
if (!fromEmail) {
|
|
1030
|
+
const spinner2 = ora7("Fetching account info...").start();
|
|
524
1031
|
const whoamiResult = await getWhoami();
|
|
1032
|
+
spinner2.stop();
|
|
525
1033
|
if (whoamiResult.ok && whoamiResult.data.username) {
|
|
526
1034
|
fromEmail = `${whoamiResult.data.username}@btc.email`;
|
|
527
1035
|
} else {
|
|
@@ -532,200 +1040,303 @@ async function sendCommand(options) {
|
|
|
532
1040
|
}
|
|
533
1041
|
}
|
|
534
1042
|
if (!fromEmail || !fromEmail.endsWith("@btc.email")) {
|
|
535
|
-
console.error(
|
|
536
|
-
console.log(
|
|
1043
|
+
console.error(pc9.red("No @btc.email address found."));
|
|
1044
|
+
console.log(pc9.dim("Register a username at https://btc.email"));
|
|
537
1045
|
process.exit(1);
|
|
538
1046
|
}
|
|
539
1047
|
if (!options.to) {
|
|
540
|
-
console.error(
|
|
1048
|
+
console.error(pc9.red("Recipient is required. Use --to <email>"));
|
|
541
1049
|
process.exit(1);
|
|
542
1050
|
}
|
|
543
1051
|
if (!options.subject) {
|
|
544
|
-
console.error(
|
|
1052
|
+
console.error(pc9.red("Subject is required. Use --subject <text>"));
|
|
545
1053
|
process.exit(1);
|
|
546
1054
|
}
|
|
547
1055
|
if (!options.body) {
|
|
548
|
-
console.error(
|
|
1056
|
+
console.error(pc9.red("Body is required. Use --body <text>"));
|
|
549
1057
|
process.exit(1);
|
|
550
1058
|
}
|
|
551
1059
|
const toEmails = options.to.split(",").map((e) => e.trim());
|
|
552
1060
|
console.log();
|
|
553
|
-
console.log(
|
|
1061
|
+
console.log(pc9.bold("Sending Email"));
|
|
554
1062
|
console.log();
|
|
555
|
-
console.log(`${
|
|
556
|
-
console.log(`${
|
|
557
|
-
console.log(`${
|
|
1063
|
+
console.log(`${pc9.dim("From:")} ${fromEmail}`);
|
|
1064
|
+
console.log(`${pc9.dim("To:")} ${toEmails.join(", ")}`);
|
|
1065
|
+
console.log(`${pc9.dim("Subject:")} ${options.subject}`);
|
|
558
1066
|
console.log();
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
1067
|
+
if (!options.paymentHash) {
|
|
1068
|
+
const spinner2 = ora7("Getting invoice...").start();
|
|
1069
|
+
const invoiceResult = await getL402Invoice({
|
|
1070
|
+
fromEmail,
|
|
1071
|
+
toEmails
|
|
1072
|
+
});
|
|
1073
|
+
spinner2.stop();
|
|
1074
|
+
if (invoiceResult.ok && invoiceResult.data.amountSats > 0) {
|
|
1075
|
+
const invoice = invoiceResult.data;
|
|
1076
|
+
console.log(pc9.yellow(`Payment required: ${invoice.amountSats} sats`));
|
|
1077
|
+
console.log();
|
|
1078
|
+
console.log(pc9.bold("Scan to pay:"));
|
|
1079
|
+
console.log();
|
|
1080
|
+
qrcode2.generate(invoice.invoice, { small: true }, (qr) => {
|
|
1081
|
+
const indentedQr = qr.split("\n").map((line) => " " + line).join("\n");
|
|
1082
|
+
console.log(indentedQr);
|
|
1083
|
+
});
|
|
1084
|
+
console.log();
|
|
1085
|
+
console.log(pc9.dim("Lightning Invoice:"));
|
|
1086
|
+
console.log(pc9.cyan(invoice.invoice));
|
|
1087
|
+
console.log();
|
|
1088
|
+
if (options.wait) {
|
|
1089
|
+
const status = await pollForPayment({
|
|
1090
|
+
paymentHash: invoice.paymentHash,
|
|
1091
|
+
checkFn: async (hash) => {
|
|
1092
|
+
const result2 = await checkPaymentStatus(hash);
|
|
1093
|
+
if (result2.ok) {
|
|
1094
|
+
return { paid: result2.data.paid };
|
|
1095
|
+
}
|
|
1096
|
+
return { paid: false };
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
if (status.paid) {
|
|
1100
|
+
const sendSpinner = ora7("Sending email...").start();
|
|
1101
|
+
const result2 = await sendEmail({
|
|
1102
|
+
to: toEmails,
|
|
1103
|
+
subject: options.subject,
|
|
1104
|
+
body: options.body,
|
|
1105
|
+
fromEmail,
|
|
1106
|
+
paymentHash: invoice.paymentHash
|
|
1107
|
+
});
|
|
1108
|
+
if (!result2.ok) {
|
|
1109
|
+
sendSpinner.fail(`Error: ${result2.error.message}`);
|
|
1110
|
+
process.exit(1);
|
|
1111
|
+
}
|
|
1112
|
+
sendSpinner.succeed("Email sent!");
|
|
1113
|
+
console.log();
|
|
1114
|
+
console.log(`${pc9.dim("Email ID:")} ${result2.data.emailId}`);
|
|
1115
|
+
if (result2.data.messageId) {
|
|
1116
|
+
console.log(`${pc9.dim("Message ID:")} ${result2.data.messageId}`);
|
|
1117
|
+
}
|
|
1118
|
+
console.log();
|
|
1119
|
+
} else {
|
|
1120
|
+
console.log(pc9.dim("Payment not confirmed. You can still pay and resend with --payment-hash."));
|
|
1121
|
+
}
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
console.log(pc9.dim("Pay this invoice with your Lightning wallet, then run:"));
|
|
1125
|
+
console.log(
|
|
1126
|
+
pc9.cyan(
|
|
1127
|
+
` btcemail send --to "${options.to}" --subject "${options.subject}" --body "${options.body}" --payment-hash ${invoice.paymentHash}`
|
|
1128
|
+
)
|
|
1129
|
+
);
|
|
1130
|
+
console.log();
|
|
1131
|
+
console.log(pc9.dim("Or use --wait to automatically wait for payment."));
|
|
1132
|
+
console.log();
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
578
1135
|
}
|
|
1136
|
+
const spinner = ora7("Sending email...").start();
|
|
579
1137
|
const result = await sendEmail({
|
|
580
1138
|
to: toEmails,
|
|
581
1139
|
subject: options.subject,
|
|
582
1140
|
body: options.body,
|
|
583
|
-
fromEmail
|
|
1141
|
+
fromEmail,
|
|
1142
|
+
paymentHash: options.paymentHash
|
|
584
1143
|
});
|
|
585
1144
|
if (!result.ok) {
|
|
586
1145
|
if (result.error.code === "PAYMENT_REQUIRED") {
|
|
587
|
-
|
|
588
|
-
console.log(
|
|
1146
|
+
spinner.fail("Payment required to send this email.");
|
|
1147
|
+
console.log(pc9.dim("Use the invoice above to pay, then include --payment-hash"));
|
|
589
1148
|
} else {
|
|
590
|
-
|
|
1149
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
591
1150
|
}
|
|
592
1151
|
process.exit(1);
|
|
593
1152
|
}
|
|
594
|
-
|
|
1153
|
+
spinner.succeed("Email sent!");
|
|
595
1154
|
console.log();
|
|
596
|
-
console.log(`${
|
|
1155
|
+
console.log(`${pc9.dim("Email ID:")} ${result.data.emailId}`);
|
|
597
1156
|
if (result.data.messageId) {
|
|
598
|
-
console.log(`${
|
|
1157
|
+
console.log(`${pc9.dim("Message ID:")} ${result.data.messageId}`);
|
|
599
1158
|
}
|
|
600
1159
|
console.log();
|
|
601
1160
|
}
|
|
602
1161
|
|
|
603
1162
|
// src/commands/inbound.ts
|
|
604
|
-
import
|
|
1163
|
+
import pc10 from "picocolors";
|
|
1164
|
+
import ora8 from "ora";
|
|
1165
|
+
async function inboundDeliveredCommand(options) {
|
|
1166
|
+
if (!isAuthenticated()) {
|
|
1167
|
+
console.log(pc10.yellow("Not logged in."));
|
|
1168
|
+
console.log(pc10.dim("Run `btcemail login` to authenticate."));
|
|
1169
|
+
process.exit(1);
|
|
1170
|
+
}
|
|
1171
|
+
const spinner = ora8("Loading delivered emails...").start();
|
|
1172
|
+
const result = await getDeliveredEmails({
|
|
1173
|
+
limit: options.limit || 20
|
|
1174
|
+
});
|
|
1175
|
+
if (!result.ok) {
|
|
1176
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
1177
|
+
process.exit(1);
|
|
1178
|
+
}
|
|
1179
|
+
const { data: emails, pagination } = result.data;
|
|
1180
|
+
spinner.stop();
|
|
1181
|
+
if (options.json) {
|
|
1182
|
+
console.log(JSON.stringify({ emails, pagination }, null, 2));
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
console.log();
|
|
1186
|
+
console.log(pc10.bold(`Delivered (${pagination.total} paid emails received)`));
|
|
1187
|
+
console.log();
|
|
1188
|
+
if (emails.length === 0) {
|
|
1189
|
+
console.log(pc10.dim(" No delivered emails yet."));
|
|
1190
|
+
console.log();
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
console.log(
|
|
1194
|
+
` ${pc10.dim("#".padEnd(4))} ${pc10.dim("From".padEnd(28))} ${pc10.dim("Subject".padEnd(40))} ${pc10.dim("Date")}`
|
|
1195
|
+
);
|
|
1196
|
+
console.log(pc10.dim(" " + "-".repeat(85)));
|
|
1197
|
+
emails.forEach((email, index) => {
|
|
1198
|
+
const num = pc10.cyan(String(index + 1).padEnd(4));
|
|
1199
|
+
const from = truncate3(email.from.name || email.from.email, 26).padEnd(28);
|
|
1200
|
+
const subject = truncate3(email.subject, 38).padEnd(40);
|
|
1201
|
+
const date = formatDate4(email.date);
|
|
1202
|
+
console.log(` ${num}${from} ${subject} ${pc10.dim(date)}`);
|
|
1203
|
+
});
|
|
1204
|
+
console.log();
|
|
1205
|
+
if (pagination.hasMore) {
|
|
1206
|
+
console.log(
|
|
1207
|
+
pc10.dim(` Showing ${emails.length} of ${pagination.total}. Use --limit to see more.`)
|
|
1208
|
+
);
|
|
1209
|
+
console.log();
|
|
1210
|
+
}
|
|
1211
|
+
console.log(pc10.dim(" Read email: btcemail read <id>"));
|
|
1212
|
+
console.log();
|
|
1213
|
+
}
|
|
605
1214
|
async function inboundPendingCommand(options) {
|
|
606
1215
|
if (!isAuthenticated()) {
|
|
607
|
-
console.log(
|
|
608
|
-
console.log(
|
|
1216
|
+
console.log(pc10.yellow("Not logged in."));
|
|
1217
|
+
console.log(pc10.dim("Run `btcemail login` to authenticate."));
|
|
609
1218
|
process.exit(1);
|
|
610
1219
|
}
|
|
1220
|
+
const spinner = ora8("Loading pending emails...").start();
|
|
611
1221
|
const result = await getPendingEmails({
|
|
612
1222
|
limit: options.limit || 20
|
|
613
1223
|
});
|
|
614
1224
|
if (!result.ok) {
|
|
615
|
-
|
|
1225
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
616
1226
|
process.exit(1);
|
|
617
1227
|
}
|
|
618
1228
|
const { data: emails, pagination } = result.data;
|
|
1229
|
+
spinner.stop();
|
|
619
1230
|
if (options.json) {
|
|
620
1231
|
console.log(JSON.stringify({ emails, pagination }, null, 2));
|
|
621
1232
|
return;
|
|
622
1233
|
}
|
|
623
1234
|
console.log();
|
|
624
|
-
console.log(
|
|
1235
|
+
console.log(pc10.bold(`Pending (${pagination.total} awaiting payment)`));
|
|
625
1236
|
console.log();
|
|
626
1237
|
if (emails.length === 0) {
|
|
627
|
-
console.log(
|
|
1238
|
+
console.log(pc10.dim(" No pending emails."));
|
|
628
1239
|
console.log();
|
|
629
1240
|
return;
|
|
630
1241
|
}
|
|
631
1242
|
console.log(
|
|
632
|
-
` ${
|
|
1243
|
+
` ${pc10.dim("#".padEnd(4))} ${pc10.dim("From".padEnd(28))} ${pc10.dim("Subject".padEnd(35))} ${pc10.dim("Sats")}`
|
|
633
1244
|
);
|
|
634
|
-
console.log(
|
|
635
|
-
|
|
636
|
-
const
|
|
637
|
-
const from =
|
|
638
|
-
const subject =
|
|
639
|
-
const sats =
|
|
640
|
-
console.log(` ${
|
|
641
|
-
}
|
|
1245
|
+
console.log(pc10.dim(" " + "-".repeat(80)));
|
|
1246
|
+
emails.forEach((email, index) => {
|
|
1247
|
+
const num = pc10.cyan(String(index + 1).padEnd(4));
|
|
1248
|
+
const from = truncate3(email.from.name || email.from.email, 26).padEnd(28);
|
|
1249
|
+
const subject = truncate3(email.subject, 33).padEnd(35);
|
|
1250
|
+
const sats = pc10.yellow(String(email.amountSats));
|
|
1251
|
+
console.log(` ${num}${from} ${subject} ${sats}`);
|
|
1252
|
+
});
|
|
642
1253
|
console.log();
|
|
643
1254
|
if (pagination.hasMore) {
|
|
644
1255
|
console.log(
|
|
645
|
-
|
|
1256
|
+
pc10.dim(` Showing ${emails.length} of ${pagination.total}. Use --limit to see more.`)
|
|
646
1257
|
);
|
|
647
1258
|
console.log();
|
|
648
1259
|
}
|
|
649
|
-
console.log(
|
|
1260
|
+
console.log(pc10.dim(" View details: btcemail inbound view <id>"));
|
|
650
1261
|
console.log();
|
|
651
1262
|
}
|
|
652
1263
|
async function inboundViewCommand(id, options) {
|
|
653
1264
|
if (!isAuthenticated()) {
|
|
654
|
-
console.log(
|
|
655
|
-
console.log(
|
|
1265
|
+
console.log(pc10.yellow("Not logged in."));
|
|
1266
|
+
console.log(pc10.dim("Run `btcemail login` to authenticate."));
|
|
656
1267
|
process.exit(1);
|
|
657
1268
|
}
|
|
1269
|
+
const spinner = ora8("Loading email details...").start();
|
|
658
1270
|
const result = await getPendingEmail(id);
|
|
659
1271
|
if (!result.ok) {
|
|
660
1272
|
if (result.error.code === "NOT_FOUND") {
|
|
661
|
-
|
|
1273
|
+
spinner.fail(`Pending email not found: ${id}`);
|
|
662
1274
|
} else {
|
|
663
|
-
|
|
1275
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
664
1276
|
}
|
|
665
1277
|
process.exit(1);
|
|
666
1278
|
}
|
|
1279
|
+
spinner.stop();
|
|
667
1280
|
const email = result.data;
|
|
668
1281
|
if (options.json) {
|
|
669
1282
|
console.log(JSON.stringify(email, null, 2));
|
|
670
1283
|
return;
|
|
671
1284
|
}
|
|
672
1285
|
console.log();
|
|
673
|
-
console.log(
|
|
1286
|
+
console.log(pc10.bold(email.subject || "(No subject)"));
|
|
674
1287
|
console.log();
|
|
675
|
-
console.log(`${
|
|
676
|
-
console.log(`${
|
|
677
|
-
console.log(`${
|
|
1288
|
+
console.log(`${pc10.dim("From:")} ${formatAddress2(email.from)}`);
|
|
1289
|
+
console.log(`${pc10.dim("Date:")} ${formatFullDate2(email.createdAt)}`);
|
|
1290
|
+
console.log(`${pc10.dim("ID:")} ${email.id}`);
|
|
678
1291
|
console.log();
|
|
679
|
-
console.log(
|
|
1292
|
+
console.log(pc10.yellow(`Payment Required: ${email.amountSats} sats`));
|
|
680
1293
|
console.log();
|
|
681
1294
|
if (email.paymentHash) {
|
|
682
|
-
console.log(
|
|
683
|
-
console.log(
|
|
1295
|
+
console.log(pc10.dim("To receive this email, pay the invoice and run:"));
|
|
1296
|
+
console.log(pc10.cyan(` btcemail inbound pay ${email.id} --payment-hash <preimage>`));
|
|
684
1297
|
} else {
|
|
685
|
-
console.log(
|
|
1298
|
+
console.log(pc10.dim("Payment information not available."));
|
|
686
1299
|
}
|
|
687
1300
|
console.log();
|
|
688
|
-
console.log(
|
|
1301
|
+
console.log(pc10.dim("-".repeat(60)));
|
|
689
1302
|
console.log();
|
|
690
|
-
console.log(
|
|
1303
|
+
console.log(pc10.dim("Preview (full content available after payment):"));
|
|
691
1304
|
console.log();
|
|
692
|
-
console.log(
|
|
1305
|
+
console.log(truncate3(email.bodyText || email.body, 200));
|
|
693
1306
|
console.log();
|
|
694
1307
|
}
|
|
695
1308
|
async function inboundPayCommand(id, options) {
|
|
696
1309
|
if (!isAuthenticated()) {
|
|
697
|
-
console.log(
|
|
698
|
-
console.log(
|
|
1310
|
+
console.log(pc10.yellow("Not logged in."));
|
|
1311
|
+
console.log(pc10.dim("Run `btcemail login` to authenticate."));
|
|
699
1312
|
process.exit(1);
|
|
700
1313
|
}
|
|
701
1314
|
if (!options.paymentHash) {
|
|
702
|
-
console.error(
|
|
1315
|
+
console.error(pc10.red("Payment hash is required. Use --payment-hash <preimage>"));
|
|
703
1316
|
process.exit(1);
|
|
704
1317
|
}
|
|
705
|
-
|
|
706
|
-
console.log(pc8.dim("Verifying payment..."));
|
|
1318
|
+
const spinner = ora8("Verifying payment...").start();
|
|
707
1319
|
const result = await payForEmail(id, options.paymentHash);
|
|
708
1320
|
if (!result.ok) {
|
|
709
1321
|
if (result.error.code === "PAYMENT_INVALID") {
|
|
710
|
-
|
|
711
|
-
console.log(
|
|
1322
|
+
spinner.fail("Payment verification failed.");
|
|
1323
|
+
console.log(pc10.dim("Make sure you paid the correct invoice and provided the preimage."));
|
|
712
1324
|
} else if (result.error.code === "NOT_FOUND") {
|
|
713
|
-
|
|
1325
|
+
spinner.fail(`Pending email not found or already delivered: ${id}`);
|
|
714
1326
|
} else {
|
|
715
|
-
|
|
1327
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
716
1328
|
}
|
|
717
1329
|
process.exit(1);
|
|
718
1330
|
}
|
|
1331
|
+
spinner.succeed("Payment verified! Email delivered.");
|
|
719
1332
|
console.log();
|
|
720
|
-
console.log(
|
|
721
|
-
console.log();
|
|
722
|
-
console.log(`${pc8.dim("Email ID:")} ${result.data.emailId}`);
|
|
723
|
-
console.log(`${pc8.dim("Status:")} ${result.data.status}`);
|
|
1333
|
+
console.log(`${pc10.dim("Email ID:")} ${result.data.emailId}`);
|
|
1334
|
+
console.log(`${pc10.dim("Status:")} ${result.data.status}`);
|
|
724
1335
|
console.log();
|
|
725
|
-
console.log(
|
|
1336
|
+
console.log(pc10.dim(`Read email: btcemail read ${result.data.emailId}`));
|
|
726
1337
|
console.log();
|
|
727
1338
|
}
|
|
728
|
-
function
|
|
1339
|
+
function truncate3(str, maxLength) {
|
|
729
1340
|
if (str.length <= maxLength) return str;
|
|
730
1341
|
return str.slice(0, maxLength - 1) + "\u2026";
|
|
731
1342
|
}
|
|
@@ -746,38 +1357,72 @@ function formatFullDate2(dateString) {
|
|
|
746
1357
|
minute: "2-digit"
|
|
747
1358
|
});
|
|
748
1359
|
}
|
|
1360
|
+
function formatDate4(dateString) {
|
|
1361
|
+
const date = new Date(dateString);
|
|
1362
|
+
const now = /* @__PURE__ */ new Date();
|
|
1363
|
+
const diffMs = now.getTime() - date.getTime();
|
|
1364
|
+
const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
|
|
1365
|
+
if (diffDays === 0) {
|
|
1366
|
+
return date.toLocaleTimeString("en-US", {
|
|
1367
|
+
hour: "numeric",
|
|
1368
|
+
minute: "2-digit"
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
if (diffDays < 7) {
|
|
1372
|
+
return date.toLocaleDateString("en-US", { weekday: "short" });
|
|
1373
|
+
}
|
|
1374
|
+
return date.toLocaleDateString("en-US", {
|
|
1375
|
+
month: "short",
|
|
1376
|
+
day: "numeric"
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
749
1379
|
|
|
750
1380
|
// src/index.ts
|
|
751
1381
|
var program = new Command();
|
|
752
|
-
program.name("btcemail").description("btc.email CLI - Spam-free email powered by Bitcoin Lightning").version("0.
|
|
1382
|
+
program.name("btcemail").description("btc.email CLI - Spam-free email powered by Bitcoin Lightning").version("0.3.0");
|
|
753
1383
|
program.command("login").description("Authenticate with btc.email (opens browser)").action(loginCommand);
|
|
754
1384
|
program.command("logout").description("Clear stored credentials").action(logoutCommand);
|
|
755
1385
|
program.command("whoami").description("Show current authenticated user").action(whoamiCommand);
|
|
756
|
-
program.command("list").description("List emails").option("-f, --folder <folder>", "Folder to list (inbox, sent, drafts)", "inbox").option("-l, --limit <number>", "Number of emails to show", "20").option("--json", "Output as JSON").action(async (options) => {
|
|
1386
|
+
program.command("list").alias("ls").description("List emails (numbered for quick access)").option("-f, --folder <folder>", "Folder to list (inbox, sent, drafts)", "inbox").option("-l, --limit <number>", "Number of emails to show", "20").option("-p, --page <number>", "Page number", "1").option("--json", "Output as JSON").action(async (options) => {
|
|
757
1387
|
await listCommand({
|
|
758
1388
|
folder: options.folder,
|
|
759
1389
|
limit: parseInt(options.limit, 10),
|
|
1390
|
+
page: parseInt(options.page, 10),
|
|
760
1391
|
json: options.json
|
|
761
1392
|
});
|
|
762
1393
|
});
|
|
763
|
-
program.command("read <id>").description("Read an email by ID").option("--json", "Output as JSON").action(async (id, options) => {
|
|
1394
|
+
program.command("read <id>").alias("r").description("Read an email by # (from list) or ID").option("--json", "Output as JSON").action(async (id, options) => {
|
|
764
1395
|
await readCommand(id, { json: options.json });
|
|
765
1396
|
});
|
|
766
|
-
program.command("
|
|
1397
|
+
program.command("search <query>").alias("s").description("Search emails by subject, body, or sender").option("-l, --limit <number>", "Number of results to show", "20").option("--json", "Output as JSON").action(async (query, options) => {
|
|
1398
|
+
await searchCommand(query, {
|
|
1399
|
+
limit: parseInt(options.limit, 10),
|
|
1400
|
+
json: options.json
|
|
1401
|
+
});
|
|
1402
|
+
});
|
|
1403
|
+
program.command("send").description("Send an email (with Lightning payment)").option("-t, --to <emails>", "Recipient email(s), comma-separated").option("-s, --subject <subject>", "Email subject").option("-b, --body <body>", "Email body").option("-f, --from <email>", "From email (@btc.email address)").option("--payment-hash <hash>", "Payment preimage for L402").option("-w, --wait", "Wait for payment confirmation").action(async (options) => {
|
|
767
1404
|
await sendCommand({
|
|
768
1405
|
to: options.to,
|
|
769
1406
|
subject: options.subject,
|
|
770
1407
|
body: options.body,
|
|
771
|
-
fromEmail: options.from
|
|
1408
|
+
fromEmail: options.from,
|
|
1409
|
+
paymentHash: options.paymentHash,
|
|
1410
|
+
wait: options.wait
|
|
772
1411
|
});
|
|
773
1412
|
});
|
|
774
|
-
var inbound = program.command("inbound").description("Manage
|
|
775
|
-
inbound.command("pending").description("List pending emails awaiting payment").option("-l, --limit <number>", "Number of emails to show", "20").option("--json", "Output as JSON").action(async (options) => {
|
|
1413
|
+
var inbound = program.command("inbound").description("Manage inbound emails (pending and delivered)");
|
|
1414
|
+
inbound.command("pending").alias("p").description("List pending emails awaiting payment").option("-l, --limit <number>", "Number of emails to show", "20").option("--json", "Output as JSON").action(async (options) => {
|
|
776
1415
|
await inboundPendingCommand({
|
|
777
1416
|
limit: parseInt(options.limit, 10),
|
|
778
1417
|
json: options.json
|
|
779
1418
|
});
|
|
780
1419
|
});
|
|
1420
|
+
inbound.command("delivered").alias("d").description("List delivered emails (paid by senders)").option("-l, --limit <number>", "Number of emails to show", "20").option("--json", "Output as JSON").action(async (options) => {
|
|
1421
|
+
await inboundDeliveredCommand({
|
|
1422
|
+
limit: parseInt(options.limit, 10),
|
|
1423
|
+
json: options.json
|
|
1424
|
+
});
|
|
1425
|
+
});
|
|
781
1426
|
inbound.command("view <id>").description("View pending email details and payment info").option("--json", "Output as JSON").action(async (id, options) => {
|
|
782
1427
|
await inboundViewCommand(id, { json: options.json });
|
|
783
1428
|
});
|
|
@@ -785,11 +1430,24 @@ inbound.command("pay <id>").description("Pay for a pending email").requiredOptio
|
|
|
785
1430
|
await inboundPayCommand(id, { paymentHash: options.paymentHash });
|
|
786
1431
|
});
|
|
787
1432
|
var credits = program.command("credits").description("Manage credits");
|
|
788
|
-
credits.command("balance").description("Show credit balance").action(creditsBalanceCommand);
|
|
1433
|
+
credits.command("balance").alias("bal").description("Show credit balance").action(creditsBalanceCommand);
|
|
1434
|
+
credits.command("history").description("Show transaction history").option("-l, --limit <number>", "Number of transactions to show", "20").option("-t, --type <type>", "Filter by type (topup, email_sent, email_received, refund, bonus)").option("--json", "Output as JSON").action(async (options) => {
|
|
1435
|
+
await creditsHistoryCommand({
|
|
1436
|
+
limit: parseInt(options.limit, 10),
|
|
1437
|
+
type: options.type,
|
|
1438
|
+
json: options.json
|
|
1439
|
+
});
|
|
1440
|
+
});
|
|
1441
|
+
credits.command("purchase").alias("buy").description("Purchase credits with Lightning").option("-o, --option <index>", "Purchase option index (0-3)").option("-w, --wait", "Wait for payment confirmation").option("--json", "Output as JSON").action(async (options) => {
|
|
1442
|
+
await creditsPurchaseCommand({
|
|
1443
|
+
option: options.option !== void 0 ? parseInt(options.option, 10) : void 0,
|
|
1444
|
+
json: options.json,
|
|
1445
|
+
wait: options.wait
|
|
1446
|
+
});
|
|
1447
|
+
});
|
|
789
1448
|
program.action(() => {
|
|
790
1449
|
console.log();
|
|
791
|
-
console.log(
|
|
792
|
-
console.log(pc9.dim("Spam-free email powered by Bitcoin Lightning"));
|
|
1450
|
+
console.log(pc11.bold("btc.email CLI") + pc11.dim(" - Spam-free email powered by Bitcoin Lightning"));
|
|
793
1451
|
console.log();
|
|
794
1452
|
program.outputHelp();
|
|
795
1453
|
});
|