@btcemail/cli 0.1.1 → 0.4.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 +186 -41
- package/dist/index.js +1519 -390
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -2,12 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import pc16 from "picocolors";
|
|
6
6
|
|
|
7
|
-
// src/commands/
|
|
8
|
-
import { randomUUID } from "crypto";
|
|
9
|
-
import open from "open";
|
|
10
|
-
import ora from "ora";
|
|
7
|
+
// src/commands/blocklist.ts
|
|
11
8
|
import pc from "picocolors";
|
|
12
9
|
|
|
13
10
|
// src/config.ts
|
|
@@ -104,103 +101,6 @@ function getToken() {
|
|
|
104
101
|
return auth.token;
|
|
105
102
|
}
|
|
106
103
|
|
|
107
|
-
// src/commands/login.ts
|
|
108
|
-
var POLL_INTERVAL_MS = 2e3;
|
|
109
|
-
var POLL_TIMEOUT_MS = 3e5;
|
|
110
|
-
async function loginCommand() {
|
|
111
|
-
const sessionId = randomUUID();
|
|
112
|
-
const baseUrl = getApiBaseUrl().replace("/api/v1", "");
|
|
113
|
-
const authUrl = `${baseUrl}/auth/cli?session_id=${sessionId}`;
|
|
114
|
-
const pollUrl = `${baseUrl}/api/auth/cli-session?session_id=${sessionId}`;
|
|
115
|
-
console.log();
|
|
116
|
-
console.log(pc.bold("btc.email CLI Login"));
|
|
117
|
-
console.log();
|
|
118
|
-
try {
|
|
119
|
-
const createResponse = await fetch(
|
|
120
|
-
`${baseUrl}/api/auth/cli-session`,
|
|
121
|
-
{
|
|
122
|
-
method: "POST",
|
|
123
|
-
headers: { "Content-Type": "application/json" },
|
|
124
|
-
body: JSON.stringify({ session_id: sessionId })
|
|
125
|
-
}
|
|
126
|
-
);
|
|
127
|
-
if (!createResponse.ok) {
|
|
128
|
-
console.error(pc.red("Failed to initialize login session"));
|
|
129
|
-
process.exit(1);
|
|
130
|
-
}
|
|
131
|
-
} catch (error) {
|
|
132
|
-
console.error(pc.red("Failed to connect to btc.email server"));
|
|
133
|
-
console.error(
|
|
134
|
-
pc.dim(error instanceof Error ? error.message : "Unknown error")
|
|
135
|
-
);
|
|
136
|
-
process.exit(1);
|
|
137
|
-
}
|
|
138
|
-
console.log(pc.dim("Opening browser for authentication..."));
|
|
139
|
-
console.log();
|
|
140
|
-
console.log(pc.dim("If the browser doesn't open, visit:"));
|
|
141
|
-
console.log(pc.cyan(authUrl));
|
|
142
|
-
console.log();
|
|
143
|
-
try {
|
|
144
|
-
await open(authUrl);
|
|
145
|
-
} catch {
|
|
146
|
-
}
|
|
147
|
-
const spinner = ora("Waiting for authentication...").start();
|
|
148
|
-
const startTime = Date.now();
|
|
149
|
-
while (Date.now() - startTime < POLL_TIMEOUT_MS) {
|
|
150
|
-
try {
|
|
151
|
-
const response = await fetch(pollUrl);
|
|
152
|
-
if (response.status === 202) {
|
|
153
|
-
await sleep(POLL_INTERVAL_MS);
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
if (response.status === 410) {
|
|
157
|
-
spinner.fail("Session expired. Please try again.");
|
|
158
|
-
process.exit(1);
|
|
159
|
-
}
|
|
160
|
-
if (response.ok) {
|
|
161
|
-
const data = await response.json();
|
|
162
|
-
if (data.status === "complete") {
|
|
163
|
-
setAuth({
|
|
164
|
-
token: data.data.token,
|
|
165
|
-
expiresAt: data.data.expiresAt,
|
|
166
|
-
userId: data.data.userId,
|
|
167
|
-
email: data.data.email
|
|
168
|
-
});
|
|
169
|
-
spinner.succeed(pc.green("Successfully logged in!"));
|
|
170
|
-
console.log();
|
|
171
|
-
console.log(` ${pc.dim("Email:")} ${data.data.email}`);
|
|
172
|
-
console.log();
|
|
173
|
-
console.log(pc.dim("You can now use btcemail commands."));
|
|
174
|
-
console.log(pc.dim("Run `btcemail --help` to see available commands."));
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
await sleep(POLL_INTERVAL_MS);
|
|
179
|
-
} catch (error) {
|
|
180
|
-
await sleep(POLL_INTERVAL_MS);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
spinner.fail("Authentication timed out. Please try again.");
|
|
184
|
-
process.exit(1);
|
|
185
|
-
}
|
|
186
|
-
function sleep(ms) {
|
|
187
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// src/commands/logout.ts
|
|
191
|
-
import pc2 from "picocolors";
|
|
192
|
-
async function logoutCommand() {
|
|
193
|
-
if (!isAuthenticated()) {
|
|
194
|
-
console.log(pc2.yellow("You are not logged in."));
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
clearAuth();
|
|
198
|
-
console.log(pc2.green("Successfully logged out."));
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// src/commands/whoami.ts
|
|
202
|
-
import pc3 from "picocolors";
|
|
203
|
-
|
|
204
104
|
// src/api.ts
|
|
205
105
|
async function apiRequest(endpoint, options = {}) {
|
|
206
106
|
const token = getToken();
|
|
@@ -258,9 +158,7 @@ async function getCreditsBalance() {
|
|
|
258
158
|
return apiRequest("/credits/balance");
|
|
259
159
|
}
|
|
260
160
|
async function getWhoami() {
|
|
261
|
-
return apiRequest(
|
|
262
|
-
"/auth/whoami"
|
|
263
|
-
);
|
|
161
|
+
return apiRequest("/auth/whoami");
|
|
264
162
|
}
|
|
265
163
|
async function getPendingEmails(options = {}) {
|
|
266
164
|
const params = new URLSearchParams();
|
|
@@ -324,175 +222,654 @@ async function getL402Invoice(options) {
|
|
|
324
222
|
};
|
|
325
223
|
}
|
|
326
224
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
225
|
+
async function getDeliveredEmails(options = {}) {
|
|
226
|
+
const params = new URLSearchParams();
|
|
227
|
+
if (options.limit) params.set("limit", String(options.limit));
|
|
228
|
+
if (options.offset) params.set("offset", String(options.offset));
|
|
229
|
+
return apiRequest(`/inbound/delivered?${params}`);
|
|
230
|
+
}
|
|
231
|
+
async function getCreditTransactions(options = {}) {
|
|
232
|
+
const params = new URLSearchParams();
|
|
233
|
+
if (options.limit) params.set("limit", String(options.limit));
|
|
234
|
+
if (options.offset) params.set("offset", String(options.offset));
|
|
235
|
+
if (options.type) params.set("type", options.type);
|
|
236
|
+
return apiRequest(`/credits/transactions?${params}`);
|
|
237
|
+
}
|
|
238
|
+
async function purchaseCredits(optionIndex) {
|
|
239
|
+
return apiRequest("/credits/purchase", {
|
|
240
|
+
method: "POST",
|
|
241
|
+
body: JSON.stringify({ optionIndex })
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
async function checkPaymentStatus(paymentHash) {
|
|
245
|
+
const v1BaseUrl = getApiBaseUrl();
|
|
246
|
+
const rootApiUrl = v1BaseUrl.replace("/api/v1", "/api");
|
|
247
|
+
const token = getToken();
|
|
248
|
+
const headers = {
|
|
249
|
+
"Content-Type": "application/json"
|
|
250
|
+
};
|
|
251
|
+
if (token) {
|
|
252
|
+
headers.Authorization = `Bearer ${token}`;
|
|
339
253
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
console.log(` ${pc3.dim("Session:")} ${expiryColor(`expires in ${expiryInfo.expiresIn}`)}`);
|
|
254
|
+
try {
|
|
255
|
+
const response = await fetch(`${rootApiUrl}/lightning/webhook?payment_hash=${paymentHash}`, {
|
|
256
|
+
headers
|
|
257
|
+
});
|
|
258
|
+
const json = await response.json();
|
|
259
|
+
if (!response.ok || json.success === false) {
|
|
260
|
+
return {
|
|
261
|
+
ok: false,
|
|
262
|
+
error: json.error || {
|
|
263
|
+
code: "UNKNOWN_ERROR",
|
|
264
|
+
message: `Request failed with status ${response.status}`
|
|
265
|
+
}
|
|
266
|
+
};
|
|
354
267
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
268
|
+
return {
|
|
269
|
+
ok: true,
|
|
270
|
+
data: {
|
|
271
|
+
paid: json.paid || false,
|
|
272
|
+
delivered: json.delivered,
|
|
273
|
+
status: json.status || json.state,
|
|
274
|
+
pendingEmailId: json.pendingEmailId
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
} catch (error) {
|
|
278
|
+
return {
|
|
279
|
+
ok: false,
|
|
280
|
+
error: {
|
|
281
|
+
code: "NETWORK_ERROR",
|
|
282
|
+
message: error instanceof Error ? error.message : "Network request failed"
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async function getSettings(username) {
|
|
288
|
+
const params = username ? `?username=${username}` : "";
|
|
289
|
+
return apiRequest(`/settings${params}`);
|
|
290
|
+
}
|
|
291
|
+
async function updateSettings(settings2, username) {
|
|
292
|
+
const params = username ? `?username=${username}` : "";
|
|
293
|
+
return apiRequest(
|
|
294
|
+
`/settings${params}`,
|
|
295
|
+
{
|
|
296
|
+
method: "POST",
|
|
297
|
+
body: JSON.stringify(settings2)
|
|
359
298
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
async function getWhitelist(username) {
|
|
302
|
+
const params = username ? `?username=${username}` : "";
|
|
303
|
+
return apiRequest(`/whitelist${params}`);
|
|
304
|
+
}
|
|
305
|
+
async function addToWhitelist(entry, username) {
|
|
306
|
+
const params = username ? `?username=${username}` : "";
|
|
307
|
+
return apiRequest(`/whitelist${params}`, {
|
|
308
|
+
method: "POST",
|
|
309
|
+
body: JSON.stringify(entry)
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
async function removeFromWhitelist(id) {
|
|
313
|
+
return apiRequest(`/whitelist/${id}`, {
|
|
314
|
+
method: "DELETE"
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
async function getBlocklist(username) {
|
|
318
|
+
const params = username ? `?username=${username}` : "";
|
|
319
|
+
return apiRequest(`/blocklist${params}`);
|
|
320
|
+
}
|
|
321
|
+
async function addToBlocklist(entry, username) {
|
|
322
|
+
const params = username ? `?username=${username}` : "";
|
|
323
|
+
return apiRequest(`/blocklist${params}`, {
|
|
324
|
+
method: "POST",
|
|
325
|
+
body: JSON.stringify(entry)
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
async function removeFromBlocklist(id) {
|
|
329
|
+
return apiRequest(`/blocklist/${id}`, {
|
|
330
|
+
method: "DELETE"
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
async function getNetwork() {
|
|
334
|
+
return apiRequest("/network");
|
|
335
|
+
}
|
|
336
|
+
async function setNetwork(network2) {
|
|
337
|
+
return apiRequest("/network", {
|
|
338
|
+
method: "POST",
|
|
339
|
+
body: JSON.stringify({ network: network2 })
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
async function getStats() {
|
|
343
|
+
return apiRequest("/stats");
|
|
344
|
+
}
|
|
345
|
+
function getCreditPurchaseOptions() {
|
|
346
|
+
return [
|
|
347
|
+
{
|
|
348
|
+
amountSats: 1e3,
|
|
349
|
+
priceSats: 1e3,
|
|
350
|
+
bonusSats: 0,
|
|
351
|
+
label: "1,000 sats",
|
|
352
|
+
description: "Good for ~10 emails"
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
amountSats: 5e3,
|
|
356
|
+
priceSats: 4500,
|
|
357
|
+
bonusSats: 500,
|
|
358
|
+
label: "5,000 sats",
|
|
359
|
+
description: "Good for ~50 emails (+10% bonus)"
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
amountSats: 1e4,
|
|
363
|
+
priceSats: 8500,
|
|
364
|
+
bonusSats: 1500,
|
|
365
|
+
label: "10,000 sats",
|
|
366
|
+
description: "Good for ~100 emails (+15% bonus)"
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
amountSats: 5e4,
|
|
370
|
+
priceSats: 4e4,
|
|
371
|
+
bonusSats: 1e4,
|
|
372
|
+
label: "50,000 sats",
|
|
373
|
+
description: "Good for ~500 emails (+20% bonus)"
|
|
369
374
|
}
|
|
370
|
-
|
|
375
|
+
];
|
|
371
376
|
}
|
|
372
377
|
|
|
373
|
-
// src/commands/
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (!
|
|
377
|
-
console.
|
|
378
|
-
console.log(pc4.dim("Run `btcemail login` to authenticate."));
|
|
378
|
+
// src/commands/blocklist.ts
|
|
379
|
+
async function blocklistListCommand(options = {}) {
|
|
380
|
+
const token = getToken();
|
|
381
|
+
if (!token) {
|
|
382
|
+
console.error(pc.red("Not logged in. Run 'btcemail login' first."));
|
|
379
383
|
process.exit(1);
|
|
380
384
|
}
|
|
381
|
-
const result = await
|
|
382
|
-
folder: options.folder || "inbox",
|
|
383
|
-
limit: options.limit || 20
|
|
384
|
-
});
|
|
385
|
+
const result = await getBlocklist(options.username);
|
|
385
386
|
if (!result.ok) {
|
|
386
|
-
console.error(
|
|
387
|
+
console.error(pc.red(`Error: ${result.error.message}`));
|
|
387
388
|
process.exit(1);
|
|
388
389
|
}
|
|
389
|
-
const {
|
|
390
|
+
const { username, entries } = result.data;
|
|
390
391
|
if (options.json) {
|
|
391
|
-
console.log(JSON.stringify({
|
|
392
|
+
console.log(JSON.stringify({ username, entries }, null, 2));
|
|
392
393
|
return;
|
|
393
394
|
}
|
|
394
395
|
console.log();
|
|
395
|
-
console.log(
|
|
396
|
-
pc4.bold(`${options.folder || "Inbox"} (${pagination.total} emails)`)
|
|
397
|
-
);
|
|
396
|
+
console.log(pc.bold("Blocklist for ") + pc.cyan(`${username}@btc.email`));
|
|
398
397
|
console.log();
|
|
399
|
-
if (
|
|
400
|
-
console.log(
|
|
398
|
+
if (entries.length === 0) {
|
|
399
|
+
console.log(pc.dim(" No entries in blocklist."));
|
|
400
|
+
console.log();
|
|
401
|
+
console.log(pc.dim(" Add entries with: btcemail blocklist add <email>"));
|
|
402
|
+
console.log(pc.dim(" Or for domains: btcemail blocklist add @domain.com"));
|
|
401
403
|
console.log();
|
|
402
404
|
return;
|
|
403
405
|
}
|
|
404
|
-
console.log(
|
|
405
|
-
` ${pc4.dim("ID".padEnd(12))} ${pc4.dim("From".padEnd(25))} ${pc4.dim("Subject".padEnd(40))} ${pc4.dim("Date")}`
|
|
406
|
-
);
|
|
407
|
-
console.log(pc4.dim(" " + "-".repeat(90)));
|
|
408
|
-
for (const email of emails) {
|
|
409
|
-
const unreadMarker = email.unread ? pc4.cyan("*") : " ";
|
|
410
|
-
const id = email.id.slice(0, 10).padEnd(12);
|
|
411
|
-
const from = truncate(email.from.name || email.from.email, 23).padEnd(25);
|
|
412
|
-
const subject = truncate(email.subject, 38).padEnd(40);
|
|
413
|
-
const date = formatDate(email.date);
|
|
414
|
-
console.log(`${unreadMarker} ${id} ${from} ${subject} ${pc4.dim(date)}`);
|
|
415
|
-
}
|
|
406
|
+
console.log(pc.dim(` ${entries.length} ${entries.length === 1 ? "entry" : "entries"}:`));
|
|
416
407
|
console.log();
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
);
|
|
423
|
-
console.log();
|
|
408
|
+
for (const entry of entries) {
|
|
409
|
+
const value = entry.sender_email || `@${entry.sender_domain}`;
|
|
410
|
+
const reason = entry.reason ? pc.dim(` - ${entry.reason}`) : "";
|
|
411
|
+
console.log(` ${pc.red(value)}${reason}`);
|
|
412
|
+
console.log(pc.dim(` ID: ${entry.id}`));
|
|
424
413
|
}
|
|
414
|
+
console.log();
|
|
425
415
|
}
|
|
426
|
-
function
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const date = new Date(dateString);
|
|
432
|
-
const now = /* @__PURE__ */ new Date();
|
|
433
|
-
const diffMs = now.getTime() - date.getTime();
|
|
434
|
-
const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
|
|
435
|
-
if (diffDays === 0) {
|
|
436
|
-
return date.toLocaleTimeString("en-US", {
|
|
437
|
-
hour: "numeric",
|
|
438
|
-
minute: "2-digit"
|
|
439
|
-
});
|
|
416
|
+
async function blocklistAddCommand(entry, options = {}) {
|
|
417
|
+
const token = getToken();
|
|
418
|
+
if (!token) {
|
|
419
|
+
console.error(pc.red("Not logged in. Run 'btcemail login' first."));
|
|
420
|
+
process.exit(1);
|
|
440
421
|
}
|
|
441
|
-
|
|
442
|
-
|
|
422
|
+
const isDomain = entry.startsWith("@");
|
|
423
|
+
const payload = isDomain ? { domain: entry.slice(1), reason: options.reason } : { email: entry, reason: options.reason };
|
|
424
|
+
const result = await addToBlocklist(payload, options.username);
|
|
425
|
+
if (!result.ok) {
|
|
426
|
+
console.error(pc.red(`Error: ${result.error.message}`));
|
|
427
|
+
process.exit(1);
|
|
443
428
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
429
|
+
if (options.json) {
|
|
430
|
+
console.log(JSON.stringify({ success: true, entry: result.data.entry }, null, 2));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
console.log(pc.green(`Added to blocklist: ${pc.red(entry)}`));
|
|
448
434
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
if (!isAuthenticated()) {
|
|
454
|
-
console.log(pc5.yellow("Not logged in."));
|
|
455
|
-
console.log(pc5.dim("Run `btcemail login` to authenticate."));
|
|
435
|
+
async function blocklistRemoveCommand(id, options = {}) {
|
|
436
|
+
const token = getToken();
|
|
437
|
+
if (!token) {
|
|
438
|
+
console.error(pc.red("Not logged in. Run 'btcemail login' first."));
|
|
456
439
|
process.exit(1);
|
|
457
440
|
}
|
|
458
|
-
const result = await
|
|
441
|
+
const result = await removeFromBlocklist(id);
|
|
459
442
|
if (!result.ok) {
|
|
460
|
-
|
|
461
|
-
console.error(pc5.red(`Email not found: ${id}`));
|
|
462
|
-
} else {
|
|
463
|
-
console.error(pc5.red(`Error: ${result.error.message}`));
|
|
464
|
-
}
|
|
443
|
+
console.error(pc.red(`Error: ${result.error.message}`));
|
|
465
444
|
process.exit(1);
|
|
466
445
|
}
|
|
467
|
-
const email = result.data;
|
|
468
446
|
if (options.json) {
|
|
469
|
-
console.log(JSON.stringify(
|
|
447
|
+
console.log(JSON.stringify({ success: true }, null, 2));
|
|
470
448
|
return;
|
|
471
449
|
}
|
|
472
|
-
console.log();
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
|
|
450
|
+
console.log(pc.green("Removed from blocklist"));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/commands/credits.ts
|
|
454
|
+
import ora from "ora";
|
|
455
|
+
import pc3 from "picocolors";
|
|
456
|
+
import qrcode from "qrcode-terminal";
|
|
457
|
+
|
|
458
|
+
// src/utils/payment-polling.ts
|
|
459
|
+
import pc2 from "picocolors";
|
|
460
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
461
|
+
async function pollForPayment(options) {
|
|
462
|
+
const { paymentHash, checkFn, maxAttempts = 120, intervalMs = 2500, onPaid } = options;
|
|
463
|
+
let frame = 0;
|
|
464
|
+
let attempts = 0;
|
|
465
|
+
process.stdout.write(`${pc2.cyan(SPINNER_FRAMES[0])} Waiting for payment...`);
|
|
466
|
+
while (attempts < maxAttempts) {
|
|
467
|
+
try {
|
|
468
|
+
const status = await checkFn(paymentHash);
|
|
469
|
+
if (status.paid) {
|
|
470
|
+
process.stdout.write(`\r${" ".repeat(50)}\r`);
|
|
471
|
+
console.log(`${pc2.green("\u2713")} Payment received!`);
|
|
472
|
+
if (onPaid) onPaid();
|
|
473
|
+
return status;
|
|
474
|
+
}
|
|
475
|
+
} catch {
|
|
476
|
+
}
|
|
477
|
+
frame = (frame + 1) % SPINNER_FRAMES.length;
|
|
478
|
+
const timeLeft = Math.ceil((maxAttempts - attempts) * intervalMs / 1e3 / 60);
|
|
479
|
+
process.stdout.write(
|
|
480
|
+
`\r${pc2.cyan(SPINNER_FRAMES[frame])} Waiting for payment... (${timeLeft}m remaining)`
|
|
481
|
+
);
|
|
482
|
+
await sleep(intervalMs);
|
|
483
|
+
attempts++;
|
|
484
|
+
}
|
|
485
|
+
process.stdout.write(`\r${" ".repeat(60)}\r`);
|
|
486
|
+
console.log(`${pc2.yellow("\u26A0")} Timed out waiting for payment.`);
|
|
487
|
+
return { paid: false };
|
|
488
|
+
}
|
|
489
|
+
function sleep(ms) {
|
|
490
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/commands/credits.ts
|
|
494
|
+
async function creditsBalanceCommand() {
|
|
495
|
+
if (!isAuthenticated()) {
|
|
496
|
+
console.log(pc3.yellow("Not logged in."));
|
|
497
|
+
console.log(pc3.dim("Run `btcemail login` to authenticate."));
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
const spinner = ora("Fetching balance...").start();
|
|
501
|
+
const result = await getCreditsBalance();
|
|
502
|
+
if (!result.ok) {
|
|
503
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
spinner.stop();
|
|
507
|
+
const { balanceSats, lifetimePurchasedSats, lifetimeSpentSats } = result.data;
|
|
508
|
+
console.log();
|
|
509
|
+
console.log(pc3.bold("Credit Balance"));
|
|
510
|
+
console.log();
|
|
511
|
+
console.log(` ${pc3.green(formatSats(balanceSats))} sats`);
|
|
512
|
+
console.log();
|
|
513
|
+
if (lifetimePurchasedSats > 0 || lifetimeSpentSats > 0) {
|
|
514
|
+
console.log(pc3.dim(` Purchased: ${formatSats(lifetimePurchasedSats)} sats`));
|
|
515
|
+
console.log(pc3.dim(` Spent: ${formatSats(lifetimeSpentSats)} sats`));
|
|
516
|
+
console.log();
|
|
517
|
+
}
|
|
518
|
+
if (balanceSats === 0) {
|
|
519
|
+
console.log(pc3.dim(" No credits available."));
|
|
520
|
+
console.log(pc3.dim(" Run `btcemail credits purchase` to buy credits."));
|
|
521
|
+
console.log();
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
async function creditsHistoryCommand(options) {
|
|
525
|
+
if (!isAuthenticated()) {
|
|
526
|
+
console.log(pc3.yellow("Not logged in."));
|
|
527
|
+
console.log(pc3.dim("Run `btcemail login` to authenticate."));
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
const spinner = ora("Loading transaction history...").start();
|
|
531
|
+
const result = await getCreditTransactions({
|
|
532
|
+
limit: options.limit || 20,
|
|
533
|
+
type: options.type
|
|
534
|
+
});
|
|
535
|
+
if (!result.ok) {
|
|
536
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
const { data: transactions, pagination } = result.data;
|
|
540
|
+
spinner.stop();
|
|
541
|
+
if (options.json) {
|
|
542
|
+
console.log(JSON.stringify({ transactions, pagination }, null, 2));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
console.log();
|
|
546
|
+
console.log(pc3.bold("Transaction History"));
|
|
547
|
+
console.log();
|
|
548
|
+
if (transactions.length === 0) {
|
|
549
|
+
console.log(pc3.dim(" No transactions found."));
|
|
550
|
+
console.log();
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
console.log(
|
|
554
|
+
` ${pc3.dim("Date".padEnd(12))} ${pc3.dim("Type".padEnd(15))} ${pc3.dim("Amount".padEnd(12))} ${pc3.dim("Balance")}`
|
|
555
|
+
);
|
|
556
|
+
console.log(pc3.dim(` ${"-".repeat(55)}`));
|
|
557
|
+
for (const tx of transactions) {
|
|
558
|
+
const date = formatDate(tx.createdAt);
|
|
559
|
+
const type = formatTransactionType(tx.transactionType).padEnd(15);
|
|
560
|
+
const amount = formatAmount(tx.amountSats).padEnd(12);
|
|
561
|
+
const balance = formatSats(tx.balanceAfter);
|
|
562
|
+
console.log(` ${date.padEnd(12)} ${type} ${amount} ${pc3.dim(balance)}`);
|
|
563
|
+
}
|
|
564
|
+
console.log();
|
|
565
|
+
if (pagination.hasMore) {
|
|
566
|
+
console.log(pc3.dim(` Use --limit to see more transactions.`));
|
|
567
|
+
console.log();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
async function creditsPurchaseCommand(options) {
|
|
571
|
+
if (!isAuthenticated()) {
|
|
572
|
+
console.log(pc3.yellow("Not logged in."));
|
|
573
|
+
console.log(pc3.dim("Run `btcemail login` to authenticate."));
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
const purchaseOptions = getCreditPurchaseOptions();
|
|
577
|
+
if (options.option === void 0) {
|
|
578
|
+
console.log();
|
|
579
|
+
console.log(pc3.bold("Purchase Options"));
|
|
580
|
+
console.log();
|
|
581
|
+
purchaseOptions.forEach((opt, index) => {
|
|
582
|
+
console.log(` ${pc3.cyan(`[${index}]`)} ${opt.label}`);
|
|
583
|
+
console.log(` ${pc3.dim(opt.description)}`);
|
|
584
|
+
console.log(` ${pc3.dim(`Price: ${formatSats(opt.priceSats)} sats`)}`);
|
|
585
|
+
if (opt.bonusSats > 0) {
|
|
586
|
+
console.log(` ${pc3.green(`+${formatSats(opt.bonusSats)} bonus sats`)}`);
|
|
587
|
+
}
|
|
588
|
+
console.log();
|
|
589
|
+
});
|
|
590
|
+
console.log(pc3.dim(" Usage: btcemail credits purchase --option <index>"));
|
|
591
|
+
console.log(pc3.dim(" Add --wait to wait for payment confirmation."));
|
|
592
|
+
console.log();
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (options.option < 0 || options.option >= purchaseOptions.length) {
|
|
596
|
+
console.error(pc3.red(`Error: Invalid option. Choose 0-${purchaseOptions.length - 1}.`));
|
|
597
|
+
process.exit(1);
|
|
598
|
+
}
|
|
599
|
+
const _selectedOption = purchaseOptions[options.option];
|
|
600
|
+
const spinner = ora("Creating invoice...").start();
|
|
601
|
+
const result = await purchaseCredits(options.option);
|
|
602
|
+
if (!result.ok) {
|
|
603
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
604
|
+
process.exit(1);
|
|
605
|
+
}
|
|
606
|
+
spinner.stop();
|
|
607
|
+
const purchase = result.data;
|
|
608
|
+
if (options.json) {
|
|
609
|
+
console.log(JSON.stringify(purchase, null, 2));
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
console.log();
|
|
613
|
+
console.log(pc3.bold("Lightning Invoice"));
|
|
614
|
+
console.log();
|
|
615
|
+
console.log(` ${pc3.dim("Amount:")} ${formatSats(purchase.amountSats)} sats`);
|
|
616
|
+
console.log(` ${pc3.dim("Credits:")} ${formatSats(purchase.creditsToReceive)} sats`);
|
|
617
|
+
if (purchase.bonusSats > 0) {
|
|
618
|
+
console.log(` ${pc3.dim("Bonus:")} ${pc3.green(`+${formatSats(purchase.bonusSats)} sats`)}`);
|
|
619
|
+
}
|
|
620
|
+
console.log();
|
|
621
|
+
console.log(pc3.bold("Scan to pay:"));
|
|
622
|
+
console.log();
|
|
623
|
+
qrcode.generate(purchase.invoice, { small: true }, (qr) => {
|
|
624
|
+
const indentedQr = qr.split("\n").map((line) => ` ${line}`).join("\n");
|
|
625
|
+
console.log(indentedQr);
|
|
626
|
+
});
|
|
627
|
+
console.log();
|
|
628
|
+
console.log(pc3.bold("Invoice:"));
|
|
629
|
+
console.log();
|
|
630
|
+
console.log(` ${purchase.invoice}`);
|
|
631
|
+
console.log();
|
|
632
|
+
console.log(pc3.dim(` Payment hash: ${purchase.paymentHash}`));
|
|
633
|
+
console.log(pc3.dim(` Expires: ${new Date(purchase.expiresAt).toLocaleString()}`));
|
|
634
|
+
console.log();
|
|
635
|
+
if (options.wait) {
|
|
636
|
+
console.log();
|
|
637
|
+
const status = await pollForPayment({
|
|
638
|
+
paymentHash: purchase.paymentHash,
|
|
639
|
+
checkFn: async (hash) => {
|
|
640
|
+
const result2 = await checkPaymentStatus(hash);
|
|
641
|
+
if (result2.ok) {
|
|
642
|
+
return { paid: result2.data.paid };
|
|
643
|
+
}
|
|
644
|
+
return { paid: false };
|
|
645
|
+
},
|
|
646
|
+
onPaid: async () => {
|
|
647
|
+
const balanceResult = await getCreditsBalance();
|
|
648
|
+
if (balanceResult.ok) {
|
|
649
|
+
console.log();
|
|
650
|
+
console.log(
|
|
651
|
+
`${pc3.dim("New balance:")} ${pc3.green(formatSats(balanceResult.data.balanceSats))} sats`
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
if (!status.paid) {
|
|
657
|
+
console.log(pc3.dim(" You can still pay the invoice and credits will be added."));
|
|
658
|
+
}
|
|
659
|
+
console.log();
|
|
660
|
+
} else {
|
|
661
|
+
console.log(pc3.dim(" Scan the QR code or copy the invoice above to pay."));
|
|
662
|
+
console.log(pc3.dim(" Use --wait to wait for payment confirmation."));
|
|
663
|
+
console.log();
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function formatSats(sats) {
|
|
667
|
+
return sats.toLocaleString();
|
|
668
|
+
}
|
|
669
|
+
function formatAmount(sats) {
|
|
670
|
+
if (sats >= 0) {
|
|
671
|
+
return pc3.green(`+${formatSats(sats)}`);
|
|
672
|
+
}
|
|
673
|
+
return pc3.red(formatSats(sats));
|
|
674
|
+
}
|
|
675
|
+
function formatTransactionType(type) {
|
|
676
|
+
const types = {
|
|
677
|
+
topup: "Top-up",
|
|
678
|
+
email_sent: "Email Sent",
|
|
679
|
+
email_received: "Email Received",
|
|
680
|
+
refund: "Refund",
|
|
681
|
+
bonus: "Bonus"
|
|
682
|
+
};
|
|
683
|
+
return types[type] || type;
|
|
684
|
+
}
|
|
685
|
+
function formatDate(dateString) {
|
|
686
|
+
const date = new Date(dateString);
|
|
687
|
+
return date.toLocaleDateString("en-US", {
|
|
688
|
+
month: "short",
|
|
689
|
+
day: "numeric"
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/commands/inbound.ts
|
|
694
|
+
import ora2 from "ora";
|
|
695
|
+
import pc4 from "picocolors";
|
|
696
|
+
async function inboundDeliveredCommand(options) {
|
|
697
|
+
if (!isAuthenticated()) {
|
|
698
|
+
console.log(pc4.yellow("Not logged in."));
|
|
699
|
+
console.log(pc4.dim("Run `btcemail login` to authenticate."));
|
|
700
|
+
process.exit(1);
|
|
701
|
+
}
|
|
702
|
+
const spinner = ora2("Loading delivered emails...").start();
|
|
703
|
+
const result = await getDeliveredEmails({
|
|
704
|
+
limit: options.limit || 20
|
|
705
|
+
});
|
|
706
|
+
if (!result.ok) {
|
|
707
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
708
|
+
process.exit(1);
|
|
709
|
+
}
|
|
710
|
+
const { data: emails, pagination } = result.data;
|
|
711
|
+
spinner.stop();
|
|
712
|
+
if (options.json) {
|
|
713
|
+
console.log(JSON.stringify({ emails, pagination }, null, 2));
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
console.log();
|
|
717
|
+
console.log(pc4.bold(`Delivered (${pagination.total} paid emails received)`));
|
|
718
|
+
console.log();
|
|
719
|
+
if (emails.length === 0) {
|
|
720
|
+
console.log(pc4.dim(" No delivered emails yet."));
|
|
721
|
+
console.log();
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
console.log(
|
|
725
|
+
` ${pc4.dim("#".padEnd(4))} ${pc4.dim("From".padEnd(28))} ${pc4.dim("Subject".padEnd(40))} ${pc4.dim("Date")}`
|
|
726
|
+
);
|
|
727
|
+
console.log(pc4.dim(` ${"-".repeat(85)}`));
|
|
728
|
+
emails.forEach((email, index) => {
|
|
729
|
+
const num = pc4.cyan(String(index + 1).padEnd(4));
|
|
730
|
+
const from = truncate(email.from.name || email.from.email, 26).padEnd(28);
|
|
731
|
+
const subject = truncate(email.subject, 38).padEnd(40);
|
|
732
|
+
const date = formatDate2(email.date);
|
|
733
|
+
console.log(` ${num}${from} ${subject} ${pc4.dim(date)}`);
|
|
734
|
+
});
|
|
735
|
+
console.log();
|
|
736
|
+
if (pagination.hasMore) {
|
|
737
|
+
console.log(
|
|
738
|
+
pc4.dim(` Showing ${emails.length} of ${pagination.total}. Use --limit to see more.`)
|
|
739
|
+
);
|
|
740
|
+
console.log();
|
|
741
|
+
}
|
|
742
|
+
console.log(pc4.dim(" Read email: btcemail read <id>"));
|
|
743
|
+
console.log();
|
|
744
|
+
}
|
|
745
|
+
async function inboundPendingCommand(options) {
|
|
746
|
+
if (!isAuthenticated()) {
|
|
747
|
+
console.log(pc4.yellow("Not logged in."));
|
|
748
|
+
console.log(pc4.dim("Run `btcemail login` to authenticate."));
|
|
749
|
+
process.exit(1);
|
|
750
|
+
}
|
|
751
|
+
const spinner = ora2("Loading pending emails...").start();
|
|
752
|
+
const result = await getPendingEmails({
|
|
753
|
+
limit: options.limit || 20
|
|
754
|
+
});
|
|
755
|
+
if (!result.ok) {
|
|
756
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
757
|
+
process.exit(1);
|
|
758
|
+
}
|
|
759
|
+
const { data: emails, pagination } = result.data;
|
|
760
|
+
spinner.stop();
|
|
761
|
+
if (options.json) {
|
|
762
|
+
console.log(JSON.stringify({ emails, pagination }, null, 2));
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
console.log();
|
|
766
|
+
console.log(pc4.bold(`Pending (${pagination.total} awaiting payment)`));
|
|
767
|
+
console.log();
|
|
768
|
+
if (emails.length === 0) {
|
|
769
|
+
console.log(pc4.dim(" No pending emails."));
|
|
770
|
+
console.log();
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
console.log(
|
|
774
|
+
` ${pc4.dim("#".padEnd(4))} ${pc4.dim("From".padEnd(28))} ${pc4.dim("Subject".padEnd(35))} ${pc4.dim("Sats")}`
|
|
775
|
+
);
|
|
776
|
+
console.log(pc4.dim(` ${"-".repeat(80)}`));
|
|
777
|
+
emails.forEach((email, index) => {
|
|
778
|
+
const num = pc4.cyan(String(index + 1).padEnd(4));
|
|
779
|
+
const from = truncate(email.from.name || email.from.email, 26).padEnd(28);
|
|
780
|
+
const subject = truncate(email.subject, 33).padEnd(35);
|
|
781
|
+
const sats = pc4.yellow(String(email.amountSats));
|
|
782
|
+
console.log(` ${num}${from} ${subject} ${sats}`);
|
|
783
|
+
});
|
|
784
|
+
console.log();
|
|
785
|
+
if (pagination.hasMore) {
|
|
786
|
+
console.log(
|
|
787
|
+
pc4.dim(` Showing ${emails.length} of ${pagination.total}. Use --limit to see more.`)
|
|
788
|
+
);
|
|
789
|
+
console.log();
|
|
790
|
+
}
|
|
791
|
+
console.log(pc4.dim(" View details: btcemail inbound view <id>"));
|
|
792
|
+
console.log();
|
|
793
|
+
}
|
|
794
|
+
async function inboundViewCommand(id, options) {
|
|
795
|
+
if (!isAuthenticated()) {
|
|
796
|
+
console.log(pc4.yellow("Not logged in."));
|
|
797
|
+
console.log(pc4.dim("Run `btcemail login` to authenticate."));
|
|
798
|
+
process.exit(1);
|
|
799
|
+
}
|
|
800
|
+
const spinner = ora2("Loading email details...").start();
|
|
801
|
+
const result = await getPendingEmail(id);
|
|
802
|
+
if (!result.ok) {
|
|
803
|
+
if (result.error.code === "NOT_FOUND") {
|
|
804
|
+
spinner.fail(`Pending email not found: ${id}`);
|
|
805
|
+
} else {
|
|
806
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
807
|
+
}
|
|
808
|
+
process.exit(1);
|
|
809
|
+
}
|
|
810
|
+
spinner.stop();
|
|
811
|
+
const email = result.data;
|
|
812
|
+
if (options.json) {
|
|
813
|
+
console.log(JSON.stringify(email, null, 2));
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
console.log();
|
|
817
|
+
console.log(pc4.bold(email.subject || "(No subject)"));
|
|
818
|
+
console.log();
|
|
819
|
+
console.log(`${pc4.dim("From:")} ${formatAddress(email.from)}`);
|
|
820
|
+
console.log(`${pc4.dim("Date:")} ${formatFullDate(email.createdAt)}`);
|
|
821
|
+
console.log(`${pc4.dim("ID:")} ${email.id}`);
|
|
822
|
+
console.log();
|
|
823
|
+
console.log(pc4.yellow(`Payment Required: ${email.amountSats} sats`));
|
|
824
|
+
console.log();
|
|
825
|
+
if (email.paymentHash) {
|
|
826
|
+
console.log(pc4.dim("To receive this email, pay the invoice and run:"));
|
|
827
|
+
console.log(pc4.cyan(` btcemail inbound pay ${email.id} --payment-hash <preimage>`));
|
|
828
|
+
} else {
|
|
829
|
+
console.log(pc4.dim("Payment information not available."));
|
|
830
|
+
}
|
|
831
|
+
console.log();
|
|
832
|
+
console.log(pc4.dim("-".repeat(60)));
|
|
833
|
+
console.log();
|
|
834
|
+
console.log(pc4.dim("Preview (full content available after payment):"));
|
|
835
|
+
console.log();
|
|
836
|
+
console.log(truncate(email.bodyText || email.body, 200));
|
|
837
|
+
console.log();
|
|
838
|
+
}
|
|
839
|
+
async function inboundPayCommand(id, options) {
|
|
840
|
+
if (!isAuthenticated()) {
|
|
841
|
+
console.log(pc4.yellow("Not logged in."));
|
|
842
|
+
console.log(pc4.dim("Run `btcemail login` to authenticate."));
|
|
843
|
+
process.exit(1);
|
|
844
|
+
}
|
|
845
|
+
if (!options.paymentHash) {
|
|
846
|
+
console.error(pc4.red("Payment hash is required. Use --payment-hash <preimage>"));
|
|
847
|
+
process.exit(1);
|
|
848
|
+
}
|
|
849
|
+
const spinner = ora2("Verifying payment...").start();
|
|
850
|
+
const result = await payForEmail(id, options.paymentHash);
|
|
851
|
+
if (!result.ok) {
|
|
852
|
+
if (result.error.code === "PAYMENT_INVALID") {
|
|
853
|
+
spinner.fail("Payment verification failed.");
|
|
854
|
+
console.log(pc4.dim("Make sure you paid the correct invoice and provided the preimage."));
|
|
855
|
+
} else if (result.error.code === "NOT_FOUND") {
|
|
856
|
+
spinner.fail(`Pending email not found or already delivered: ${id}`);
|
|
857
|
+
} else {
|
|
858
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
859
|
+
}
|
|
860
|
+
process.exit(1);
|
|
861
|
+
}
|
|
862
|
+
spinner.succeed("Payment verified! Email delivered.");
|
|
863
|
+
console.log();
|
|
864
|
+
console.log(`${pc4.dim("Email ID:")} ${result.data.emailId}`);
|
|
865
|
+
console.log(`${pc4.dim("Status:")} ${result.data.status}`);
|
|
866
|
+
console.log();
|
|
867
|
+
console.log(pc4.dim(`Read email: btcemail read ${result.data.emailId}`));
|
|
868
|
+
console.log();
|
|
869
|
+
}
|
|
870
|
+
function truncate(str, maxLength) {
|
|
871
|
+
if (str.length <= maxLength) return str;
|
|
872
|
+
return `${str.slice(0, maxLength - 1)}\u2026`;
|
|
496
873
|
}
|
|
497
874
|
function formatAddress(addr) {
|
|
498
875
|
if (addr.name) {
|
|
@@ -511,47 +888,511 @@ function formatFullDate(dateString) {
|
|
|
511
888
|
minute: "2-digit"
|
|
512
889
|
});
|
|
513
890
|
}
|
|
891
|
+
function formatDate2(dateString) {
|
|
892
|
+
const date = new Date(dateString);
|
|
893
|
+
const now = /* @__PURE__ */ new Date();
|
|
894
|
+
const diffMs = now.getTime() - date.getTime();
|
|
895
|
+
const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
|
|
896
|
+
if (diffDays === 0) {
|
|
897
|
+
return date.toLocaleTimeString("en-US", {
|
|
898
|
+
hour: "numeric",
|
|
899
|
+
minute: "2-digit"
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
if (diffDays < 7) {
|
|
903
|
+
return date.toLocaleDateString("en-US", { weekday: "short" });
|
|
904
|
+
}
|
|
905
|
+
return date.toLocaleDateString("en-US", {
|
|
906
|
+
month: "short",
|
|
907
|
+
day: "numeric"
|
|
908
|
+
});
|
|
909
|
+
}
|
|
514
910
|
|
|
515
|
-
// src/commands/
|
|
911
|
+
// src/commands/list.ts
|
|
912
|
+
import ora3 from "ora";
|
|
913
|
+
import pc5 from "picocolors";
|
|
914
|
+
|
|
915
|
+
// src/utils/cache.ts
|
|
916
|
+
var CACHE_KEY = "emailCache";
|
|
917
|
+
var CACHE_TTL_MS = 30 * 60 * 1e3;
|
|
918
|
+
function cacheEmailList(folder, emails) {
|
|
919
|
+
config.set(CACHE_KEY, {
|
|
920
|
+
folder,
|
|
921
|
+
emails,
|
|
922
|
+
timestamp: Date.now()
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
function getCachedEmailList(folder) {
|
|
926
|
+
const cache = config.get(CACHE_KEY);
|
|
927
|
+
if (!cache) return null;
|
|
928
|
+
if (Date.now() - cache.timestamp > CACHE_TTL_MS) {
|
|
929
|
+
config.delete(CACHE_KEY);
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
if (folder && cache.folder !== folder) {
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
return cache;
|
|
936
|
+
}
|
|
937
|
+
function getEmailIdByIndex(index) {
|
|
938
|
+
const cache = getCachedEmailList();
|
|
939
|
+
if (!cache) return null;
|
|
940
|
+
const idx = index - 1;
|
|
941
|
+
if (idx < 0 || idx >= cache.emails.length) {
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
return cache.emails[idx].id;
|
|
945
|
+
}
|
|
946
|
+
function isNumericId(id) {
|
|
947
|
+
return /^\d+$/.test(id);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/commands/list.ts
|
|
951
|
+
async function listCommand(options) {
|
|
952
|
+
if (!isAuthenticated()) {
|
|
953
|
+
console.log(pc5.yellow("Not logged in."));
|
|
954
|
+
console.log(pc5.dim("Run `btcemail login` to authenticate."));
|
|
955
|
+
process.exit(1);
|
|
956
|
+
}
|
|
957
|
+
const folder = options.folder || "inbox";
|
|
958
|
+
const limit = options.limit || 20;
|
|
959
|
+
const page = options.page || 1;
|
|
960
|
+
const offset = (page - 1) * limit;
|
|
961
|
+
const spinner = ora3("Loading emails...").start();
|
|
962
|
+
const result = await getEmails({
|
|
963
|
+
folder,
|
|
964
|
+
limit,
|
|
965
|
+
offset
|
|
966
|
+
});
|
|
967
|
+
if (!result.ok) {
|
|
968
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
969
|
+
process.exit(1);
|
|
970
|
+
}
|
|
971
|
+
const { data: emails, pagination } = result.data;
|
|
972
|
+
spinner.stop();
|
|
973
|
+
const cachedEmails = emails.map((e) => ({
|
|
974
|
+
id: e.id,
|
|
975
|
+
subject: e.subject,
|
|
976
|
+
from: e.from.name || e.from.email
|
|
977
|
+
}));
|
|
978
|
+
cacheEmailList(folder, cachedEmails);
|
|
979
|
+
if (options.json) {
|
|
980
|
+
console.log(JSON.stringify({ emails, pagination }, null, 2));
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
console.log();
|
|
984
|
+
console.log(pc5.bold(`${capitalize(folder)} (${pagination.total} emails)`));
|
|
985
|
+
console.log();
|
|
986
|
+
if (emails.length === 0) {
|
|
987
|
+
console.log(pc5.dim(" No emails found."));
|
|
988
|
+
console.log();
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
console.log(
|
|
992
|
+
` ${pc5.dim("#".padEnd(4))} ${pc5.dim("From".padEnd(25))} ${pc5.dim("Subject".padEnd(45))} ${pc5.dim("Date")}`
|
|
993
|
+
);
|
|
994
|
+
console.log(pc5.dim(` ${"-".repeat(85)}`));
|
|
995
|
+
emails.forEach((email, index) => {
|
|
996
|
+
const num = pc5.cyan(String(index + 1).padEnd(4));
|
|
997
|
+
const unreadMarker = email.unread ? pc5.cyan("\u25CF") : " ";
|
|
998
|
+
const from = truncate2(email.from.name || email.from.email, 23).padEnd(25);
|
|
999
|
+
const subject = truncate2(email.subject, 43).padEnd(45);
|
|
1000
|
+
const date = formatDate3(email.date);
|
|
1001
|
+
console.log(`${unreadMarker} ${num}${from} ${subject} ${pc5.dim(date)}`);
|
|
1002
|
+
});
|
|
1003
|
+
console.log();
|
|
1004
|
+
const totalPages = Math.ceil(pagination.total / limit);
|
|
1005
|
+
const currentPage = page;
|
|
1006
|
+
if (totalPages > 1) {
|
|
1007
|
+
console.log(
|
|
1008
|
+
pc5.dim(` Page ${currentPage} of ${totalPages} (${emails.length} of ${pagination.total})`)
|
|
1009
|
+
);
|
|
1010
|
+
if (currentPage < totalPages) {
|
|
1011
|
+
console.log(pc5.dim(` Next page: btcemail list --page ${currentPage + 1}`));
|
|
1012
|
+
}
|
|
1013
|
+
console.log();
|
|
1014
|
+
}
|
|
1015
|
+
console.log(pc5.dim(" Read email: btcemail read <#> or btcemail read <id>"));
|
|
1016
|
+
console.log();
|
|
1017
|
+
}
|
|
1018
|
+
function capitalize(str) {
|
|
1019
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
1020
|
+
}
|
|
1021
|
+
function truncate2(str, maxLength) {
|
|
1022
|
+
if (str.length <= maxLength) return str;
|
|
1023
|
+
return `${str.slice(0, maxLength - 1)}\u2026`;
|
|
1024
|
+
}
|
|
1025
|
+
function formatDate3(dateString) {
|
|
1026
|
+
const date = new Date(dateString);
|
|
1027
|
+
const now = /* @__PURE__ */ new Date();
|
|
1028
|
+
const diffMs = now.getTime() - date.getTime();
|
|
1029
|
+
const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
|
|
1030
|
+
if (diffDays === 0) {
|
|
1031
|
+
return date.toLocaleTimeString("en-US", {
|
|
1032
|
+
hour: "numeric",
|
|
1033
|
+
minute: "2-digit"
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
if (diffDays < 7) {
|
|
1037
|
+
return date.toLocaleDateString("en-US", { weekday: "short" });
|
|
1038
|
+
}
|
|
1039
|
+
return date.toLocaleDateString("en-US", {
|
|
1040
|
+
month: "short",
|
|
1041
|
+
day: "numeric"
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/commands/login.ts
|
|
1046
|
+
import { randomUUID } from "crypto";
|
|
1047
|
+
import open from "open";
|
|
1048
|
+
import ora4 from "ora";
|
|
516
1049
|
import pc6 from "picocolors";
|
|
517
|
-
|
|
1050
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
1051
|
+
var POLL_TIMEOUT_MS = 3e5;
|
|
1052
|
+
async function loginCommand() {
|
|
1053
|
+
const sessionId = randomUUID();
|
|
1054
|
+
const baseUrl = getApiBaseUrl().replace("/api/v1", "");
|
|
1055
|
+
const authUrl = `${baseUrl}/auth/cli?session_id=${sessionId}`;
|
|
1056
|
+
const pollUrl = `${baseUrl}/api/auth/cli-session?session_id=${sessionId}`;
|
|
1057
|
+
console.log();
|
|
1058
|
+
console.log(pc6.bold("btc.email CLI Login"));
|
|
1059
|
+
console.log();
|
|
1060
|
+
try {
|
|
1061
|
+
const createResponse = await fetch(`${baseUrl}/api/auth/cli-session`, {
|
|
1062
|
+
method: "POST",
|
|
1063
|
+
headers: { "Content-Type": "application/json" },
|
|
1064
|
+
body: JSON.stringify({ session_id: sessionId })
|
|
1065
|
+
});
|
|
1066
|
+
if (!createResponse.ok) {
|
|
1067
|
+
console.error(pc6.red("Failed to initialize login session"));
|
|
1068
|
+
process.exit(1);
|
|
1069
|
+
}
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
console.error(pc6.red("Failed to connect to btc.email server"));
|
|
1072
|
+
console.error(pc6.dim(error instanceof Error ? error.message : "Unknown error"));
|
|
1073
|
+
process.exit(1);
|
|
1074
|
+
}
|
|
1075
|
+
console.log(pc6.dim("Opening browser for authentication..."));
|
|
1076
|
+
console.log();
|
|
1077
|
+
console.log(pc6.dim("If the browser doesn't open, visit:"));
|
|
1078
|
+
console.log(pc6.cyan(authUrl));
|
|
1079
|
+
console.log();
|
|
1080
|
+
try {
|
|
1081
|
+
await open(authUrl);
|
|
1082
|
+
} catch {
|
|
1083
|
+
}
|
|
1084
|
+
const spinner = ora4("Waiting for authentication...").start();
|
|
1085
|
+
const startTime = Date.now();
|
|
1086
|
+
while (Date.now() - startTime < POLL_TIMEOUT_MS) {
|
|
1087
|
+
try {
|
|
1088
|
+
const response = await fetch(pollUrl);
|
|
1089
|
+
if (response.status === 202) {
|
|
1090
|
+
await sleep2(POLL_INTERVAL_MS);
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
if (response.status === 410) {
|
|
1094
|
+
spinner.fail("Session expired. Please try again.");
|
|
1095
|
+
process.exit(1);
|
|
1096
|
+
}
|
|
1097
|
+
if (response.ok) {
|
|
1098
|
+
const data = await response.json();
|
|
1099
|
+
if (data.status === "complete") {
|
|
1100
|
+
setAuth({
|
|
1101
|
+
token: data.data.token,
|
|
1102
|
+
expiresAt: data.data.expiresAt,
|
|
1103
|
+
userId: data.data.userId,
|
|
1104
|
+
email: data.data.email
|
|
1105
|
+
});
|
|
1106
|
+
spinner.succeed(pc6.green("Successfully logged in!"));
|
|
1107
|
+
console.log();
|
|
1108
|
+
console.log(` ${pc6.dim("Email:")} ${data.data.email}`);
|
|
1109
|
+
console.log();
|
|
1110
|
+
console.log(pc6.dim("You can now use btcemail commands."));
|
|
1111
|
+
console.log(pc6.dim("Run `btcemail --help` to see available commands."));
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
await sleep2(POLL_INTERVAL_MS);
|
|
1116
|
+
} catch (_error) {
|
|
1117
|
+
await sleep2(POLL_INTERVAL_MS);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
spinner.fail("Authentication timed out. Please try again.");
|
|
1121
|
+
process.exit(1);
|
|
1122
|
+
}
|
|
1123
|
+
function sleep2(ms) {
|
|
1124
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// src/commands/logout.ts
|
|
1128
|
+
import pc7 from "picocolors";
|
|
1129
|
+
async function logoutCommand() {
|
|
518
1130
|
if (!isAuthenticated()) {
|
|
519
|
-
console.log(
|
|
520
|
-
|
|
1131
|
+
console.log(pc7.yellow("You are not logged in."));
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
clearAuth();
|
|
1135
|
+
console.log(pc7.green("Successfully logged out."));
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// src/commands/network.ts
|
|
1139
|
+
import pc8 from "picocolors";
|
|
1140
|
+
async function networkShowCommand(options = {}) {
|
|
1141
|
+
const token = getToken();
|
|
1142
|
+
if (!token) {
|
|
1143
|
+
console.error(pc8.red("Not logged in. Run 'btcemail login' first."));
|
|
1144
|
+
process.exit(1);
|
|
1145
|
+
}
|
|
1146
|
+
const result = await getNetwork();
|
|
1147
|
+
if (!result.ok) {
|
|
1148
|
+
console.error(pc8.red(`Error: ${result.error.message}`));
|
|
1149
|
+
process.exit(1);
|
|
1150
|
+
}
|
|
1151
|
+
const { network_mode, networks } = result.data;
|
|
1152
|
+
if (options.json) {
|
|
1153
|
+
console.log(JSON.stringify({ network_mode, networks }, null, 2));
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
console.log();
|
|
1157
|
+
console.log(pc8.bold("Network Mode"));
|
|
1158
|
+
console.log();
|
|
1159
|
+
const isTestnet = network_mode === "testnet";
|
|
1160
|
+
const currentLabel = isTestnet ? pc8.blue("testnet") + pc8.dim(" (fake Bitcoin)") : pc8.red("mainnet") + pc8.dim(" (real Bitcoin)");
|
|
1161
|
+
console.log(` Current: ${currentLabel}`);
|
|
1162
|
+
console.log();
|
|
1163
|
+
console.log(pc8.dim(" Available networks:"));
|
|
1164
|
+
console.log(
|
|
1165
|
+
` testnet: ${networks.testnet.available ? pc8.green("available") : pc8.red("unavailable")}`
|
|
1166
|
+
);
|
|
1167
|
+
console.log(
|
|
1168
|
+
` mainnet: ${networks.mainnet.available ? pc8.green("available") : pc8.red("unavailable")}`
|
|
1169
|
+
);
|
|
1170
|
+
console.log();
|
|
1171
|
+
if (!isTestnet) {
|
|
1172
|
+
console.log(pc8.yellow(" Warning: You are on mainnet. Payments use real Bitcoin!"));
|
|
1173
|
+
console.log();
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
async function networkSwitchCommand(network2, options = {}) {
|
|
1177
|
+
const token = getToken();
|
|
1178
|
+
if (!token) {
|
|
1179
|
+
console.error(pc8.red("Not logged in. Run 'btcemail login' first."));
|
|
1180
|
+
process.exit(1);
|
|
1181
|
+
}
|
|
1182
|
+
if (network2 !== "testnet" && network2 !== "mainnet") {
|
|
1183
|
+
console.error(pc8.red("Invalid network. Use 'testnet' or 'mainnet'."));
|
|
1184
|
+
process.exit(1);
|
|
1185
|
+
}
|
|
1186
|
+
if (network2 === "mainnet") {
|
|
1187
|
+
console.log();
|
|
1188
|
+
console.log(pc8.yellow("Warning: You are switching to mainnet."));
|
|
1189
|
+
console.log(pc8.yellow("All payments will use real Bitcoin!"));
|
|
1190
|
+
console.log();
|
|
1191
|
+
}
|
|
1192
|
+
const result = await setNetwork(network2);
|
|
1193
|
+
if (!result.ok) {
|
|
1194
|
+
console.error(pc8.red(`Error: ${result.error.message}`));
|
|
1195
|
+
process.exit(1);
|
|
1196
|
+
}
|
|
1197
|
+
if (options.json) {
|
|
1198
|
+
console.log(JSON.stringify({ success: true, network_mode: result.data.network_mode }, null, 2));
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
const label = network2 === "testnet" ? pc8.blue("testnet") : pc8.red("mainnet");
|
|
1202
|
+
console.log(pc8.green(`Switched to ${label}`));
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/commands/read.ts
|
|
1206
|
+
import ora5 from "ora";
|
|
1207
|
+
import pc9 from "picocolors";
|
|
1208
|
+
async function readCommand(idOrNumber, options) {
|
|
1209
|
+
if (!isAuthenticated()) {
|
|
1210
|
+
console.log(pc9.yellow("Not logged in."));
|
|
1211
|
+
console.log(pc9.dim("Run `btcemail login` to authenticate."));
|
|
1212
|
+
process.exit(1);
|
|
1213
|
+
}
|
|
1214
|
+
let emailId = idOrNumber;
|
|
1215
|
+
if (isNumericId(idOrNumber)) {
|
|
1216
|
+
const index = parseInt(idOrNumber, 10);
|
|
1217
|
+
const cachedId = getEmailIdByIndex(index);
|
|
1218
|
+
if (cachedId) {
|
|
1219
|
+
emailId = cachedId;
|
|
1220
|
+
} else {
|
|
1221
|
+
const cache = getCachedEmailList();
|
|
1222
|
+
if (cache) {
|
|
1223
|
+
console.error(pc9.red(`Email #${index} not found in list.`));
|
|
1224
|
+
console.log(
|
|
1225
|
+
pc9.dim(`List has ${cache.emails.length} emails. Run 'btcemail list' to refresh.`)
|
|
1226
|
+
);
|
|
1227
|
+
} else {
|
|
1228
|
+
console.error(pc9.red(`No email list cached.`));
|
|
1229
|
+
console.log(pc9.dim(`Run 'btcemail list' first, then use 'btcemail read <#>'.`));
|
|
1230
|
+
}
|
|
1231
|
+
process.exit(1);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
const spinner = ora5("Loading email...").start();
|
|
1235
|
+
const result = await getEmail(emailId);
|
|
1236
|
+
if (!result.ok) {
|
|
1237
|
+
if (result.error.code === "NOT_FOUND") {
|
|
1238
|
+
spinner.fail(`Email not found: ${emailId}`);
|
|
1239
|
+
} else {
|
|
1240
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
1241
|
+
}
|
|
1242
|
+
process.exit(1);
|
|
1243
|
+
}
|
|
1244
|
+
spinner.stop();
|
|
1245
|
+
const email = result.data;
|
|
1246
|
+
if (options.json) {
|
|
1247
|
+
console.log(JSON.stringify(email, null, 2));
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
console.log();
|
|
1251
|
+
console.log(pc9.bold(email.subject || "(No subject)"));
|
|
1252
|
+
console.log();
|
|
1253
|
+
console.log(`${pc9.dim("From:")} ${formatAddress2(email.from)}`);
|
|
1254
|
+
console.log(`${pc9.dim("To:")} ${email.to.map(formatAddress2).join(", ")}`);
|
|
1255
|
+
console.log(`${pc9.dim("Date:")} ${formatFullDate2(email.date)}`);
|
|
1256
|
+
console.log(`${pc9.dim("ID:")} ${email.id}`);
|
|
1257
|
+
if (email.labels.length > 0) {
|
|
1258
|
+
console.log(`${pc9.dim("Labels:")} ${email.labels.join(", ")}`);
|
|
1259
|
+
}
|
|
1260
|
+
console.log();
|
|
1261
|
+
console.log(pc9.dim("-".repeat(60)));
|
|
1262
|
+
console.log();
|
|
1263
|
+
const content = email.bodyText || email.body || email.snippet;
|
|
1264
|
+
if (content) {
|
|
1265
|
+
const displayContent = email.bodyText || stripHtml(email.body || email.snippet);
|
|
1266
|
+
console.log(displayContent);
|
|
1267
|
+
} else {
|
|
1268
|
+
console.log(pc9.dim("(No content)"));
|
|
1269
|
+
}
|
|
1270
|
+
console.log();
|
|
1271
|
+
}
|
|
1272
|
+
function stripHtml(html) {
|
|
1273
|
+
return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<\/div>/gi, "\n").replace(/<[^>]*>/g, "").replace(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').trim();
|
|
1274
|
+
}
|
|
1275
|
+
function formatAddress2(addr) {
|
|
1276
|
+
if (addr.name) {
|
|
1277
|
+
return `${addr.name} <${addr.email}>`;
|
|
1278
|
+
}
|
|
1279
|
+
return addr.email;
|
|
1280
|
+
}
|
|
1281
|
+
function formatFullDate2(dateString) {
|
|
1282
|
+
const date = new Date(dateString);
|
|
1283
|
+
return date.toLocaleString("en-US", {
|
|
1284
|
+
weekday: "short",
|
|
1285
|
+
year: "numeric",
|
|
1286
|
+
month: "short",
|
|
1287
|
+
day: "numeric",
|
|
1288
|
+
hour: "numeric",
|
|
1289
|
+
minute: "2-digit"
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// src/commands/search.ts
|
|
1294
|
+
import ora6 from "ora";
|
|
1295
|
+
import pc10 from "picocolors";
|
|
1296
|
+
async function searchCommand(query, options) {
|
|
1297
|
+
if (!isAuthenticated()) {
|
|
1298
|
+
console.log(pc10.yellow("Not logged in."));
|
|
1299
|
+
console.log(pc10.dim("Run `btcemail login` to authenticate."));
|
|
1300
|
+
process.exit(1);
|
|
1301
|
+
}
|
|
1302
|
+
if (!query || query.trim().length === 0) {
|
|
1303
|
+
console.error(pc10.red("Error: Search query is required"));
|
|
521
1304
|
process.exit(1);
|
|
522
1305
|
}
|
|
523
|
-
const
|
|
1306
|
+
const spinner = ora6(`Searching for "${query}"...`).start();
|
|
1307
|
+
const result = await getEmails({
|
|
1308
|
+
search: query.trim(),
|
|
1309
|
+
limit: options.limit || 20
|
|
1310
|
+
});
|
|
524
1311
|
if (!result.ok) {
|
|
525
|
-
|
|
1312
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
526
1313
|
process.exit(1);
|
|
527
1314
|
}
|
|
528
|
-
const {
|
|
1315
|
+
const { data: emails, pagination } = result.data;
|
|
1316
|
+
spinner.stop();
|
|
1317
|
+
const cachedEmails = emails.map((e) => ({
|
|
1318
|
+
id: e.id,
|
|
1319
|
+
subject: e.subject,
|
|
1320
|
+
from: e.from.name || e.from.email
|
|
1321
|
+
}));
|
|
1322
|
+
cacheEmailList("search", cachedEmails);
|
|
1323
|
+
if (options.json) {
|
|
1324
|
+
console.log(JSON.stringify({ query, emails, pagination }, null, 2));
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
529
1327
|
console.log();
|
|
530
|
-
console.log(
|
|
1328
|
+
console.log(pc10.bold(`Search: "${query}" (${pagination.total} found)`));
|
|
531
1329
|
console.log();
|
|
532
|
-
|
|
1330
|
+
if (emails.length === 0) {
|
|
1331
|
+
console.log(pc10.dim(" No emails found matching your search."));
|
|
1332
|
+
console.log();
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
console.log(
|
|
1336
|
+
` ${pc10.dim("#".padEnd(4))} ${pc10.dim("From".padEnd(25))} ${pc10.dim("Subject".padEnd(45))} ${pc10.dim("Date")}`
|
|
1337
|
+
);
|
|
1338
|
+
console.log(pc10.dim(` ${"-".repeat(85)}`));
|
|
1339
|
+
emails.forEach((email, index) => {
|
|
1340
|
+
const num = pc10.cyan(String(index + 1).padEnd(4));
|
|
1341
|
+
const unreadMarker = email.unread ? pc10.cyan("\u25CF") : " ";
|
|
1342
|
+
const from = truncate3(email.from.name || email.from.email, 23).padEnd(25);
|
|
1343
|
+
const subject = truncate3(email.subject, 43).padEnd(45);
|
|
1344
|
+
const date = formatDate4(email.date);
|
|
1345
|
+
console.log(`${unreadMarker} ${num}${from} ${subject} ${pc10.dim(date)}`);
|
|
1346
|
+
});
|
|
533
1347
|
console.log();
|
|
534
|
-
if (
|
|
535
|
-
console.log(
|
|
536
|
-
|
|
1348
|
+
if (pagination.hasMore) {
|
|
1349
|
+
console.log(
|
|
1350
|
+
pc10.dim(` Showing ${emails.length} of ${pagination.total}. Use --limit to see more.`)
|
|
1351
|
+
);
|
|
537
1352
|
console.log();
|
|
538
1353
|
}
|
|
1354
|
+
console.log(pc10.dim(" Read email: btcemail read <#> or btcemail read <id>"));
|
|
1355
|
+
console.log();
|
|
539
1356
|
}
|
|
540
|
-
function
|
|
541
|
-
|
|
1357
|
+
function truncate3(str, maxLength) {
|
|
1358
|
+
if (str.length <= maxLength) return str;
|
|
1359
|
+
return `${str.slice(0, maxLength - 1)}\u2026`;
|
|
1360
|
+
}
|
|
1361
|
+
function formatDate4(dateString) {
|
|
1362
|
+
const date = new Date(dateString);
|
|
1363
|
+
const now = /* @__PURE__ */ new Date();
|
|
1364
|
+
const diffMs = now.getTime() - date.getTime();
|
|
1365
|
+
const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
|
|
1366
|
+
if (diffDays === 0) {
|
|
1367
|
+
return date.toLocaleTimeString("en-US", {
|
|
1368
|
+
hour: "numeric",
|
|
1369
|
+
minute: "2-digit"
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
if (diffDays < 7) {
|
|
1373
|
+
return date.toLocaleDateString("en-US", { weekday: "short" });
|
|
1374
|
+
}
|
|
1375
|
+
return date.toLocaleDateString("en-US", {
|
|
1376
|
+
month: "short",
|
|
1377
|
+
day: "numeric"
|
|
1378
|
+
});
|
|
542
1379
|
}
|
|
543
1380
|
|
|
544
1381
|
// src/commands/send.ts
|
|
545
|
-
import
|
|
1382
|
+
import ora7 from "ora";
|
|
1383
|
+
import pc11 from "picocolors";
|
|
1384
|
+
import qrcode2 from "qrcode-terminal";
|
|
546
1385
|
async function sendCommand(options) {
|
|
547
1386
|
if (!isAuthenticated()) {
|
|
548
|
-
console.log(
|
|
549
|
-
console.log(
|
|
1387
|
+
console.log(pc11.yellow("Not logged in."));
|
|
1388
|
+
console.log(pc11.dim("Run `btcemail login` to authenticate."));
|
|
550
1389
|
process.exit(1);
|
|
551
1390
|
}
|
|
552
1391
|
let fromEmail = options.fromEmail;
|
|
553
1392
|
if (!fromEmail) {
|
|
1393
|
+
const spinner2 = ora7("Fetching account info...").start();
|
|
554
1394
|
const whoamiResult = await getWhoami();
|
|
1395
|
+
spinner2.stop();
|
|
555
1396
|
if (whoamiResult.ok && whoamiResult.data.username) {
|
|
556
1397
|
fromEmail = `${whoamiResult.data.username}@btc.email`;
|
|
557
1398
|
} else {
|
|
@@ -562,52 +1403,102 @@ async function sendCommand(options) {
|
|
|
562
1403
|
}
|
|
563
1404
|
}
|
|
564
1405
|
if (!fromEmail || !fromEmail.endsWith("@btc.email")) {
|
|
565
|
-
console.error(
|
|
566
|
-
console.log(
|
|
1406
|
+
console.error(pc11.red("No @btc.email address found."));
|
|
1407
|
+
console.log(pc11.dim("Register a username at https://btc.email"));
|
|
567
1408
|
process.exit(1);
|
|
568
1409
|
}
|
|
569
1410
|
if (!options.to) {
|
|
570
|
-
console.error(
|
|
1411
|
+
console.error(pc11.red("Recipient is required. Use --to <email>"));
|
|
571
1412
|
process.exit(1);
|
|
572
1413
|
}
|
|
573
1414
|
if (!options.subject) {
|
|
574
|
-
console.error(
|
|
1415
|
+
console.error(pc11.red("Subject is required. Use --subject <text>"));
|
|
575
1416
|
process.exit(1);
|
|
576
1417
|
}
|
|
577
1418
|
if (!options.body) {
|
|
578
|
-
console.error(
|
|
1419
|
+
console.error(pc11.red("Body is required. Use --body <text>"));
|
|
579
1420
|
process.exit(1);
|
|
580
1421
|
}
|
|
581
1422
|
const toEmails = options.to.split(",").map((e) => e.trim());
|
|
582
1423
|
console.log();
|
|
583
|
-
console.log(
|
|
1424
|
+
console.log(pc11.bold("Sending Email"));
|
|
584
1425
|
console.log();
|
|
585
|
-
console.log(`${
|
|
586
|
-
console.log(`${
|
|
587
|
-
console.log(`${
|
|
1426
|
+
console.log(`${pc11.dim("From:")} ${fromEmail}`);
|
|
1427
|
+
console.log(`${pc11.dim("To:")} ${toEmails.join(", ")}`);
|
|
1428
|
+
console.log(`${pc11.dim("Subject:")} ${options.subject}`);
|
|
588
1429
|
console.log();
|
|
589
1430
|
if (!options.paymentHash) {
|
|
1431
|
+
const spinner2 = ora7("Getting invoice...").start();
|
|
590
1432
|
const invoiceResult = await getL402Invoice({
|
|
591
1433
|
fromEmail,
|
|
592
1434
|
toEmails
|
|
593
1435
|
});
|
|
1436
|
+
spinner2.stop();
|
|
594
1437
|
if (invoiceResult.ok && invoiceResult.data.amountSats > 0) {
|
|
595
1438
|
const invoice = invoiceResult.data;
|
|
596
|
-
console.log(
|
|
1439
|
+
console.log(pc11.yellow(`Payment required: ${invoice.amountSats} sats`));
|
|
1440
|
+
console.log();
|
|
1441
|
+
console.log(pc11.bold("Scan to pay:"));
|
|
597
1442
|
console.log();
|
|
598
|
-
|
|
599
|
-
|
|
1443
|
+
qrcode2.generate(invoice.invoice, { small: true }, (qr) => {
|
|
1444
|
+
const indentedQr = qr.split("\n").map((line) => ` ${line}`).join("\n");
|
|
1445
|
+
console.log(indentedQr);
|
|
1446
|
+
});
|
|
600
1447
|
console.log();
|
|
601
|
-
console.log(
|
|
1448
|
+
console.log(pc11.dim("Lightning Invoice:"));
|
|
1449
|
+
console.log(pc11.cyan(invoice.invoice));
|
|
1450
|
+
console.log();
|
|
1451
|
+
if (options.wait) {
|
|
1452
|
+
const status = await pollForPayment({
|
|
1453
|
+
paymentHash: invoice.paymentHash,
|
|
1454
|
+
checkFn: async (hash) => {
|
|
1455
|
+
const result2 = await checkPaymentStatus(hash);
|
|
1456
|
+
if (result2.ok) {
|
|
1457
|
+
return { paid: result2.data.paid };
|
|
1458
|
+
}
|
|
1459
|
+
return { paid: false };
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
if (status.paid) {
|
|
1463
|
+
const sendSpinner = ora7("Sending email...").start();
|
|
1464
|
+
const result2 = await sendEmail({
|
|
1465
|
+
to: toEmails,
|
|
1466
|
+
subject: options.subject,
|
|
1467
|
+
body: options.body,
|
|
1468
|
+
fromEmail,
|
|
1469
|
+
paymentHash: invoice.paymentHash
|
|
1470
|
+
});
|
|
1471
|
+
if (!result2.ok) {
|
|
1472
|
+
sendSpinner.fail(`Error: ${result2.error.message}`);
|
|
1473
|
+
process.exit(1);
|
|
1474
|
+
}
|
|
1475
|
+
sendSpinner.succeed("Email sent!");
|
|
1476
|
+
console.log();
|
|
1477
|
+
console.log(`${pc11.dim("Email ID:")} ${result2.data.emailId}`);
|
|
1478
|
+
if (result2.data.messageId) {
|
|
1479
|
+
console.log(`${pc11.dim("Message ID:")} ${result2.data.messageId}`);
|
|
1480
|
+
}
|
|
1481
|
+
console.log();
|
|
1482
|
+
} else {
|
|
1483
|
+
console.log(
|
|
1484
|
+
pc11.dim("Payment not confirmed. You can still pay and resend with --payment-hash.")
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
console.log(pc11.dim("Pay this invoice with your Lightning wallet, then run:"));
|
|
602
1490
|
console.log(
|
|
603
|
-
|
|
1491
|
+
pc11.cyan(
|
|
604
1492
|
` btcemail send --to "${options.to}" --subject "${options.subject}" --body "${options.body}" --payment-hash ${invoice.paymentHash}`
|
|
605
1493
|
)
|
|
606
1494
|
);
|
|
607
1495
|
console.log();
|
|
1496
|
+
console.log(pc11.dim("Or use --wait to automatically wait for payment."));
|
|
1497
|
+
console.log();
|
|
608
1498
|
return;
|
|
609
1499
|
}
|
|
610
1500
|
}
|
|
1501
|
+
const spinner = ora7("Sending email...").start();
|
|
611
1502
|
const result = await sendEmail({
|
|
612
1503
|
to: toEmails,
|
|
613
1504
|
subject: options.subject,
|
|
@@ -617,201 +1508,357 @@ async function sendCommand(options) {
|
|
|
617
1508
|
});
|
|
618
1509
|
if (!result.ok) {
|
|
619
1510
|
if (result.error.code === "PAYMENT_REQUIRED") {
|
|
620
|
-
|
|
621
|
-
console.log(
|
|
1511
|
+
spinner.fail("Payment required to send this email.");
|
|
1512
|
+
console.log(pc11.dim("Use the invoice above to pay, then include --payment-hash"));
|
|
622
1513
|
} else {
|
|
623
|
-
|
|
1514
|
+
spinner.fail(`Error: ${result.error.message}`);
|
|
624
1515
|
}
|
|
625
1516
|
process.exit(1);
|
|
626
1517
|
}
|
|
627
|
-
|
|
1518
|
+
spinner.succeed("Email sent!");
|
|
628
1519
|
console.log();
|
|
629
|
-
console.log(`${
|
|
1520
|
+
console.log(`${pc11.dim("Email ID:")} ${result.data.emailId}`);
|
|
630
1521
|
if (result.data.messageId) {
|
|
631
|
-
console.log(`${
|
|
1522
|
+
console.log(`${pc11.dim("Message ID:")} ${result.data.messageId}`);
|
|
632
1523
|
}
|
|
633
1524
|
console.log();
|
|
634
1525
|
}
|
|
635
1526
|
|
|
636
|
-
// src/commands/
|
|
637
|
-
import
|
|
638
|
-
async function
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
console.
|
|
1527
|
+
// src/commands/settings.ts
|
|
1528
|
+
import pc12 from "picocolors";
|
|
1529
|
+
async function settingsShowCommand(options = {}) {
|
|
1530
|
+
const token = getToken();
|
|
1531
|
+
if (!token) {
|
|
1532
|
+
console.error(pc12.red("Not logged in. Run 'btcemail login' first."));
|
|
642
1533
|
process.exit(1);
|
|
643
1534
|
}
|
|
644
|
-
const result = await
|
|
645
|
-
limit: options.limit || 20
|
|
646
|
-
});
|
|
1535
|
+
const result = await getSettings(options.username);
|
|
647
1536
|
if (!result.ok) {
|
|
648
|
-
console.error(
|
|
1537
|
+
console.error(pc12.red(`Error: ${result.error.message}`));
|
|
649
1538
|
process.exit(1);
|
|
650
1539
|
}
|
|
651
|
-
const {
|
|
1540
|
+
const { username, usernames, settings: settings2 } = result.data;
|
|
652
1541
|
if (options.json) {
|
|
653
|
-
console.log(JSON.stringify({
|
|
1542
|
+
console.log(JSON.stringify({ username, usernames, settings: settings2 }, null, 2));
|
|
654
1543
|
return;
|
|
655
1544
|
}
|
|
656
1545
|
console.log();
|
|
657
|
-
console.log(
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
console.log(pc8.dim(" No pending emails."));
|
|
661
|
-
console.log();
|
|
662
|
-
return;
|
|
1546
|
+
console.log(pc12.bold("Settings for ") + pc12.cyan(`${username}@btc.email`));
|
|
1547
|
+
if (usernames.length > 1) {
|
|
1548
|
+
console.log(pc12.dim(`(You have ${usernames.length} addresses: ${usernames.join(", ")})`));
|
|
663
1549
|
}
|
|
1550
|
+
console.log();
|
|
1551
|
+
console.log(pc12.dim("Pricing:"));
|
|
1552
|
+
console.log(` Email price: ${pc12.yellow(settings2.default_price_sats.toLocaleString())} sats`);
|
|
1553
|
+
console.log();
|
|
1554
|
+
console.log(pc12.dim("Toggles:"));
|
|
664
1555
|
console.log(
|
|
665
|
-
`
|
|
1556
|
+
` Require payment: ${settings2.require_payment_from_unknown ? pc12.green("on") : pc12.red("off")}`
|
|
1557
|
+
);
|
|
1558
|
+
console.log(
|
|
1559
|
+
` Auto-whitelist: ${settings2.auto_whitelist_on_payment ? pc12.green("on") : pc12.red("off")}`
|
|
666
1560
|
);
|
|
667
|
-
console.log(pc8.dim(" " + "-".repeat(85)));
|
|
668
|
-
for (const email of emails) {
|
|
669
|
-
const id = email.id.slice(0, 10).padEnd(12);
|
|
670
|
-
const from = truncate2(email.from.name || email.from.email, 28).padEnd(30);
|
|
671
|
-
const subject = truncate2(email.subject, 28).padEnd(30);
|
|
672
|
-
const sats = pc8.yellow(String(email.amountSats));
|
|
673
|
-
console.log(` ${id} ${from} ${subject} ${sats}`);
|
|
674
|
-
}
|
|
675
1561
|
console.log();
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
console.log();
|
|
1562
|
+
console.log(pc12.dim("Auto-reply:"));
|
|
1563
|
+
if (settings2.custom_auto_reply_body) {
|
|
1564
|
+
console.log(` ${pc12.italic(settings2.custom_auto_reply_body)}`);
|
|
1565
|
+
} else {
|
|
1566
|
+
console.log(` ${pc12.dim("(default message)")}`);
|
|
681
1567
|
}
|
|
682
|
-
console.log(pc8.dim(" Use `btcemail inbound view <id>` to see details and payment invoice."));
|
|
683
1568
|
console.log();
|
|
684
1569
|
}
|
|
685
|
-
async function
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
console.
|
|
1570
|
+
async function settingsPricingCommand(sats, options = {}) {
|
|
1571
|
+
const token = getToken();
|
|
1572
|
+
if (!token) {
|
|
1573
|
+
console.error(pc12.red("Not logged in. Run 'btcemail login' first."));
|
|
689
1574
|
process.exit(1);
|
|
690
1575
|
}
|
|
691
|
-
const
|
|
1576
|
+
const price = parseInt(sats, 10);
|
|
1577
|
+
if (Number.isNaN(price) || price < 0) {
|
|
1578
|
+
console.error(pc12.red("Invalid price. Must be a non-negative number."));
|
|
1579
|
+
process.exit(1);
|
|
1580
|
+
}
|
|
1581
|
+
const result = await updateSettings({ default_price_sats: price }, options.username);
|
|
692
1582
|
if (!result.ok) {
|
|
693
|
-
|
|
694
|
-
console.error(pc8.red(`Pending email not found: ${id}`));
|
|
695
|
-
} else {
|
|
696
|
-
console.error(pc8.red(`Error: ${result.error.message}`));
|
|
697
|
-
}
|
|
1583
|
+
console.error(pc12.red(`Error: ${result.error.message}`));
|
|
698
1584
|
process.exit(1);
|
|
699
1585
|
}
|
|
700
|
-
const email = result.data;
|
|
701
1586
|
if (options.json) {
|
|
702
|
-
console.log(JSON.stringify(
|
|
1587
|
+
console.log(JSON.stringify({ success: true, price }, null, 2));
|
|
703
1588
|
return;
|
|
704
1589
|
}
|
|
705
|
-
console.log();
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
console.
|
|
1590
|
+
console.log(pc12.green(`Email price set to ${pc12.yellow(price.toLocaleString())} sats`));
|
|
1591
|
+
}
|
|
1592
|
+
async function settingsRequirePaymentCommand(value, options = {}) {
|
|
1593
|
+
const token = getToken();
|
|
1594
|
+
if (!token) {
|
|
1595
|
+
console.error(pc12.red("Not logged in. Run 'btcemail login' first."));
|
|
1596
|
+
process.exit(1);
|
|
1597
|
+
}
|
|
1598
|
+
const enabled = value.toLowerCase() === "on" || value === "true" || value === "1";
|
|
1599
|
+
const disabled = value.toLowerCase() === "off" || value === "false" || value === "0";
|
|
1600
|
+
if (!enabled && !disabled) {
|
|
1601
|
+
console.error(pc12.red("Invalid value. Use 'on' or 'off'."));
|
|
1602
|
+
process.exit(1);
|
|
1603
|
+
}
|
|
1604
|
+
const result = await updateSettings({ require_payment_from_unknown: enabled }, options.username);
|
|
1605
|
+
if (!result.ok) {
|
|
1606
|
+
console.error(pc12.red(`Error: ${result.error.message}`));
|
|
1607
|
+
process.exit(1);
|
|
1608
|
+
}
|
|
1609
|
+
console.log(pc12.green(`Require payment: ${enabled ? pc12.green("on") : pc12.red("off")}`));
|
|
1610
|
+
}
|
|
1611
|
+
async function settingsAutoWhitelistCommand(value, options = {}) {
|
|
1612
|
+
const token = getToken();
|
|
1613
|
+
if (!token) {
|
|
1614
|
+
console.error(pc12.red("Not logged in. Run 'btcemail login' first."));
|
|
1615
|
+
process.exit(1);
|
|
1616
|
+
}
|
|
1617
|
+
const enabled = value.toLowerCase() === "on" || value === "true" || value === "1";
|
|
1618
|
+
const disabled = value.toLowerCase() === "off" || value === "false" || value === "0";
|
|
1619
|
+
if (!enabled && !disabled) {
|
|
1620
|
+
console.error(pc12.red("Invalid value. Use 'on' or 'off'."));
|
|
1621
|
+
process.exit(1);
|
|
1622
|
+
}
|
|
1623
|
+
const result = await updateSettings({ auto_whitelist_on_payment: enabled }, options.username);
|
|
1624
|
+
if (!result.ok) {
|
|
1625
|
+
console.error(pc12.red(`Error: ${result.error.message}`));
|
|
1626
|
+
process.exit(1);
|
|
1627
|
+
}
|
|
1628
|
+
console.log(pc12.green(`Auto-whitelist: ${enabled ? pc12.green("on") : pc12.red("off")}`));
|
|
1629
|
+
}
|
|
1630
|
+
async function settingsAutoReplyCommand(message, options = {}) {
|
|
1631
|
+
const token = getToken();
|
|
1632
|
+
if (!token) {
|
|
1633
|
+
console.error(pc12.red("Not logged in. Run 'btcemail login' first."));
|
|
1634
|
+
process.exit(1);
|
|
1635
|
+
}
|
|
1636
|
+
const newMessage = options.clear ? null : message || null;
|
|
1637
|
+
const result = await updateSettings({ custom_auto_reply_body: newMessage }, options.username);
|
|
1638
|
+
if (!result.ok) {
|
|
1639
|
+
console.error(pc12.red(`Error: ${result.error.message}`));
|
|
1640
|
+
process.exit(1);
|
|
1641
|
+
}
|
|
1642
|
+
if (options.clear) {
|
|
1643
|
+
console.log(pc12.green("Auto-reply reset to default message"));
|
|
1644
|
+
} else if (newMessage) {
|
|
1645
|
+
console.log(pc12.green("Auto-reply message updated"));
|
|
717
1646
|
} else {
|
|
718
|
-
console.log(
|
|
1647
|
+
console.log(pc12.yellow("No message provided. Use --clear to reset to default."));
|
|
719
1648
|
}
|
|
720
|
-
console.log();
|
|
721
|
-
console.log(pc8.dim("-".repeat(60)));
|
|
722
|
-
console.log();
|
|
723
|
-
console.log(pc8.dim("Preview (full content available after payment):"));
|
|
724
|
-
console.log();
|
|
725
|
-
console.log(truncate2(email.bodyText || email.body, 200));
|
|
726
|
-
console.log();
|
|
727
1649
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
1650
|
+
|
|
1651
|
+
// src/commands/stats.ts
|
|
1652
|
+
import pc13 from "picocolors";
|
|
1653
|
+
async function statsCommand(options = {}) {
|
|
1654
|
+
const token = getToken();
|
|
1655
|
+
if (!token) {
|
|
1656
|
+
console.error(pc13.red("Not logged in. Run 'btcemail login' first."));
|
|
732
1657
|
process.exit(1);
|
|
733
1658
|
}
|
|
734
|
-
|
|
735
|
-
|
|
1659
|
+
const result = await getStats();
|
|
1660
|
+
if (!result.ok) {
|
|
1661
|
+
console.error(pc13.red(`Error: ${result.error.message}`));
|
|
736
1662
|
process.exit(1);
|
|
737
1663
|
}
|
|
1664
|
+
const { totalReceived, totalSent, pendingCount, addressCount, usernames } = result.data;
|
|
1665
|
+
if (options.json) {
|
|
1666
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
738
1669
|
console.log();
|
|
739
|
-
console.log(
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1670
|
+
console.log(pc13.bold("Dashboard Statistics"));
|
|
1671
|
+
console.log();
|
|
1672
|
+
console.log(` Total Received: ${pc13.green(`+${totalReceived.toLocaleString()}`)} sats`);
|
|
1673
|
+
console.log(` Total Sent: ${pc13.yellow(`-${totalSent.toLocaleString()}`)} sats`);
|
|
1674
|
+
console.log(` Pending Emails: ${pendingCount > 0 ? pc13.yellow(pendingCount) : pc13.dim("0")}`);
|
|
1675
|
+
console.log(` Active Addresses: ${addressCount}`);
|
|
1676
|
+
console.log();
|
|
1677
|
+
if (usernames.length > 0) {
|
|
1678
|
+
console.log(pc13.dim(" Your addresses:"));
|
|
1679
|
+
for (const username of usernames) {
|
|
1680
|
+
console.log(` ${pc13.cyan(username)}@btc.email`);
|
|
749
1681
|
}
|
|
1682
|
+
console.log();
|
|
1683
|
+
}
|
|
1684
|
+
if (pendingCount > 0) {
|
|
1685
|
+
console.log(pc13.dim(" View pending emails: btcemail inbound pending"));
|
|
1686
|
+
console.log();
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// src/commands/whitelist.ts
|
|
1691
|
+
import pc14 from "picocolors";
|
|
1692
|
+
async function whitelistListCommand(options = {}) {
|
|
1693
|
+
const token = getToken();
|
|
1694
|
+
if (!token) {
|
|
1695
|
+
console.error(pc14.red("Not logged in. Run 'btcemail login' first."));
|
|
750
1696
|
process.exit(1);
|
|
751
1697
|
}
|
|
1698
|
+
const result = await getWhitelist(options.username);
|
|
1699
|
+
if (!result.ok) {
|
|
1700
|
+
console.error(pc14.red(`Error: ${result.error.message}`));
|
|
1701
|
+
process.exit(1);
|
|
1702
|
+
}
|
|
1703
|
+
const { username, entries } = result.data;
|
|
1704
|
+
if (options.json) {
|
|
1705
|
+
console.log(JSON.stringify({ username, entries }, null, 2));
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
752
1708
|
console.log();
|
|
753
|
-
console.log(
|
|
1709
|
+
console.log(pc14.bold("Whitelist for ") + pc14.cyan(`${username}@btc.email`));
|
|
754
1710
|
console.log();
|
|
755
|
-
|
|
756
|
-
|
|
1711
|
+
if (entries.length === 0) {
|
|
1712
|
+
console.log(pc14.dim(" No entries in whitelist."));
|
|
1713
|
+
console.log();
|
|
1714
|
+
console.log(pc14.dim(" Add entries with: btcemail whitelist add <email>"));
|
|
1715
|
+
console.log(pc14.dim(" Or for domains: btcemail whitelist add @domain.com"));
|
|
1716
|
+
console.log();
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
console.log(pc14.dim(` ${entries.length} ${entries.length === 1 ? "entry" : "entries"}:`));
|
|
757
1720
|
console.log();
|
|
758
|
-
|
|
1721
|
+
for (const entry of entries) {
|
|
1722
|
+
const value = entry.sender_email || `@${entry.sender_domain}`;
|
|
1723
|
+
const via = entry.added_via !== "manual" ? pc14.dim(` (via ${entry.added_via})`) : "";
|
|
1724
|
+
const note = entry.note ? pc14.dim(` - ${entry.note}`) : "";
|
|
1725
|
+
console.log(` ${pc14.green(value)}${via}${note}`);
|
|
1726
|
+
console.log(pc14.dim(` ID: ${entry.id}`));
|
|
1727
|
+
}
|
|
759
1728
|
console.log();
|
|
760
1729
|
}
|
|
761
|
-
function
|
|
762
|
-
|
|
763
|
-
|
|
1730
|
+
async function whitelistAddCommand(entry, options = {}) {
|
|
1731
|
+
const token = getToken();
|
|
1732
|
+
if (!token) {
|
|
1733
|
+
console.error(pc14.red("Not logged in. Run 'btcemail login' first."));
|
|
1734
|
+
process.exit(1);
|
|
1735
|
+
}
|
|
1736
|
+
const isDomain = entry.startsWith("@");
|
|
1737
|
+
const payload = isDomain ? { domain: entry.slice(1), note: options.note } : { email: entry, note: options.note };
|
|
1738
|
+
const result = await addToWhitelist(payload, options.username);
|
|
1739
|
+
if (!result.ok) {
|
|
1740
|
+
console.error(pc14.red(`Error: ${result.error.message}`));
|
|
1741
|
+
process.exit(1);
|
|
1742
|
+
}
|
|
1743
|
+
if (options.json) {
|
|
1744
|
+
console.log(JSON.stringify({ success: true, entry: result.data.entry }, null, 2));
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
console.log(pc14.green(`Added to whitelist: ${pc14.cyan(entry)}`));
|
|
764
1748
|
}
|
|
765
|
-
function
|
|
766
|
-
|
|
767
|
-
|
|
1749
|
+
async function whitelistRemoveCommand(id, options = {}) {
|
|
1750
|
+
const token = getToken();
|
|
1751
|
+
if (!token) {
|
|
1752
|
+
console.error(pc14.red("Not logged in. Run 'btcemail login' first."));
|
|
1753
|
+
process.exit(1);
|
|
768
1754
|
}
|
|
769
|
-
|
|
1755
|
+
const result = await removeFromWhitelist(id);
|
|
1756
|
+
if (!result.ok) {
|
|
1757
|
+
console.error(pc14.red(`Error: ${result.error.message}`));
|
|
1758
|
+
process.exit(1);
|
|
1759
|
+
}
|
|
1760
|
+
if (options.json) {
|
|
1761
|
+
console.log(JSON.stringify({ success: true }, null, 2));
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
console.log(pc14.green("Removed from whitelist"));
|
|
770
1765
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
}
|
|
1766
|
+
|
|
1767
|
+
// src/commands/whoami.ts
|
|
1768
|
+
import ora8 from "ora";
|
|
1769
|
+
import pc15 from "picocolors";
|
|
1770
|
+
async function whoamiCommand() {
|
|
1771
|
+
if (!isAuthenticated()) {
|
|
1772
|
+
console.log(pc15.yellow("Not logged in."));
|
|
1773
|
+
console.log(pc15.dim("Run `btcemail login` to authenticate."));
|
|
1774
|
+
process.exit(1);
|
|
1775
|
+
}
|
|
1776
|
+
const auth = getAuth();
|
|
1777
|
+
if (!auth) {
|
|
1778
|
+
console.log(pc15.yellow("Not logged in."));
|
|
1779
|
+
process.exit(1);
|
|
1780
|
+
}
|
|
1781
|
+
const spinner = ora8("Fetching account info...").start();
|
|
1782
|
+
const result = await getWhoami();
|
|
1783
|
+
spinner.stop();
|
|
1784
|
+
if (result.ok) {
|
|
1785
|
+
console.log();
|
|
1786
|
+
console.log(pc15.bold("Current User"));
|
|
1787
|
+
console.log();
|
|
1788
|
+
console.log(` ${pc15.dim("Email:")} ${result.data.email}`);
|
|
1789
|
+
if (result.data.username) {
|
|
1790
|
+
console.log(` ${pc15.dim("Username:")} ${result.data.username}@btc.email`);
|
|
1791
|
+
}
|
|
1792
|
+
console.log(` ${pc15.dim("User ID:")} ${result.data.id}`);
|
|
1793
|
+
const expiryInfo = getTokenExpiryInfo();
|
|
1794
|
+
if (expiryInfo.expiresIn) {
|
|
1795
|
+
const expiryColor = isTokenExpiringSoon() ? pc15.yellow : pc15.dim;
|
|
1796
|
+
console.log(` ${pc15.dim("Session:")} ${expiryColor(`expires in ${expiryInfo.expiresIn}`)}`);
|
|
1797
|
+
}
|
|
1798
|
+
console.log();
|
|
1799
|
+
if (isTokenExpiringSoon()) {
|
|
1800
|
+
console.log(pc15.yellow("Session expiring soon. Run `btcemail login` to refresh."));
|
|
1801
|
+
console.log();
|
|
1802
|
+
}
|
|
1803
|
+
} else {
|
|
1804
|
+
console.log();
|
|
1805
|
+
console.log(pc15.bold("Current User") + pc15.dim(" (cached)"));
|
|
1806
|
+
console.log();
|
|
1807
|
+
console.log(` ${pc15.dim("Email:")} ${auth.email}`);
|
|
1808
|
+
console.log(` ${pc15.dim("User ID:")} ${auth.userId}`);
|
|
1809
|
+
console.log();
|
|
1810
|
+
if (result.error.code === "UNAUTHENTICATED" || result.error.code === "UNAUTHORIZED") {
|
|
1811
|
+
console.log(pc15.yellow("Session expired. Run `btcemail login` to re-authenticate."));
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
781
1814
|
}
|
|
782
1815
|
|
|
783
1816
|
// src/index.ts
|
|
784
1817
|
var program = new Command();
|
|
785
|
-
program.name("btcemail").description("btc.email CLI - Spam-free email powered by Bitcoin Lightning").version("0.
|
|
1818
|
+
program.name("btcemail").description("btc.email CLI - Spam-free email powered by Bitcoin Lightning").version("0.4.0");
|
|
786
1819
|
program.command("login").description("Authenticate with btc.email (opens browser)").action(loginCommand);
|
|
787
1820
|
program.command("logout").description("Clear stored credentials").action(logoutCommand);
|
|
788
1821
|
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) => {
|
|
1822
|
+
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
1823
|
await listCommand({
|
|
791
1824
|
folder: options.folder,
|
|
792
1825
|
limit: parseInt(options.limit, 10),
|
|
1826
|
+
page: parseInt(options.page, 10),
|
|
793
1827
|
json: options.json
|
|
794
1828
|
});
|
|
795
1829
|
});
|
|
796
|
-
program.command("read <id>").description("Read an email by ID").option("--json", "Output as JSON").action(async (id, options) => {
|
|
1830
|
+
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
1831
|
await readCommand(id, { json: options.json });
|
|
798
1832
|
});
|
|
799
|
-
program.command("
|
|
1833
|
+
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) => {
|
|
1834
|
+
await searchCommand(query, {
|
|
1835
|
+
limit: parseInt(options.limit, 10),
|
|
1836
|
+
json: options.json
|
|
1837
|
+
});
|
|
1838
|
+
});
|
|
1839
|
+
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
1840
|
await sendCommand({
|
|
801
1841
|
to: options.to,
|
|
802
1842
|
subject: options.subject,
|
|
803
1843
|
body: options.body,
|
|
804
1844
|
fromEmail: options.from,
|
|
805
|
-
paymentHash: options.paymentHash
|
|
1845
|
+
paymentHash: options.paymentHash,
|
|
1846
|
+
wait: options.wait
|
|
806
1847
|
});
|
|
807
1848
|
});
|
|
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) => {
|
|
1849
|
+
var inbound = program.command("inbound").description("Manage inbound emails (pending and delivered)");
|
|
1850
|
+
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
1851
|
await inboundPendingCommand({
|
|
811
1852
|
limit: parseInt(options.limit, 10),
|
|
812
1853
|
json: options.json
|
|
813
1854
|
});
|
|
814
1855
|
});
|
|
1856
|
+
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) => {
|
|
1857
|
+
await inboundDeliveredCommand({
|
|
1858
|
+
limit: parseInt(options.limit, 10),
|
|
1859
|
+
json: options.json
|
|
1860
|
+
});
|
|
1861
|
+
});
|
|
815
1862
|
inbound.command("view <id>").description("View pending email details and payment info").option("--json", "Output as JSON").action(async (id, options) => {
|
|
816
1863
|
await inboundViewCommand(id, { json: options.json });
|
|
817
1864
|
});
|
|
@@ -819,11 +1866,93 @@ inbound.command("pay <id>").description("Pay for a pending email").requiredOptio
|
|
|
819
1866
|
await inboundPayCommand(id, { paymentHash: options.paymentHash });
|
|
820
1867
|
});
|
|
821
1868
|
var credits = program.command("credits").description("Manage credits");
|
|
822
|
-
credits.command("balance").description("Show credit balance").action(creditsBalanceCommand);
|
|
1869
|
+
credits.command("balance").alias("bal").description("Show credit balance").action(creditsBalanceCommand);
|
|
1870
|
+
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) => {
|
|
1871
|
+
await creditsHistoryCommand({
|
|
1872
|
+
limit: parseInt(options.limit, 10),
|
|
1873
|
+
type: options.type,
|
|
1874
|
+
json: options.json
|
|
1875
|
+
});
|
|
1876
|
+
});
|
|
1877
|
+
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) => {
|
|
1878
|
+
await creditsPurchaseCommand({
|
|
1879
|
+
option: options.option !== void 0 ? parseInt(options.option, 10) : void 0,
|
|
1880
|
+
json: options.json,
|
|
1881
|
+
wait: options.wait
|
|
1882
|
+
});
|
|
1883
|
+
});
|
|
1884
|
+
var settings = program.command("settings").description("Manage email pricing and preferences");
|
|
1885
|
+
settings.command("show").description("Show current settings").option("-u, --username <name>", "Specific @btc.email address").option("--json", "Output as JSON").action(async (options) => {
|
|
1886
|
+
await settingsShowCommand({ username: options.username, json: options.json });
|
|
1887
|
+
});
|
|
1888
|
+
settings.command("pricing <sats>").description("Set email price in sats").option("-u, --username <name>", "Specific @btc.email address").option("--json", "Output as JSON").action(async (sats, options) => {
|
|
1889
|
+
await settingsPricingCommand(sats, { username: options.username, json: options.json });
|
|
1890
|
+
});
|
|
1891
|
+
settings.command("require-payment <on|off>").description("Toggle payment requirement for unknown senders").option("-u, --username <name>", "Specific @btc.email address").action(async (value, options) => {
|
|
1892
|
+
await settingsRequirePaymentCommand(value, { username: options.username });
|
|
1893
|
+
});
|
|
1894
|
+
settings.command("auto-whitelist <on|off>").description("Toggle auto-whitelist after payment").option("-u, --username <name>", "Specific @btc.email address").action(async (value, options) => {
|
|
1895
|
+
await settingsAutoWhitelistCommand(value, { username: options.username });
|
|
1896
|
+
});
|
|
1897
|
+
settings.command("auto-reply [message]").description("Set custom auto-reply message").option("-u, --username <name>", "Specific @btc.email address").option("--clear", "Reset to default message").action(async (message, options) => {
|
|
1898
|
+
await settingsAutoReplyCommand(message, { username: options.username, clear: options.clear });
|
|
1899
|
+
});
|
|
1900
|
+
settings.action(async () => {
|
|
1901
|
+
await settingsShowCommand({});
|
|
1902
|
+
});
|
|
1903
|
+
var whitelist = program.command("whitelist").alias("wl").description("Manage senders who can email you for free");
|
|
1904
|
+
whitelist.command("list").description("List whitelist entries").option("-u, --username <name>", "Specific @btc.email address").option("--json", "Output as JSON").action(async (options) => {
|
|
1905
|
+
await whitelistListCommand({ username: options.username, json: options.json });
|
|
1906
|
+
});
|
|
1907
|
+
whitelist.command("add <entry>").description("Add email or @domain to whitelist").option("-u, --username <name>", "Specific @btc.email address").option("-n, --note <note>", "Optional note").option("--json", "Output as JSON").action(async (entry, options) => {
|
|
1908
|
+
await whitelistAddCommand(entry, {
|
|
1909
|
+
username: options.username,
|
|
1910
|
+
note: options.note,
|
|
1911
|
+
json: options.json
|
|
1912
|
+
});
|
|
1913
|
+
});
|
|
1914
|
+
whitelist.command("remove <id>").description("Remove entry from whitelist by ID").option("--json", "Output as JSON").action(async (id, options) => {
|
|
1915
|
+
await whitelistRemoveCommand(id, { json: options.json });
|
|
1916
|
+
});
|
|
1917
|
+
whitelist.action(async () => {
|
|
1918
|
+
await whitelistListCommand({});
|
|
1919
|
+
});
|
|
1920
|
+
var blocklist = program.command("blocklist").alias("bl").description("Manage blocked senders");
|
|
1921
|
+
blocklist.command("list").description("List blocklist entries").option("-u, --username <name>", "Specific @btc.email address").option("--json", "Output as JSON").action(async (options) => {
|
|
1922
|
+
await blocklistListCommand({ username: options.username, json: options.json });
|
|
1923
|
+
});
|
|
1924
|
+
blocklist.command("add <entry>").description("Add email or @domain to blocklist").option("-u, --username <name>", "Specific @btc.email address").option("-r, --reason <reason>", "Optional reason").option("--json", "Output as JSON").action(async (entry, options) => {
|
|
1925
|
+
await blocklistAddCommand(entry, {
|
|
1926
|
+
username: options.username,
|
|
1927
|
+
reason: options.reason,
|
|
1928
|
+
json: options.json
|
|
1929
|
+
});
|
|
1930
|
+
});
|
|
1931
|
+
blocklist.command("remove <id>").description("Remove entry from blocklist by ID").option("--json", "Output as JSON").action(async (id, options) => {
|
|
1932
|
+
await blocklistRemoveCommand(id, { json: options.json });
|
|
1933
|
+
});
|
|
1934
|
+
blocklist.action(async () => {
|
|
1935
|
+
await blocklistListCommand({});
|
|
1936
|
+
});
|
|
1937
|
+
var network = program.command("network").description("Manage Lightning network mode");
|
|
1938
|
+
network.command("show").description("Show current network mode").option("--json", "Output as JSON").action(async (options) => {
|
|
1939
|
+
await networkShowCommand({ json: options.json });
|
|
1940
|
+
});
|
|
1941
|
+
network.command("testnet").description("Switch to testnet (fake Bitcoin)").option("--json", "Output as JSON").action(async (options) => {
|
|
1942
|
+
await networkSwitchCommand("testnet", { json: options.json });
|
|
1943
|
+
});
|
|
1944
|
+
network.command("mainnet").description("Switch to mainnet (real Bitcoin)").option("--json", "Output as JSON").action(async (options) => {
|
|
1945
|
+
await networkSwitchCommand("mainnet", { json: options.json });
|
|
1946
|
+
});
|
|
1947
|
+
network.action(async () => {
|
|
1948
|
+
await networkShowCommand({});
|
|
1949
|
+
});
|
|
1950
|
+
program.command("stats").description("Show dashboard statistics").option("--json", "Output as JSON").action(async (options) => {
|
|
1951
|
+
await statsCommand({ json: options.json });
|
|
1952
|
+
});
|
|
823
1953
|
program.action(() => {
|
|
824
1954
|
console.log();
|
|
825
|
-
console.log(
|
|
826
|
-
console.log(pc9.dim("Spam-free email powered by Bitcoin Lightning"));
|
|
1955
|
+
console.log(pc16.bold("btc.email CLI") + pc16.dim(" - Spam-free email powered by Bitcoin Lightning"));
|
|
827
1956
|
console.log();
|
|
828
1957
|
program.outputHelp();
|
|
829
1958
|
});
|