@btcemail/cli 0.1.1 → 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 +737 -113
- 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,6 +200,7 @@ 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 = {}) {
|
|
@@ -324,6 +325,100 @@ async function getL402Invoice(options) {
|
|
|
324
325
|
};
|
|
325
326
|
}
|
|
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", {
|
|
343
|
+
method: "POST",
|
|
344
|
+
body: JSON.stringify({ optionIndex })
|
|
345
|
+
});
|
|
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
|
+
}
|
|
327
422
|
|
|
328
423
|
// src/commands/whoami.ts
|
|
329
424
|
async function whoamiCommand() {
|
|
@@ -337,14 +432,16 @@ async function whoamiCommand() {
|
|
|
337
432
|
console.log(pc3.yellow("Not logged in."));
|
|
338
433
|
process.exit(1);
|
|
339
434
|
}
|
|
435
|
+
const spinner = ora2("Fetching account info...").start();
|
|
340
436
|
const result = await getWhoami();
|
|
437
|
+
spinner.stop();
|
|
341
438
|
if (result.ok) {
|
|
342
439
|
console.log();
|
|
343
440
|
console.log(pc3.bold("Current User"));
|
|
344
441
|
console.log();
|
|
345
442
|
console.log(` ${pc3.dim("Email:")} ${result.data.email}`);
|
|
346
443
|
if (result.data.username) {
|
|
347
|
-
console.log(` ${pc3.dim("Username:")} ${result.data.username}`);
|
|
444
|
+
console.log(` ${pc3.dim("Username:")} ${result.data.username}@btc.email`);
|
|
348
445
|
}
|
|
349
446
|
console.log(` ${pc3.dim("User ID:")} ${result.data.id}`);
|
|
350
447
|
const expiryInfo = getTokenExpiryInfo();
|
|
@@ -359,7 +456,7 @@ async function whoamiCommand() {
|
|
|
359
456
|
}
|
|
360
457
|
} else {
|
|
361
458
|
console.log();
|
|
362
|
-
console.log(pc3.bold("Current User (cached)"));
|
|
459
|
+
console.log(pc3.bold("Current User") + pc3.dim(" (cached)"));
|
|
363
460
|
console.log();
|
|
364
461
|
console.log(` ${pc3.dim("Email:")} ${auth.email}`);
|
|
365
462
|
console.log(` ${pc3.dim("User ID:")} ${auth.userId}`);
|
|
@@ -372,29 +469,78 @@ async function whoamiCommand() {
|
|
|
372
469
|
|
|
373
470
|
// src/commands/list.ts
|
|
374
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
|
|
375
510
|
async function listCommand(options) {
|
|
376
511
|
if (!isAuthenticated()) {
|
|
377
512
|
console.log(pc4.yellow("Not logged in."));
|
|
378
513
|
console.log(pc4.dim("Run `btcemail login` to authenticate."));
|
|
379
514
|
process.exit(1);
|
|
380
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();
|
|
381
521
|
const result = await getEmails({
|
|
382
|
-
folder
|
|
383
|
-
limit
|
|
522
|
+
folder,
|
|
523
|
+
limit,
|
|
524
|
+
offset
|
|
384
525
|
});
|
|
385
526
|
if (!result.ok) {
|
|
386
|
-
|
|
527
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
387
528
|
process.exit(1);
|
|
388
529
|
}
|
|
389
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);
|
|
390
538
|
if (options.json) {
|
|
391
539
|
console.log(JSON.stringify({ emails, pagination }, null, 2));
|
|
392
540
|
return;
|
|
393
541
|
}
|
|
394
542
|
console.log();
|
|
395
|
-
console.log(
|
|
396
|
-
pc4.bold(`${options.folder || "Inbox"} (${pagination.total} emails)`)
|
|
397
|
-
);
|
|
543
|
+
console.log(pc4.bold(`${capitalize(folder)} (${pagination.total} emails)`));
|
|
398
544
|
console.log();
|
|
399
545
|
if (emails.length === 0) {
|
|
400
546
|
console.log(pc4.dim(" No emails found."));
|
|
@@ -402,26 +548,34 @@ async function listCommand(options) {
|
|
|
402
548
|
return;
|
|
403
549
|
}
|
|
404
550
|
console.log(
|
|
405
|
-
` ${pc4.dim("
|
|
551
|
+
` ${pc4.dim("#".padEnd(4))} ${pc4.dim("From".padEnd(25))} ${pc4.dim("Subject".padEnd(45))} ${pc4.dim("Date")}`
|
|
406
552
|
);
|
|
407
|
-
console.log(pc4.dim(" " + "-".repeat(
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
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") : " ";
|
|
411
557
|
const from = truncate(email.from.name || email.from.email, 23).padEnd(25);
|
|
412
|
-
const subject = truncate(email.subject,
|
|
558
|
+
const subject = truncate(email.subject, 43).padEnd(45);
|
|
413
559
|
const date = formatDate(email.date);
|
|
414
|
-
console.log(`${unreadMarker} ${
|
|
415
|
-
}
|
|
560
|
+
console.log(`${unreadMarker} ${num}${from} ${subject} ${pc4.dim(date)}`);
|
|
561
|
+
});
|
|
416
562
|
console.log();
|
|
417
|
-
|
|
563
|
+
const totalPages = Math.ceil(pagination.total / limit);
|
|
564
|
+
const currentPage = page;
|
|
565
|
+
if (totalPages > 1) {
|
|
418
566
|
console.log(
|
|
419
|
-
pc4.dim(
|
|
420
|
-
` Showing ${emails.length} of ${pagination.total}. Use --limit to see more.`
|
|
421
|
-
)
|
|
567
|
+
pc4.dim(` Page ${currentPage} of ${totalPages} (${emails.length} of ${pagination.total})`)
|
|
422
568
|
);
|
|
569
|
+
if (currentPage < totalPages) {
|
|
570
|
+
console.log(pc4.dim(` Next page: btcemail list --page ${currentPage + 1}`));
|
|
571
|
+
}
|
|
423
572
|
console.log();
|
|
424
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);
|
|
425
579
|
}
|
|
426
580
|
function truncate(str, maxLength) {
|
|
427
581
|
if (str.length <= maxLength) return str;
|
|
@@ -449,21 +603,42 @@ function formatDate(dateString) {
|
|
|
449
603
|
|
|
450
604
|
// src/commands/read.ts
|
|
451
605
|
import pc5 from "picocolors";
|
|
452
|
-
|
|
606
|
+
import ora4 from "ora";
|
|
607
|
+
async function readCommand(idOrNumber, options) {
|
|
453
608
|
if (!isAuthenticated()) {
|
|
454
609
|
console.log(pc5.yellow("Not logged in."));
|
|
455
610
|
console.log(pc5.dim("Run `btcemail login` to authenticate."));
|
|
456
611
|
process.exit(1);
|
|
457
612
|
}
|
|
458
|
-
|
|
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);
|
|
459
633
|
if (!result.ok) {
|
|
460
634
|
if (result.error.code === "NOT_FOUND") {
|
|
461
|
-
|
|
635
|
+
spinner.fail(`Email not found: ${emailId}`);
|
|
462
636
|
} else {
|
|
463
|
-
|
|
637
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
464
638
|
}
|
|
465
639
|
process.exit(1);
|
|
466
640
|
}
|
|
641
|
+
spinner.stop();
|
|
467
642
|
const email = result.data;
|
|
468
643
|
if (options.json) {
|
|
469
644
|
console.log(JSON.stringify(email, null, 2));
|
|
@@ -512,46 +687,349 @@ function formatFullDate(dateString) {
|
|
|
512
687
|
});
|
|
513
688
|
}
|
|
514
689
|
|
|
515
|
-
// src/commands/
|
|
690
|
+
// src/commands/search.ts
|
|
516
691
|
import pc6 from "picocolors";
|
|
517
|
-
|
|
692
|
+
import ora5 from "ora";
|
|
693
|
+
async function searchCommand(query, options) {
|
|
518
694
|
if (!isAuthenticated()) {
|
|
519
695
|
console.log(pc6.yellow("Not logged in."));
|
|
520
696
|
console.log(pc6.dim("Run `btcemail login` to authenticate."));
|
|
521
697
|
process.exit(1);
|
|
522
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();
|
|
523
828
|
const result = await getCreditsBalance();
|
|
524
829
|
if (!result.ok) {
|
|
525
|
-
|
|
830
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
526
831
|
process.exit(1);
|
|
527
832
|
}
|
|
528
|
-
|
|
833
|
+
spinner.stop();
|
|
834
|
+
const { balanceSats, lifetimePurchasedSats, lifetimeSpentSats } = result.data;
|
|
529
835
|
console.log();
|
|
530
|
-
console.log(
|
|
836
|
+
console.log(pc8.bold("Credit Balance"));
|
|
531
837
|
console.log();
|
|
532
|
-
console.log(` ${
|
|
838
|
+
console.log(` ${pc8.green(formatSats(balanceSats))} sats`);
|
|
533
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
|
+
}
|
|
534
845
|
if (balanceSats === 0) {
|
|
535
|
-
console.log(
|
|
536
|
-
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."));
|
|
537
988
|
console.log();
|
|
538
989
|
}
|
|
539
990
|
}
|
|
540
991
|
function formatSats(sats) {
|
|
541
992
|
return sats.toLocaleString();
|
|
542
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
|
+
}
|
|
543
1017
|
|
|
544
1018
|
// src/commands/send.ts
|
|
545
|
-
import
|
|
1019
|
+
import pc9 from "picocolors";
|
|
1020
|
+
import ora7 from "ora";
|
|
1021
|
+
import qrcode2 from "qrcode-terminal";
|
|
546
1022
|
async function sendCommand(options) {
|
|
547
1023
|
if (!isAuthenticated()) {
|
|
548
|
-
console.log(
|
|
549
|
-
console.log(
|
|
1024
|
+
console.log(pc9.yellow("Not logged in."));
|
|
1025
|
+
console.log(pc9.dim("Run `btcemail login` to authenticate."));
|
|
550
1026
|
process.exit(1);
|
|
551
1027
|
}
|
|
552
1028
|
let fromEmail = options.fromEmail;
|
|
553
1029
|
if (!fromEmail) {
|
|
1030
|
+
const spinner2 = ora7("Fetching account info...").start();
|
|
554
1031
|
const whoamiResult = await getWhoami();
|
|
1032
|
+
spinner2.stop();
|
|
555
1033
|
if (whoamiResult.ok && whoamiResult.data.username) {
|
|
556
1034
|
fromEmail = `${whoamiResult.data.username}@btc.email`;
|
|
557
1035
|
} else {
|
|
@@ -562,52 +1040,100 @@ async function sendCommand(options) {
|
|
|
562
1040
|
}
|
|
563
1041
|
}
|
|
564
1042
|
if (!fromEmail || !fromEmail.endsWith("@btc.email")) {
|
|
565
|
-
console.error(
|
|
566
|
-
console.log(
|
|
1043
|
+
console.error(pc9.red("No @btc.email address found."));
|
|
1044
|
+
console.log(pc9.dim("Register a username at https://btc.email"));
|
|
567
1045
|
process.exit(1);
|
|
568
1046
|
}
|
|
569
1047
|
if (!options.to) {
|
|
570
|
-
console.error(
|
|
1048
|
+
console.error(pc9.red("Recipient is required. Use --to <email>"));
|
|
571
1049
|
process.exit(1);
|
|
572
1050
|
}
|
|
573
1051
|
if (!options.subject) {
|
|
574
|
-
console.error(
|
|
1052
|
+
console.error(pc9.red("Subject is required. Use --subject <text>"));
|
|
575
1053
|
process.exit(1);
|
|
576
1054
|
}
|
|
577
1055
|
if (!options.body) {
|
|
578
|
-
console.error(
|
|
1056
|
+
console.error(pc9.red("Body is required. Use --body <text>"));
|
|
579
1057
|
process.exit(1);
|
|
580
1058
|
}
|
|
581
1059
|
const toEmails = options.to.split(",").map((e) => e.trim());
|
|
582
1060
|
console.log();
|
|
583
|
-
console.log(
|
|
1061
|
+
console.log(pc9.bold("Sending Email"));
|
|
584
1062
|
console.log();
|
|
585
|
-
console.log(`${
|
|
586
|
-
console.log(`${
|
|
587
|
-
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}`);
|
|
588
1066
|
console.log();
|
|
589
1067
|
if (!options.paymentHash) {
|
|
1068
|
+
const spinner2 = ora7("Getting invoice...").start();
|
|
590
1069
|
const invoiceResult = await getL402Invoice({
|
|
591
1070
|
fromEmail,
|
|
592
1071
|
toEmails
|
|
593
1072
|
});
|
|
1073
|
+
spinner2.stop();
|
|
594
1074
|
if (invoiceResult.ok && invoiceResult.data.amountSats > 0) {
|
|
595
1075
|
const invoice = invoiceResult.data;
|
|
596
|
-
console.log(
|
|
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
|
+
});
|
|
597
1084
|
console.log();
|
|
598
|
-
console.log(
|
|
599
|
-
console.log(
|
|
1085
|
+
console.log(pc9.dim("Lightning Invoice:"));
|
|
1086
|
+
console.log(pc9.cyan(invoice.invoice));
|
|
600
1087
|
console.log();
|
|
601
|
-
|
|
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:"));
|
|
602
1125
|
console.log(
|
|
603
|
-
|
|
1126
|
+
pc9.cyan(
|
|
604
1127
|
` btcemail send --to "${options.to}" --subject "${options.subject}" --body "${options.body}" --payment-hash ${invoice.paymentHash}`
|
|
605
1128
|
)
|
|
606
1129
|
);
|
|
607
1130
|
console.log();
|
|
1131
|
+
console.log(pc9.dim("Or use --wait to automatically wait for payment."));
|
|
1132
|
+
console.log();
|
|
608
1133
|
return;
|
|
609
1134
|
}
|
|
610
1135
|
}
|
|
1136
|
+
const spinner = ora7("Sending email...").start();
|
|
611
1137
|
const result = await sendEmail({
|
|
612
1138
|
to: toEmails,
|
|
613
1139
|
subject: options.subject,
|
|
@@ -617,148 +1143,200 @@ async function sendCommand(options) {
|
|
|
617
1143
|
});
|
|
618
1144
|
if (!result.ok) {
|
|
619
1145
|
if (result.error.code === "PAYMENT_REQUIRED") {
|
|
620
|
-
|
|
621
|
-
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"));
|
|
622
1148
|
} else {
|
|
623
|
-
|
|
1149
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
624
1150
|
}
|
|
625
1151
|
process.exit(1);
|
|
626
1152
|
}
|
|
627
|
-
|
|
1153
|
+
spinner.succeed("Email sent!");
|
|
628
1154
|
console.log();
|
|
629
|
-
console.log(`${
|
|
1155
|
+
console.log(`${pc9.dim("Email ID:")} ${result.data.emailId}`);
|
|
630
1156
|
if (result.data.messageId) {
|
|
631
|
-
console.log(`${
|
|
1157
|
+
console.log(`${pc9.dim("Message ID:")} ${result.data.messageId}`);
|
|
632
1158
|
}
|
|
633
1159
|
console.log();
|
|
634
1160
|
}
|
|
635
1161
|
|
|
636
1162
|
// src/commands/inbound.ts
|
|
637
|
-
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
|
+
}
|
|
638
1214
|
async function inboundPendingCommand(options) {
|
|
639
1215
|
if (!isAuthenticated()) {
|
|
640
|
-
console.log(
|
|
641
|
-
console.log(
|
|
1216
|
+
console.log(pc10.yellow("Not logged in."));
|
|
1217
|
+
console.log(pc10.dim("Run `btcemail login` to authenticate."));
|
|
642
1218
|
process.exit(1);
|
|
643
1219
|
}
|
|
1220
|
+
const spinner = ora8("Loading pending emails...").start();
|
|
644
1221
|
const result = await getPendingEmails({
|
|
645
1222
|
limit: options.limit || 20
|
|
646
1223
|
});
|
|
647
1224
|
if (!result.ok) {
|
|
648
|
-
|
|
1225
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
649
1226
|
process.exit(1);
|
|
650
1227
|
}
|
|
651
1228
|
const { data: emails, pagination } = result.data;
|
|
1229
|
+
spinner.stop();
|
|
652
1230
|
if (options.json) {
|
|
653
1231
|
console.log(JSON.stringify({ emails, pagination }, null, 2));
|
|
654
1232
|
return;
|
|
655
1233
|
}
|
|
656
1234
|
console.log();
|
|
657
|
-
console.log(
|
|
1235
|
+
console.log(pc10.bold(`Pending (${pagination.total} awaiting payment)`));
|
|
658
1236
|
console.log();
|
|
659
1237
|
if (emails.length === 0) {
|
|
660
|
-
console.log(
|
|
1238
|
+
console.log(pc10.dim(" No pending emails."));
|
|
661
1239
|
console.log();
|
|
662
1240
|
return;
|
|
663
1241
|
}
|
|
664
1242
|
console.log(
|
|
665
|
-
` ${
|
|
1243
|
+
` ${pc10.dim("#".padEnd(4))} ${pc10.dim("From".padEnd(28))} ${pc10.dim("Subject".padEnd(35))} ${pc10.dim("Sats")}`
|
|
666
1244
|
);
|
|
667
|
-
console.log(
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
const from =
|
|
671
|
-
const subject =
|
|
672
|
-
const sats =
|
|
673
|
-
console.log(` ${
|
|
674
|
-
}
|
|
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
|
+
});
|
|
675
1253
|
console.log();
|
|
676
1254
|
if (pagination.hasMore) {
|
|
677
1255
|
console.log(
|
|
678
|
-
|
|
1256
|
+
pc10.dim(` Showing ${emails.length} of ${pagination.total}. Use --limit to see more.`)
|
|
679
1257
|
);
|
|
680
1258
|
console.log();
|
|
681
1259
|
}
|
|
682
|
-
console.log(
|
|
1260
|
+
console.log(pc10.dim(" View details: btcemail inbound view <id>"));
|
|
683
1261
|
console.log();
|
|
684
1262
|
}
|
|
685
1263
|
async function inboundViewCommand(id, options) {
|
|
686
1264
|
if (!isAuthenticated()) {
|
|
687
|
-
console.log(
|
|
688
|
-
console.log(
|
|
1265
|
+
console.log(pc10.yellow("Not logged in."));
|
|
1266
|
+
console.log(pc10.dim("Run `btcemail login` to authenticate."));
|
|
689
1267
|
process.exit(1);
|
|
690
1268
|
}
|
|
1269
|
+
const spinner = ora8("Loading email details...").start();
|
|
691
1270
|
const result = await getPendingEmail(id);
|
|
692
1271
|
if (!result.ok) {
|
|
693
1272
|
if (result.error.code === "NOT_FOUND") {
|
|
694
|
-
|
|
1273
|
+
spinner.fail(`Pending email not found: ${id}`);
|
|
695
1274
|
} else {
|
|
696
|
-
|
|
1275
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
697
1276
|
}
|
|
698
1277
|
process.exit(1);
|
|
699
1278
|
}
|
|
1279
|
+
spinner.stop();
|
|
700
1280
|
const email = result.data;
|
|
701
1281
|
if (options.json) {
|
|
702
1282
|
console.log(JSON.stringify(email, null, 2));
|
|
703
1283
|
return;
|
|
704
1284
|
}
|
|
705
1285
|
console.log();
|
|
706
|
-
console.log(
|
|
1286
|
+
console.log(pc10.bold(email.subject || "(No subject)"));
|
|
707
1287
|
console.log();
|
|
708
|
-
console.log(`${
|
|
709
|
-
console.log(`${
|
|
710
|
-
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}`);
|
|
711
1291
|
console.log();
|
|
712
|
-
console.log(
|
|
1292
|
+
console.log(pc10.yellow(`Payment Required: ${email.amountSats} sats`));
|
|
713
1293
|
console.log();
|
|
714
1294
|
if (email.paymentHash) {
|
|
715
|
-
console.log(
|
|
716
|
-
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>`));
|
|
717
1297
|
} else {
|
|
718
|
-
console.log(
|
|
1298
|
+
console.log(pc10.dim("Payment information not available."));
|
|
719
1299
|
}
|
|
720
1300
|
console.log();
|
|
721
|
-
console.log(
|
|
1301
|
+
console.log(pc10.dim("-".repeat(60)));
|
|
722
1302
|
console.log();
|
|
723
|
-
console.log(
|
|
1303
|
+
console.log(pc10.dim("Preview (full content available after payment):"));
|
|
724
1304
|
console.log();
|
|
725
|
-
console.log(
|
|
1305
|
+
console.log(truncate3(email.bodyText || email.body, 200));
|
|
726
1306
|
console.log();
|
|
727
1307
|
}
|
|
728
1308
|
async function inboundPayCommand(id, options) {
|
|
729
1309
|
if (!isAuthenticated()) {
|
|
730
|
-
console.log(
|
|
731
|
-
console.log(
|
|
1310
|
+
console.log(pc10.yellow("Not logged in."));
|
|
1311
|
+
console.log(pc10.dim("Run `btcemail login` to authenticate."));
|
|
732
1312
|
process.exit(1);
|
|
733
1313
|
}
|
|
734
1314
|
if (!options.paymentHash) {
|
|
735
|
-
console.error(
|
|
1315
|
+
console.error(pc10.red("Payment hash is required. Use --payment-hash <preimage>"));
|
|
736
1316
|
process.exit(1);
|
|
737
1317
|
}
|
|
738
|
-
|
|
739
|
-
console.log(pc8.dim("Verifying payment..."));
|
|
1318
|
+
const spinner = ora8("Verifying payment...").start();
|
|
740
1319
|
const result = await payForEmail(id, options.paymentHash);
|
|
741
1320
|
if (!result.ok) {
|
|
742
1321
|
if (result.error.code === "PAYMENT_INVALID") {
|
|
743
|
-
|
|
744
|
-
console.log(
|
|
1322
|
+
spinner.fail("Payment verification failed.");
|
|
1323
|
+
console.log(pc10.dim("Make sure you paid the correct invoice and provided the preimage."));
|
|
745
1324
|
} else if (result.error.code === "NOT_FOUND") {
|
|
746
|
-
|
|
1325
|
+
spinner.fail(`Pending email not found or already delivered: ${id}`);
|
|
747
1326
|
} else {
|
|
748
|
-
|
|
1327
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
749
1328
|
}
|
|
750
1329
|
process.exit(1);
|
|
751
1330
|
}
|
|
1331
|
+
spinner.succeed("Payment verified! Email delivered.");
|
|
752
1332
|
console.log();
|
|
753
|
-
console.log(
|
|
1333
|
+
console.log(`${pc10.dim("Email ID:")} ${result.data.emailId}`);
|
|
1334
|
+
console.log(`${pc10.dim("Status:")} ${result.data.status}`);
|
|
754
1335
|
console.log();
|
|
755
|
-
console.log(
|
|
756
|
-
console.log(`${pc8.dim("Status:")} ${result.data.status}`);
|
|
757
|
-
console.log();
|
|
758
|
-
console.log(pc8.dim("Run `btcemail read " + result.data.emailId + "` to read the email."));
|
|
1336
|
+
console.log(pc10.dim(`Read email: btcemail read ${result.data.emailId}`));
|
|
759
1337
|
console.log();
|
|
760
1338
|
}
|
|
761
|
-
function
|
|
1339
|
+
function truncate3(str, maxLength) {
|
|
762
1340
|
if (str.length <= maxLength) return str;
|
|
763
1341
|
return str.slice(0, maxLength - 1) + "\u2026";
|
|
764
1342
|
}
|
|
@@ -779,39 +1357,72 @@ function formatFullDate2(dateString) {
|
|
|
779
1357
|
minute: "2-digit"
|
|
780
1358
|
});
|
|
781
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
|
+
}
|
|
782
1379
|
|
|
783
1380
|
// src/index.ts
|
|
784
1381
|
var program = new Command();
|
|
785
|
-
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");
|
|
786
1383
|
program.command("login").description("Authenticate with btc.email (opens browser)").action(loginCommand);
|
|
787
1384
|
program.command("logout").description("Clear stored credentials").action(logoutCommand);
|
|
788
1385
|
program.command("whoami").description("Show current authenticated user").action(whoamiCommand);
|
|
789
|
-
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) => {
|
|
790
1387
|
await listCommand({
|
|
791
1388
|
folder: options.folder,
|
|
792
1389
|
limit: parseInt(options.limit, 10),
|
|
1390
|
+
page: parseInt(options.page, 10),
|
|
793
1391
|
json: options.json
|
|
794
1392
|
});
|
|
795
1393
|
});
|
|
796
|
-
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) => {
|
|
797
1395
|
await readCommand(id, { json: options.json });
|
|
798
1396
|
});
|
|
799
|
-
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) => {
|
|
800
1404
|
await sendCommand({
|
|
801
1405
|
to: options.to,
|
|
802
1406
|
subject: options.subject,
|
|
803
1407
|
body: options.body,
|
|
804
1408
|
fromEmail: options.from,
|
|
805
|
-
paymentHash: options.paymentHash
|
|
1409
|
+
paymentHash: options.paymentHash,
|
|
1410
|
+
wait: options.wait
|
|
806
1411
|
});
|
|
807
1412
|
});
|
|
808
|
-
var inbound = program.command("inbound").description("Manage
|
|
809
|
-
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) => {
|
|
810
1415
|
await inboundPendingCommand({
|
|
811
1416
|
limit: parseInt(options.limit, 10),
|
|
812
1417
|
json: options.json
|
|
813
1418
|
});
|
|
814
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
|
+
});
|
|
815
1426
|
inbound.command("view <id>").description("View pending email details and payment info").option("--json", "Output as JSON").action(async (id, options) => {
|
|
816
1427
|
await inboundViewCommand(id, { json: options.json });
|
|
817
1428
|
});
|
|
@@ -819,11 +1430,24 @@ inbound.command("pay <id>").description("Pay for a pending email").requiredOptio
|
|
|
819
1430
|
await inboundPayCommand(id, { paymentHash: options.paymentHash });
|
|
820
1431
|
});
|
|
821
1432
|
var credits = program.command("credits").description("Manage credits");
|
|
822
|
-
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
|
+
});
|
|
823
1448
|
program.action(() => {
|
|
824
1449
|
console.log();
|
|
825
|
-
console.log(
|
|
826
|
-
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"));
|
|
827
1451
|
console.log();
|
|
828
1452
|
program.outputHelp();
|
|
829
1453
|
});
|