shellmail 1.0.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/dist/api.d.ts +90 -0
- package/dist/api.js +90 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +45 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +433 -0
- package/package.json +45 -0
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShellMail API Client
|
|
3
|
+
*/
|
|
4
|
+
export interface CreateAddressResponse {
|
|
5
|
+
address: string;
|
|
6
|
+
token: string;
|
|
7
|
+
note: string;
|
|
8
|
+
}
|
|
9
|
+
export interface EmailSummary {
|
|
10
|
+
id: string;
|
|
11
|
+
from_addr: string;
|
|
12
|
+
from_name: string | null;
|
|
13
|
+
subject: string;
|
|
14
|
+
received_at: string;
|
|
15
|
+
is_read: boolean;
|
|
16
|
+
otp_code?: string | null;
|
|
17
|
+
otp_link?: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface EmailDetail extends EmailSummary {
|
|
20
|
+
body_text: string | null;
|
|
21
|
+
body_html: string | null;
|
|
22
|
+
is_archived: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface InboxResponse {
|
|
25
|
+
address: string;
|
|
26
|
+
unread_count: number;
|
|
27
|
+
emails: EmailSummary[];
|
|
28
|
+
}
|
|
29
|
+
export interface OtpResponse {
|
|
30
|
+
found: boolean;
|
|
31
|
+
email_id?: string;
|
|
32
|
+
from?: string;
|
|
33
|
+
subject?: string;
|
|
34
|
+
code?: string | null;
|
|
35
|
+
link?: string | null;
|
|
36
|
+
received_at?: string;
|
|
37
|
+
message?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface WebhookResponse {
|
|
40
|
+
configured: boolean;
|
|
41
|
+
url: string | null;
|
|
42
|
+
has_secret: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface WebhookSetResponse {
|
|
45
|
+
ok: boolean;
|
|
46
|
+
url: string;
|
|
47
|
+
secret: string;
|
|
48
|
+
note: string;
|
|
49
|
+
}
|
|
50
|
+
export declare class ShellMailAPI {
|
|
51
|
+
private token;
|
|
52
|
+
constructor(token?: string);
|
|
53
|
+
private request;
|
|
54
|
+
createAddress(local: string, recoveryEmail: string): Promise<CreateAddressResponse>;
|
|
55
|
+
inbox(unreadOnly?: boolean, limit?: number): Promise<InboxResponse>;
|
|
56
|
+
read(emailId: string): Promise<EmailDetail>;
|
|
57
|
+
markRead(emailId: string): Promise<{
|
|
58
|
+
ok: boolean;
|
|
59
|
+
}>;
|
|
60
|
+
archive(emailId: string): Promise<{
|
|
61
|
+
ok: boolean;
|
|
62
|
+
}>;
|
|
63
|
+
delete(emailId: string): Promise<{
|
|
64
|
+
ok: boolean;
|
|
65
|
+
}>;
|
|
66
|
+
otp(options?: {
|
|
67
|
+
timeout?: number;
|
|
68
|
+
from?: string;
|
|
69
|
+
since?: string;
|
|
70
|
+
}): Promise<OtpResponse>;
|
|
71
|
+
search(options: {
|
|
72
|
+
q?: string;
|
|
73
|
+
from?: string;
|
|
74
|
+
hasOtp?: boolean;
|
|
75
|
+
limit?: number;
|
|
76
|
+
}): Promise<{
|
|
77
|
+
count: number;
|
|
78
|
+
emails: EmailSummary[];
|
|
79
|
+
}>;
|
|
80
|
+
getWebhook(): Promise<WebhookResponse>;
|
|
81
|
+
setWebhook(url: string, secret?: string): Promise<WebhookSetResponse>;
|
|
82
|
+
deleteWebhook(): Promise<{
|
|
83
|
+
ok: boolean;
|
|
84
|
+
}>;
|
|
85
|
+
health(): Promise<{
|
|
86
|
+
service: string;
|
|
87
|
+
status: string;
|
|
88
|
+
domain: string;
|
|
89
|
+
}>;
|
|
90
|
+
}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShellMail API Client
|
|
3
|
+
*/
|
|
4
|
+
const API_BASE = process.env.SHELLMAIL_API_URL || "https://shellmail.ai";
|
|
5
|
+
export class ShellMailAPI {
|
|
6
|
+
token;
|
|
7
|
+
constructor(token) {
|
|
8
|
+
this.token = token || process.env.SHELLMAIL_TOKEN || null;
|
|
9
|
+
}
|
|
10
|
+
async request(method, path, body, requireAuth = true) {
|
|
11
|
+
const headers = {
|
|
12
|
+
"Content-Type": "application/json",
|
|
13
|
+
};
|
|
14
|
+
if (requireAuth) {
|
|
15
|
+
if (!this.token) {
|
|
16
|
+
throw new Error("No token configured. Run 'shellmail setup' first.");
|
|
17
|
+
}
|
|
18
|
+
headers["Authorization"] = `Bearer ${this.token}`;
|
|
19
|
+
}
|
|
20
|
+
const response = await fetch(`${API_BASE}${path}`, {
|
|
21
|
+
method,
|
|
22
|
+
headers,
|
|
23
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
24
|
+
});
|
|
25
|
+
const data = await response.json();
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(data.error || `Request failed: ${response.status}`);
|
|
28
|
+
}
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
async createAddress(local, recoveryEmail) {
|
|
32
|
+
return this.request("POST", "/api/addresses", { local, recovery_email: recoveryEmail }, false);
|
|
33
|
+
}
|
|
34
|
+
async inbox(unreadOnly = false, limit = 20) {
|
|
35
|
+
const params = new URLSearchParams();
|
|
36
|
+
if (unreadOnly)
|
|
37
|
+
params.set("unread", "true");
|
|
38
|
+
params.set("limit", limit.toString());
|
|
39
|
+
return this.request("GET", `/api/mail?${params}`);
|
|
40
|
+
}
|
|
41
|
+
async read(emailId) {
|
|
42
|
+
return this.request("GET", `/api/mail/${emailId}`);
|
|
43
|
+
}
|
|
44
|
+
async markRead(emailId) {
|
|
45
|
+
return this.request("PATCH", `/api/mail/${emailId}`, { is_read: true });
|
|
46
|
+
}
|
|
47
|
+
async archive(emailId) {
|
|
48
|
+
return this.request("PATCH", `/api/mail/${emailId}`, { is_archived: true });
|
|
49
|
+
}
|
|
50
|
+
async delete(emailId) {
|
|
51
|
+
return this.request("DELETE", `/api/mail/${emailId}`);
|
|
52
|
+
}
|
|
53
|
+
async otp(options) {
|
|
54
|
+
const params = new URLSearchParams();
|
|
55
|
+
if (options?.timeout)
|
|
56
|
+
params.set("timeout", options.timeout.toString());
|
|
57
|
+
if (options?.from)
|
|
58
|
+
params.set("from", options.from);
|
|
59
|
+
if (options?.since)
|
|
60
|
+
params.set("since", options.since);
|
|
61
|
+
return this.request("GET", `/api/mail/otp?${params}`);
|
|
62
|
+
}
|
|
63
|
+
async search(options) {
|
|
64
|
+
const params = new URLSearchParams();
|
|
65
|
+
if (options.q)
|
|
66
|
+
params.set("q", options.q);
|
|
67
|
+
if (options.from)
|
|
68
|
+
params.set("from", options.from);
|
|
69
|
+
if (options.hasOtp)
|
|
70
|
+
params.set("has_otp", "true");
|
|
71
|
+
if (options.limit)
|
|
72
|
+
params.set("limit", options.limit.toString());
|
|
73
|
+
return this.request("GET", `/api/mail/search?${params}`);
|
|
74
|
+
}
|
|
75
|
+
async getWebhook() {
|
|
76
|
+
return this.request("GET", "/api/webhook");
|
|
77
|
+
}
|
|
78
|
+
async setWebhook(url, secret) {
|
|
79
|
+
return this.request("PUT", "/api/webhook", {
|
|
80
|
+
url,
|
|
81
|
+
secret,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async deleteWebhook() {
|
|
85
|
+
return this.request("DELETE", "/api/webhook");
|
|
86
|
+
}
|
|
87
|
+
async health() {
|
|
88
|
+
return this.request("GET", "/health", undefined, false);
|
|
89
|
+
}
|
|
90
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for ShellMail CLI
|
|
3
|
+
*/
|
|
4
|
+
export interface ShellMailConfig {
|
|
5
|
+
token?: string;
|
|
6
|
+
address?: string;
|
|
7
|
+
apiUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function loadConfig(): ShellMailConfig;
|
|
10
|
+
export declare function saveConfig(config: ShellMailConfig): void;
|
|
11
|
+
export declare function clearConfig(): void;
|
|
12
|
+
export declare function getToken(): string | undefined;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for ShellMail CLI
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
const CONFIG_DIR = join(homedir(), ".shellmail");
|
|
8
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
9
|
+
export function loadConfig() {
|
|
10
|
+
// Environment variables take precedence
|
|
11
|
+
if (process.env.SHELLMAIL_TOKEN) {
|
|
12
|
+
return {
|
|
13
|
+
token: process.env.SHELLMAIL_TOKEN,
|
|
14
|
+
address: process.env.SHELLMAIL_ADDRESS,
|
|
15
|
+
apiUrl: process.env.SHELLMAIL_API_URL,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
// Try to load from config file
|
|
19
|
+
if (existsSync(CONFIG_FILE)) {
|
|
20
|
+
try {
|
|
21
|
+
const data = readFileSync(CONFIG_FILE, "utf-8");
|
|
22
|
+
return JSON.parse(data);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
export function saveConfig(config) {
|
|
31
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
32
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
33
|
+
}
|
|
34
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
|
|
35
|
+
mode: 0o600, // Only owner can read/write
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function clearConfig() {
|
|
39
|
+
if (existsSync(CONFIG_FILE)) {
|
|
40
|
+
writeFileSync(CONFIG_FILE, "{}", { mode: 0o600 });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function getToken() {
|
|
44
|
+
return process.env.SHELLMAIL_TOKEN || loadConfig().token;
|
|
45
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ShellMail CLI
|
|
4
|
+
* Email for AI agents — in 30 seconds
|
|
5
|
+
*/
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import ora from "ora";
|
|
9
|
+
import inquirer from "inquirer";
|
|
10
|
+
import { ShellMailAPI } from "./api.js";
|
|
11
|
+
import { loadConfig, saveConfig, clearConfig, getToken } from "./config.js";
|
|
12
|
+
const program = new Command();
|
|
13
|
+
program
|
|
14
|
+
.name("shellmail")
|
|
15
|
+
.description("Email for AI agents — create addresses, check mail, extract OTPs")
|
|
16
|
+
.version("1.0.0");
|
|
17
|
+
// ── Setup Command ────────────────────────────────────────
|
|
18
|
+
program
|
|
19
|
+
.command("setup")
|
|
20
|
+
.description("Create a new ShellMail address interactively")
|
|
21
|
+
.option("-l, --local <name>", "Local part of email address")
|
|
22
|
+
.option("-r, --recovery <email>", "Recovery email address")
|
|
23
|
+
.action(async (options) => {
|
|
24
|
+
console.log(chalk.bold("\n📧 ShellMail Setup\n"));
|
|
25
|
+
let local = options.local;
|
|
26
|
+
let recoveryEmail = options.recovery;
|
|
27
|
+
if (!local) {
|
|
28
|
+
const answers = await inquirer.prompt([
|
|
29
|
+
{
|
|
30
|
+
type: "input",
|
|
31
|
+
name: "local",
|
|
32
|
+
message: "Choose your email address:",
|
|
33
|
+
suffix: chalk.gray("@shellmail.ai"),
|
|
34
|
+
validate: (input) => {
|
|
35
|
+
if (!input || input.length < 2)
|
|
36
|
+
return "Must be at least 2 characters";
|
|
37
|
+
if (!/^[a-z0-9][a-z0-9._-]*[a-z0-9]$/i.test(input) && input.length > 2) {
|
|
38
|
+
return "Only letters, numbers, dots, hyphens, underscores allowed";
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
]);
|
|
44
|
+
local = answers.local;
|
|
45
|
+
}
|
|
46
|
+
if (!recoveryEmail) {
|
|
47
|
+
const answers = await inquirer.prompt([
|
|
48
|
+
{
|
|
49
|
+
type: "input",
|
|
50
|
+
name: "recovery",
|
|
51
|
+
message: "Recovery email (for token recovery):",
|
|
52
|
+
validate: (input) => {
|
|
53
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input)) {
|
|
54
|
+
return "Enter a valid email address";
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
]);
|
|
60
|
+
recoveryEmail = answers.recovery;
|
|
61
|
+
}
|
|
62
|
+
const spinner = ora("Creating address...").start();
|
|
63
|
+
try {
|
|
64
|
+
const api = new ShellMailAPI();
|
|
65
|
+
const result = await api.createAddress(local, recoveryEmail);
|
|
66
|
+
spinner.succeed(chalk.green("Address created!"));
|
|
67
|
+
console.log("\n" + chalk.bold("Your ShellMail address:"));
|
|
68
|
+
console.log(chalk.cyan(` ${result.address}\n`));
|
|
69
|
+
console.log(chalk.bold("Your API token:"));
|
|
70
|
+
console.log(chalk.yellow(` ${result.token}\n`));
|
|
71
|
+
console.log(chalk.gray("⚠️ Save this token! It won't be shown again.\n"));
|
|
72
|
+
// Save to config
|
|
73
|
+
const { save } = await inquirer.prompt([
|
|
74
|
+
{
|
|
75
|
+
type: "confirm",
|
|
76
|
+
name: "save",
|
|
77
|
+
message: "Save token to ~/.shellmail/config.json?",
|
|
78
|
+
default: true,
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
if (save) {
|
|
82
|
+
saveConfig({
|
|
83
|
+
token: result.token,
|
|
84
|
+
address: result.address,
|
|
85
|
+
});
|
|
86
|
+
console.log(chalk.green("\n✓ Config saved! You can now use other shellmail commands.\n"));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
console.log(chalk.gray("\nSet SHELLMAIL_TOKEN env var to use the CLI:\n"));
|
|
90
|
+
console.log(chalk.cyan(` export SHELLMAIL_TOKEN="${result.token}"\n`));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
spinner.fail(chalk.red("Failed to create address"));
|
|
95
|
+
console.error(chalk.red(` ${err.message}\n`));
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
// ── Inbox Command ────────────────────────────────────────
|
|
100
|
+
program
|
|
101
|
+
.command("inbox")
|
|
102
|
+
.description("List emails in your inbox")
|
|
103
|
+
.option("-u, --unread", "Show only unread emails")
|
|
104
|
+
.option("-n, --limit <number>", "Number of emails to show", "10")
|
|
105
|
+
.action(async (options) => {
|
|
106
|
+
const token = getToken();
|
|
107
|
+
if (!token) {
|
|
108
|
+
console.error(chalk.red("No token configured. Run 'shellmail setup' first."));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
const spinner = ora("Fetching inbox...").start();
|
|
112
|
+
try {
|
|
113
|
+
const api = new ShellMailAPI(token);
|
|
114
|
+
const result = await api.inbox(options.unread, parseInt(options.limit));
|
|
115
|
+
spinner.stop();
|
|
116
|
+
console.log(chalk.bold(`\n📬 ${result.address}`));
|
|
117
|
+
console.log(chalk.gray(` ${result.unread_count} unread\n`));
|
|
118
|
+
if (result.emails.length === 0) {
|
|
119
|
+
console.log(chalk.gray(" No emails.\n"));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
for (const email of result.emails) {
|
|
123
|
+
const unread = !email.is_read ? chalk.blue("●") : " ";
|
|
124
|
+
const from = email.from_name || email.from_addr.split("@")[0];
|
|
125
|
+
const date = new Date(email.received_at).toLocaleString();
|
|
126
|
+
const otp = email.otp_code ? chalk.yellow(` [OTP: ${email.otp_code}]`) : "";
|
|
127
|
+
console.log(`${unread} ${chalk.bold(from.slice(0, 20).padEnd(20))} ${email.subject?.slice(0, 40) || "(no subject)"}${otp}`);
|
|
128
|
+
console.log(chalk.gray(` ${email.id} ${date}\n`));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
spinner.fail(chalk.red("Failed to fetch inbox"));
|
|
133
|
+
console.error(chalk.red(` ${err.message}\n`));
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
// ── Read Command ─────────────────────────────────────────
|
|
138
|
+
program
|
|
139
|
+
.command("read <id>")
|
|
140
|
+
.description("Read a specific email")
|
|
141
|
+
.option("-m, --mark-read", "Mark as read after viewing", true)
|
|
142
|
+
.action(async (id, options) => {
|
|
143
|
+
const token = getToken();
|
|
144
|
+
if (!token) {
|
|
145
|
+
console.error(chalk.red("No token configured. Run 'shellmail setup' first."));
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
const spinner = ora("Fetching email...").start();
|
|
149
|
+
try {
|
|
150
|
+
const api = new ShellMailAPI(token);
|
|
151
|
+
const email = await api.read(id);
|
|
152
|
+
if (options.markRead && !email.is_read) {
|
|
153
|
+
await api.markRead(id);
|
|
154
|
+
}
|
|
155
|
+
spinner.stop();
|
|
156
|
+
console.log("\n" + chalk.bold("From: ") + (email.from_name ? `${email.from_name} <${email.from_addr}>` : email.from_addr));
|
|
157
|
+
console.log(chalk.bold("Subject: ") + (email.subject || "(no subject)"));
|
|
158
|
+
console.log(chalk.bold("Date: ") + new Date(email.received_at).toLocaleString());
|
|
159
|
+
console.log(chalk.gray("─".repeat(60)) + "\n");
|
|
160
|
+
if (email.body_text) {
|
|
161
|
+
// Clean up quoted-printable encoding
|
|
162
|
+
const body = email.body_text
|
|
163
|
+
.replace(/=\r?\n/g, "")
|
|
164
|
+
.replace(/=([0-9A-F]{2})/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
165
|
+
console.log(body);
|
|
166
|
+
}
|
|
167
|
+
else if (email.body_html) {
|
|
168
|
+
// Strip HTML tags for terminal display
|
|
169
|
+
const text = email.body_html
|
|
170
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
|
|
171
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
|
|
172
|
+
.replace(/<[^>]+>/g, " ")
|
|
173
|
+
.replace(/\s+/g, " ")
|
|
174
|
+
.trim();
|
|
175
|
+
console.log(text);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
console.log(chalk.gray("(no content)"));
|
|
179
|
+
}
|
|
180
|
+
console.log("\n");
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
spinner.fail(chalk.red("Failed to read email"));
|
|
184
|
+
console.error(chalk.red(` ${err.message}\n`));
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
// ── OTP Command ──────────────────────────────────────────
|
|
189
|
+
program
|
|
190
|
+
.command("otp")
|
|
191
|
+
.description("Get the latest OTP/verification code")
|
|
192
|
+
.option("-w, --wait <seconds>", "Wait for OTP to arrive (max 30s)")
|
|
193
|
+
.option("-f, --from <domain>", "Filter by sender domain")
|
|
194
|
+
.action(async (options) => {
|
|
195
|
+
const token = getToken();
|
|
196
|
+
if (!token) {
|
|
197
|
+
console.error(chalk.red("No token configured. Run 'shellmail setup' first."));
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
const timeout = options.wait ? Math.min(parseInt(options.wait) * 1000, 30000) : 0;
|
|
201
|
+
const spinner = timeout
|
|
202
|
+
? ora(`Waiting for OTP (${options.wait}s timeout)...`).start()
|
|
203
|
+
: ora("Checking for OTP...").start();
|
|
204
|
+
try {
|
|
205
|
+
const api = new ShellMailAPI(token);
|
|
206
|
+
const result = await api.otp({
|
|
207
|
+
timeout,
|
|
208
|
+
from: options.from,
|
|
209
|
+
});
|
|
210
|
+
if (result.found) {
|
|
211
|
+
spinner.succeed(chalk.green("OTP found!"));
|
|
212
|
+
console.log("\n" + chalk.bold.yellow(` ${result.code || result.link}\n`));
|
|
213
|
+
console.log(chalk.gray(` From: ${result.from}`));
|
|
214
|
+
console.log(chalk.gray(` Subject: ${result.subject}`));
|
|
215
|
+
console.log(chalk.gray(` Received: ${new Date(result.received_at).toLocaleString()}\n`));
|
|
216
|
+
// Output just the code for piping
|
|
217
|
+
if (process.stdout.isTTY === false) {
|
|
218
|
+
process.stdout.write(result.code || result.link || "");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
spinner.warn(chalk.yellow(result.message || "No OTP found"));
|
|
223
|
+
console.log(chalk.gray("\n Tip: Send a verification email to your address, then try again.\n"));
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
spinner.fail(chalk.red("Failed to check OTP"));
|
|
229
|
+
console.error(chalk.red(` ${err.message}\n`));
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
// ── Search Command ───────────────────────────────────────
|
|
234
|
+
program
|
|
235
|
+
.command("search")
|
|
236
|
+
.description("Search emails")
|
|
237
|
+
.option("-q, --query <text>", "Search text in subject/body/sender")
|
|
238
|
+
.option("-f, --from <domain>", "Filter by sender")
|
|
239
|
+
.option("--otp", "Only show emails with OTP codes")
|
|
240
|
+
.option("-n, --limit <number>", "Number of results", "10")
|
|
241
|
+
.action(async (options) => {
|
|
242
|
+
const token = getToken();
|
|
243
|
+
if (!token) {
|
|
244
|
+
console.error(chalk.red("No token configured. Run 'shellmail setup' first."));
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
if (!options.query && !options.from && !options.otp) {
|
|
248
|
+
console.error(chalk.red("Provide at least one search option: --query, --from, or --otp"));
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
const spinner = ora("Searching...").start();
|
|
252
|
+
try {
|
|
253
|
+
const api = new ShellMailAPI(token);
|
|
254
|
+
const result = await api.search({
|
|
255
|
+
q: options.query,
|
|
256
|
+
from: options.from,
|
|
257
|
+
hasOtp: options.otp,
|
|
258
|
+
limit: parseInt(options.limit),
|
|
259
|
+
});
|
|
260
|
+
spinner.stop();
|
|
261
|
+
console.log(chalk.bold(`\n🔍 ${result.count} result(s)\n`));
|
|
262
|
+
for (const email of result.emails) {
|
|
263
|
+
const from = email.from_name || email.from_addr.split("@")[0];
|
|
264
|
+
const otp = email.otp_code ? chalk.yellow(` [OTP: ${email.otp_code}]`) : "";
|
|
265
|
+
console.log(`${chalk.bold(from.slice(0, 20).padEnd(20))} ${email.subject?.slice(0, 40) || "(no subject)"}${otp}`);
|
|
266
|
+
console.log(chalk.gray(` ${email.id}\n`));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
spinner.fail(chalk.red("Search failed"));
|
|
271
|
+
console.error(chalk.red(` ${err.message}\n`));
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
// ── Webhook Command ──────────────────────────────────────
|
|
276
|
+
program
|
|
277
|
+
.command("webhook")
|
|
278
|
+
.description("Configure webhook notifications")
|
|
279
|
+
.option("-s, --set <url>", "Set webhook URL")
|
|
280
|
+
.option("-d, --delete", "Remove webhook configuration")
|
|
281
|
+
.action(async (options) => {
|
|
282
|
+
const token = getToken();
|
|
283
|
+
if (!token) {
|
|
284
|
+
console.error(chalk.red("No token configured. Run 'shellmail setup' first."));
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
const api = new ShellMailAPI(token);
|
|
288
|
+
if (options.delete) {
|
|
289
|
+
const spinner = ora("Removing webhook...").start();
|
|
290
|
+
try {
|
|
291
|
+
await api.deleteWebhook();
|
|
292
|
+
spinner.succeed(chalk.green("Webhook removed"));
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
spinner.fail(chalk.red("Failed to remove webhook"));
|
|
296
|
+
console.error(chalk.red(` ${err.message}\n`));
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (options.set) {
|
|
302
|
+
const spinner = ora("Configuring webhook...").start();
|
|
303
|
+
try {
|
|
304
|
+
const result = await api.setWebhook(options.set);
|
|
305
|
+
spinner.succeed(chalk.green("Webhook configured!"));
|
|
306
|
+
console.log("\n" + chalk.bold("URL: ") + result.url);
|
|
307
|
+
console.log(chalk.bold("Secret: ") + chalk.yellow(result.secret));
|
|
308
|
+
console.log(chalk.gray("\n⚠️ Save the secret! Use it to verify webhook signatures.\n"));
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
spinner.fail(chalk.red("Failed to configure webhook"));
|
|
312
|
+
console.error(chalk.red(` ${err.message}\n`));
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
// Show current config
|
|
318
|
+
const spinner = ora("Fetching webhook config...").start();
|
|
319
|
+
try {
|
|
320
|
+
const result = await api.getWebhook();
|
|
321
|
+
spinner.stop();
|
|
322
|
+
if (result.configured) {
|
|
323
|
+
console.log(chalk.bold("\n🔔 Webhook configured"));
|
|
324
|
+
console.log(chalk.bold("URL: ") + result.url);
|
|
325
|
+
console.log(chalk.bold("Secret: ") + (result.has_secret ? chalk.green("configured") : chalk.yellow("not set")));
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
console.log(chalk.gray("\nNo webhook configured."));
|
|
329
|
+
console.log(chalk.gray("Use 'shellmail webhook --set <url>' to configure.\n"));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
spinner.fail(chalk.red("Failed to fetch webhook config"));
|
|
334
|
+
console.error(chalk.red(` ${err.message}\n`));
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
// ── Delete Command ───────────────────────────────────────
|
|
339
|
+
program
|
|
340
|
+
.command("delete <id>")
|
|
341
|
+
.description("Delete an email")
|
|
342
|
+
.action(async (id) => {
|
|
343
|
+
const token = getToken();
|
|
344
|
+
if (!token) {
|
|
345
|
+
console.error(chalk.red("No token configured. Run 'shellmail setup' first."));
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
const spinner = ora("Deleting email...").start();
|
|
349
|
+
try {
|
|
350
|
+
const api = new ShellMailAPI(token);
|
|
351
|
+
await api.delete(id);
|
|
352
|
+
spinner.succeed(chalk.green("Email deleted"));
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
spinner.fail(chalk.red("Failed to delete email"));
|
|
356
|
+
console.error(chalk.red(` ${err.message}\n`));
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
// ── Archive Command ──────────────────────────────────────
|
|
361
|
+
program
|
|
362
|
+
.command("archive <id>")
|
|
363
|
+
.description("Archive an email")
|
|
364
|
+
.action(async (id) => {
|
|
365
|
+
const token = getToken();
|
|
366
|
+
if (!token) {
|
|
367
|
+
console.error(chalk.red("No token configured. Run 'shellmail setup' first."));
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
const spinner = ora("Archiving email...").start();
|
|
371
|
+
try {
|
|
372
|
+
const api = new ShellMailAPI(token);
|
|
373
|
+
await api.archive(id);
|
|
374
|
+
spinner.succeed(chalk.green("Email archived"));
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
spinner.fail(chalk.red("Failed to archive email"));
|
|
378
|
+
console.error(chalk.red(` ${err.message}\n`));
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
// ── Logout Command ───────────────────────────────────────
|
|
383
|
+
program
|
|
384
|
+
.command("logout")
|
|
385
|
+
.description("Clear saved configuration")
|
|
386
|
+
.action(async () => {
|
|
387
|
+
const { confirm } = await inquirer.prompt([
|
|
388
|
+
{
|
|
389
|
+
type: "confirm",
|
|
390
|
+
name: "confirm",
|
|
391
|
+
message: "This will remove your saved token. Continue?",
|
|
392
|
+
default: false,
|
|
393
|
+
},
|
|
394
|
+
]);
|
|
395
|
+
if (confirm) {
|
|
396
|
+
clearConfig();
|
|
397
|
+
console.log(chalk.green("\n✓ Configuration cleared.\n"));
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
// ── Status Command ───────────────────────────────────────
|
|
401
|
+
program
|
|
402
|
+
.command("status")
|
|
403
|
+
.description("Check ShellMail service status and current config")
|
|
404
|
+
.action(async () => {
|
|
405
|
+
const config = loadConfig();
|
|
406
|
+
const api = new ShellMailAPI();
|
|
407
|
+
console.log(chalk.bold("\n📧 ShellMail Status\n"));
|
|
408
|
+
// Check service health
|
|
409
|
+
const spinner = ora("Checking service...").start();
|
|
410
|
+
try {
|
|
411
|
+
const health = await api.health();
|
|
412
|
+
spinner.succeed(chalk.green(`Service: ${health.status}`));
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
spinner.fail(chalk.red("Service: unreachable"));
|
|
416
|
+
}
|
|
417
|
+
// Show config
|
|
418
|
+
if (config.token) {
|
|
419
|
+
console.log(chalk.green("✓ Token: configured"));
|
|
420
|
+
if (config.address) {
|
|
421
|
+
console.log(chalk.green(`✓ Address: ${config.address}`));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
else if (process.env.SHELLMAIL_TOKEN) {
|
|
425
|
+
console.log(chalk.green("✓ Token: set via SHELLMAIL_TOKEN"));
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
console.log(chalk.yellow("✗ Token: not configured"));
|
|
429
|
+
console.log(chalk.gray(" Run 'shellmail setup' to create an address"));
|
|
430
|
+
}
|
|
431
|
+
console.log("");
|
|
432
|
+
});
|
|
433
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shellmail",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI for ShellMail - Email for AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"shellmail": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc -w",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"email",
|
|
17
|
+
"ai",
|
|
18
|
+
"agents",
|
|
19
|
+
"cli",
|
|
20
|
+
"shellmail"
|
|
21
|
+
],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/aaronbatchelder/shellmail"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"chalk": "^5.3.0",
|
|
30
|
+
"commander": "^12.1.0",
|
|
31
|
+
"inquirer": "^9.2.15",
|
|
32
|
+
"ora": "^8.0.1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/inquirer": "^9.0.7",
|
|
36
|
+
"@types/node": "^20.11.0",
|
|
37
|
+
"typescript": "^5.3.3"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist"
|
|
44
|
+
]
|
|
45
|
+
}
|