mails 0.0.8 → 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/README.ja.md +146 -0
- package/README.md +196 -148
- package/README.zh.md +160 -0
- package/dist/cli.js +668 -0
- package/dist/index.js +411 -0
- package/package.json +39 -26
- package/skill.md +213 -0
- package/.npmignore +0 -7
- package/LICENSE +0 -19
- package/README.en.md +0 -106
- package/bin/cli +0 -3
- package/index.js +0 -6
- package/libs/cli.js +0 -58
- package/libs/mail.js +0 -25
- package/libs/render.js +0 -13
- package/libs/serve.js +0 -59
package/dist/index.js
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/core/config.ts
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
var CONFIG_DIR = join(homedir(), ".mails");
|
|
7
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
8
|
+
var DEFAULT_CONFIG = {
|
|
9
|
+
mode: "hosted",
|
|
10
|
+
domain: "mails.dev",
|
|
11
|
+
mailbox: "",
|
|
12
|
+
send_provider: "resend",
|
|
13
|
+
storage_provider: "sqlite"
|
|
14
|
+
};
|
|
15
|
+
function ensureDir() {
|
|
16
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
function loadConfig() {
|
|
19
|
+
ensureDir();
|
|
20
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
21
|
+
return { ...DEFAULT_CONFIG };
|
|
22
|
+
}
|
|
23
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
24
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
|
|
25
|
+
}
|
|
26
|
+
function saveConfig(config) {
|
|
27
|
+
ensureDir();
|
|
28
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + `
|
|
29
|
+
`);
|
|
30
|
+
}
|
|
31
|
+
function getConfigValue(key) {
|
|
32
|
+
const config = { ...loadConfig() };
|
|
33
|
+
return config[key];
|
|
34
|
+
}
|
|
35
|
+
function setConfigValue(key, value) {
|
|
36
|
+
const config = { ...loadConfig() };
|
|
37
|
+
config[key] = value;
|
|
38
|
+
saveConfig(config);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/providers/send/resend.ts
|
|
42
|
+
function createResendProvider(apiKey) {
|
|
43
|
+
return {
|
|
44
|
+
name: "resend",
|
|
45
|
+
async send(options) {
|
|
46
|
+
const body = {
|
|
47
|
+
from: options.from,
|
|
48
|
+
to: options.to,
|
|
49
|
+
subject: options.subject
|
|
50
|
+
};
|
|
51
|
+
if (options.text)
|
|
52
|
+
body.text = options.text;
|
|
53
|
+
if (options.html)
|
|
54
|
+
body.html = options.html;
|
|
55
|
+
if (options.replyTo)
|
|
56
|
+
body.reply_to = options.replyTo;
|
|
57
|
+
const res = await fetch("https://api.resend.com/emails", {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: {
|
|
60
|
+
Authorization: `Bearer ${apiKey}`,
|
|
61
|
+
"Content-Type": "application/json"
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify(body)
|
|
64
|
+
});
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
const err = await res.json();
|
|
67
|
+
throw new Error(`Resend error: ${err.message}`);
|
|
68
|
+
}
|
|
69
|
+
const data = await res.json();
|
|
70
|
+
return { id: data.id, provider: "resend" };
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/core/send.ts
|
|
76
|
+
function resolveProvider() {
|
|
77
|
+
const config = loadConfig();
|
|
78
|
+
switch (config.send_provider) {
|
|
79
|
+
case "resend": {
|
|
80
|
+
if (!config.resend_api_key) {
|
|
81
|
+
throw new Error("resend_api_key not configured. Run: mails config set resend_api_key <key>");
|
|
82
|
+
}
|
|
83
|
+
return createResendProvider(config.resend_api_key);
|
|
84
|
+
}
|
|
85
|
+
default:
|
|
86
|
+
throw new Error(`Unknown send provider: ${config.send_provider}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function send(options) {
|
|
90
|
+
const config = loadConfig();
|
|
91
|
+
const provider = resolveProvider();
|
|
92
|
+
const from = options.from ?? config.default_from;
|
|
93
|
+
if (!from) {
|
|
94
|
+
throw new Error('No "from" address. Set default_from or pass --from');
|
|
95
|
+
}
|
|
96
|
+
const to = Array.isArray(options.to) ? options.to : [options.to];
|
|
97
|
+
if (!options.text && !options.html) {
|
|
98
|
+
throw new Error("Either text or html body is required");
|
|
99
|
+
}
|
|
100
|
+
return provider.send({
|
|
101
|
+
from,
|
|
102
|
+
to,
|
|
103
|
+
subject: options.subject,
|
|
104
|
+
text: options.text,
|
|
105
|
+
html: options.html,
|
|
106
|
+
replyTo: options.replyTo
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// src/providers/storage/sqlite.ts
|
|
110
|
+
import { Database } from "bun:sqlite";
|
|
111
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
112
|
+
import { homedir as homedir2 } from "os";
|
|
113
|
+
import { join as join2 } from "path";
|
|
114
|
+
var SCHEMA = `
|
|
115
|
+
CREATE TABLE IF NOT EXISTS emails (
|
|
116
|
+
id TEXT PRIMARY KEY,
|
|
117
|
+
mailbox TEXT NOT NULL,
|
|
118
|
+
from_address TEXT NOT NULL,
|
|
119
|
+
from_name TEXT DEFAULT '',
|
|
120
|
+
to_address TEXT NOT NULL,
|
|
121
|
+
subject TEXT DEFAULT '',
|
|
122
|
+
body_text TEXT DEFAULT '',
|
|
123
|
+
body_html TEXT DEFAULT '',
|
|
124
|
+
code TEXT,
|
|
125
|
+
headers TEXT DEFAULT '{}',
|
|
126
|
+
metadata TEXT DEFAULT '{}',
|
|
127
|
+
direction TEXT NOT NULL CHECK (direction IN ('inbound', 'outbound')),
|
|
128
|
+
status TEXT DEFAULT 'received' CHECK (status IN ('received', 'sent', 'failed', 'queued')),
|
|
129
|
+
received_at TEXT NOT NULL,
|
|
130
|
+
created_at TEXT NOT NULL
|
|
131
|
+
);
|
|
132
|
+
CREATE INDEX IF NOT EXISTS idx_emails_mailbox ON emails(mailbox, received_at DESC);
|
|
133
|
+
CREATE INDEX IF NOT EXISTS idx_emails_code ON emails(mailbox) WHERE code IS NOT NULL;
|
|
134
|
+
`;
|
|
135
|
+
function createSqliteProvider(dbPath) {
|
|
136
|
+
const dir = join2(homedir2(), ".mails");
|
|
137
|
+
if (!existsSync2(dir))
|
|
138
|
+
mkdirSync2(dir, { recursive: true });
|
|
139
|
+
const path = dbPath ?? join2(dir, "mails.db");
|
|
140
|
+
let db;
|
|
141
|
+
return {
|
|
142
|
+
name: "sqlite",
|
|
143
|
+
async init() {
|
|
144
|
+
db = new Database(path);
|
|
145
|
+
db.exec("PRAGMA journal_mode=WAL;");
|
|
146
|
+
db.exec(SCHEMA);
|
|
147
|
+
},
|
|
148
|
+
async saveEmail(email) {
|
|
149
|
+
db.prepare(`
|
|
150
|
+
INSERT OR REPLACE INTO emails (id, mailbox, from_address, from_name, to_address, subject, body_text, body_html, code, headers, metadata, direction, status, received_at, created_at)
|
|
151
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
152
|
+
`).run(email.id, email.mailbox, email.from_address, email.from_name, email.to_address, email.subject, email.body_text, email.body_html, email.code, JSON.stringify(email.headers), JSON.stringify(email.metadata), email.direction, email.status, email.received_at, email.created_at);
|
|
153
|
+
},
|
|
154
|
+
async getEmails(mailbox, options) {
|
|
155
|
+
const limit = options?.limit ?? 20;
|
|
156
|
+
const offset = options?.offset ?? 0;
|
|
157
|
+
let query = "SELECT * FROM emails WHERE mailbox = ?";
|
|
158
|
+
const params = [mailbox];
|
|
159
|
+
if (options?.direction) {
|
|
160
|
+
query += " AND direction = ?";
|
|
161
|
+
params.push(options.direction);
|
|
162
|
+
}
|
|
163
|
+
query += " ORDER BY received_at DESC LIMIT ? OFFSET ?";
|
|
164
|
+
params.push(limit, offset);
|
|
165
|
+
const rows = db.prepare(query).all(...params);
|
|
166
|
+
return rows.map(rowToEmail);
|
|
167
|
+
},
|
|
168
|
+
async getEmail(id) {
|
|
169
|
+
const row = db.prepare("SELECT * FROM emails WHERE id = ?").get(id);
|
|
170
|
+
return row ? rowToEmail(row) : null;
|
|
171
|
+
},
|
|
172
|
+
async getCode(mailbox, options) {
|
|
173
|
+
const timeout = (options?.timeout ?? 30) * 1000;
|
|
174
|
+
const since = options?.since;
|
|
175
|
+
const deadline = Date.now() + timeout;
|
|
176
|
+
while (Date.now() < deadline) {
|
|
177
|
+
let query = "SELECT code, from_address, subject FROM emails WHERE mailbox = ? AND code IS NOT NULL";
|
|
178
|
+
const params = [mailbox];
|
|
179
|
+
if (since) {
|
|
180
|
+
query += " AND received_at > ?";
|
|
181
|
+
params.push(since);
|
|
182
|
+
}
|
|
183
|
+
query += " ORDER BY received_at DESC LIMIT 1";
|
|
184
|
+
const row = db.prepare(query).get(...params);
|
|
185
|
+
if (row) {
|
|
186
|
+
return { code: row.code, from: row.from_address, subject: row.subject };
|
|
187
|
+
}
|
|
188
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function rowToEmail(row) {
|
|
195
|
+
return {
|
|
196
|
+
id: row.id,
|
|
197
|
+
mailbox: row.mailbox,
|
|
198
|
+
from_address: row.from_address,
|
|
199
|
+
from_name: row.from_name ?? "",
|
|
200
|
+
to_address: row.to_address,
|
|
201
|
+
subject: row.subject ?? "",
|
|
202
|
+
body_text: row.body_text ?? "",
|
|
203
|
+
body_html: row.body_html ?? "",
|
|
204
|
+
code: row.code ?? null,
|
|
205
|
+
headers: safeJsonParse(row.headers, {}),
|
|
206
|
+
metadata: safeJsonParse(row.metadata, {}),
|
|
207
|
+
direction: row.direction,
|
|
208
|
+
status: row.status,
|
|
209
|
+
received_at: row.received_at,
|
|
210
|
+
created_at: row.created_at
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function safeJsonParse(str, fallback) {
|
|
214
|
+
if (!str)
|
|
215
|
+
return fallback;
|
|
216
|
+
try {
|
|
217
|
+
return JSON.parse(str);
|
|
218
|
+
} catch {
|
|
219
|
+
return fallback;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/providers/storage/db9.ts
|
|
224
|
+
var SCHEMA2 = `
|
|
225
|
+
CREATE TABLE IF NOT EXISTS emails (
|
|
226
|
+
id TEXT PRIMARY KEY,
|
|
227
|
+
mailbox TEXT NOT NULL,
|
|
228
|
+
from_address TEXT NOT NULL,
|
|
229
|
+
from_name TEXT DEFAULT '',
|
|
230
|
+
to_address TEXT NOT NULL,
|
|
231
|
+
subject TEXT DEFAULT '',
|
|
232
|
+
body_text TEXT DEFAULT '',
|
|
233
|
+
body_html TEXT DEFAULT '',
|
|
234
|
+
code TEXT,
|
|
235
|
+
headers JSONB DEFAULT '{}',
|
|
236
|
+
metadata JSONB DEFAULT '{}',
|
|
237
|
+
direction TEXT NOT NULL CHECK (direction IN ('inbound', 'outbound')),
|
|
238
|
+
status TEXT DEFAULT 'received' CHECK (status IN ('received', 'sent', 'failed', 'queued')),
|
|
239
|
+
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
240
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
241
|
+
);
|
|
242
|
+
CREATE INDEX IF NOT EXISTS idx_emails_mailbox ON emails(mailbox, received_at DESC);
|
|
243
|
+
CREATE INDEX IF NOT EXISTS idx_emails_code ON emails(mailbox) WHERE code IS NOT NULL;
|
|
244
|
+
`;
|
|
245
|
+
function createDb9Provider(token, databaseId) {
|
|
246
|
+
const baseUrl = "https://api.db9.ai";
|
|
247
|
+
async function sql(query) {
|
|
248
|
+
const res = await fetch(`${baseUrl}/customer/databases/${databaseId}/sql`, {
|
|
249
|
+
method: "POST",
|
|
250
|
+
headers: {
|
|
251
|
+
Authorization: `Bearer ${token}`,
|
|
252
|
+
"Content-Type": "application/json"
|
|
253
|
+
},
|
|
254
|
+
body: JSON.stringify({ query })
|
|
255
|
+
});
|
|
256
|
+
if (!res.ok) {
|
|
257
|
+
const text = await res.text();
|
|
258
|
+
throw new Error(`db9 error (${res.status}): ${text}`);
|
|
259
|
+
}
|
|
260
|
+
return await res.json();
|
|
261
|
+
}
|
|
262
|
+
function rowsToEmails(result) {
|
|
263
|
+
return result.rows.map((row) => {
|
|
264
|
+
const obj = {};
|
|
265
|
+
result.columns.forEach((col, i) => {
|
|
266
|
+
obj[col] = row[i];
|
|
267
|
+
});
|
|
268
|
+
return {
|
|
269
|
+
id: obj.id,
|
|
270
|
+
mailbox: obj.mailbox,
|
|
271
|
+
from_address: obj.from_address,
|
|
272
|
+
from_name: obj.from_name ?? "",
|
|
273
|
+
to_address: obj.to_address,
|
|
274
|
+
subject: obj.subject ?? "",
|
|
275
|
+
body_text: obj.body_text ?? "",
|
|
276
|
+
body_html: obj.body_html ?? "",
|
|
277
|
+
code: obj.code ?? null,
|
|
278
|
+
headers: typeof obj.headers === "string" ? JSON.parse(obj.headers) : obj.headers ?? {},
|
|
279
|
+
metadata: typeof obj.metadata === "string" ? JSON.parse(obj.metadata) : obj.metadata ?? {},
|
|
280
|
+
direction: obj.direction,
|
|
281
|
+
status: obj.status,
|
|
282
|
+
received_at: obj.received_at,
|
|
283
|
+
created_at: obj.created_at
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
name: "db9",
|
|
289
|
+
async init() {
|
|
290
|
+
await sql(SCHEMA2);
|
|
291
|
+
},
|
|
292
|
+
async saveEmail(email) {
|
|
293
|
+
const esc = (s) => s.replace(/'/g, "''");
|
|
294
|
+
await sql(`
|
|
295
|
+
INSERT INTO emails (id, mailbox, from_address, from_name, to_address, subject, body_text, body_html, code, headers, metadata, direction, status, received_at, created_at)
|
|
296
|
+
VALUES (
|
|
297
|
+
'${esc(email.id)}', '${esc(email.mailbox)}', '${esc(email.from_address)}', '${esc(email.from_name)}',
|
|
298
|
+
'${esc(email.to_address)}', '${esc(email.subject)}', '${esc(email.body_text)}', '${esc(email.body_html)}',
|
|
299
|
+
${email.code ? `'${esc(email.code)}'` : "NULL"},
|
|
300
|
+
'${esc(JSON.stringify(email.headers))}'::jsonb,
|
|
301
|
+
'${esc(JSON.stringify(email.metadata))}'::jsonb,
|
|
302
|
+
'${esc(email.direction)}', '${esc(email.status)}',
|
|
303
|
+
'${esc(email.received_at)}', '${esc(email.created_at)}'
|
|
304
|
+
)
|
|
305
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
306
|
+
status = EXCLUDED.status,
|
|
307
|
+
metadata = EXCLUDED.metadata
|
|
308
|
+
`);
|
|
309
|
+
},
|
|
310
|
+
async getEmails(mailbox, options) {
|
|
311
|
+
const limit = options?.limit ?? 20;
|
|
312
|
+
const offset = options?.offset ?? 0;
|
|
313
|
+
const esc = (s) => s.replace(/'/g, "''");
|
|
314
|
+
let query = `SELECT * FROM emails WHERE mailbox = '${esc(mailbox)}'`;
|
|
315
|
+
if (options?.direction) {
|
|
316
|
+
query += ` AND direction = '${esc(options.direction)}'`;
|
|
317
|
+
}
|
|
318
|
+
query += ` ORDER BY received_at DESC LIMIT ${limit} OFFSET ${offset}`;
|
|
319
|
+
const result = await sql(query);
|
|
320
|
+
return rowsToEmails(result);
|
|
321
|
+
},
|
|
322
|
+
async getEmail(id) {
|
|
323
|
+
const esc = (s) => s.replace(/'/g, "''");
|
|
324
|
+
const result = await sql(`SELECT * FROM emails WHERE id = '${esc(id)}' LIMIT 1`);
|
|
325
|
+
const emails = rowsToEmails(result);
|
|
326
|
+
return emails[0] ?? null;
|
|
327
|
+
},
|
|
328
|
+
async getCode(mailbox, options) {
|
|
329
|
+
const timeout = (options?.timeout ?? 30) * 1000;
|
|
330
|
+
const since = options?.since;
|
|
331
|
+
const esc = (s) => s.replace(/'/g, "''");
|
|
332
|
+
const deadline = Date.now() + timeout;
|
|
333
|
+
while (Date.now() < deadline) {
|
|
334
|
+
let query = `SELECT code, from_address, subject FROM emails WHERE mailbox = '${esc(mailbox)}' AND code IS NOT NULL`;
|
|
335
|
+
if (since) {
|
|
336
|
+
query += ` AND received_at > '${esc(since)}'`;
|
|
337
|
+
}
|
|
338
|
+
query += " ORDER BY received_at DESC LIMIT 1";
|
|
339
|
+
const result = await sql(query);
|
|
340
|
+
if (result.row_count > 0) {
|
|
341
|
+
const row = result.rows[0];
|
|
342
|
+
const codeIdx = result.columns.indexOf("code");
|
|
343
|
+
const fromIdx = result.columns.indexOf("from_address");
|
|
344
|
+
const subIdx = result.columns.indexOf("subject");
|
|
345
|
+
return {
|
|
346
|
+
code: row[codeIdx],
|
|
347
|
+
from: row[fromIdx],
|
|
348
|
+
subject: row[subIdx]
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/core/storage.ts
|
|
359
|
+
var _provider = null;
|
|
360
|
+
async function getStorage() {
|
|
361
|
+
if (_provider)
|
|
362
|
+
return _provider;
|
|
363
|
+
const config = loadConfig();
|
|
364
|
+
switch (config.storage_provider) {
|
|
365
|
+
case "db9": {
|
|
366
|
+
if (!config.db9_token) {
|
|
367
|
+
throw new Error("db9_token not configured. Run: mails config set db9_token <token>");
|
|
368
|
+
}
|
|
369
|
+
if (!config.db9_database_id) {
|
|
370
|
+
throw new Error("db9_database_id not configured. Run: mails config set db9_database_id <id>");
|
|
371
|
+
}
|
|
372
|
+
_provider = createDb9Provider(config.db9_token, config.db9_database_id);
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
case "sqlite":
|
|
376
|
+
default: {
|
|
377
|
+
_provider = createSqliteProvider();
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
await _provider.init();
|
|
382
|
+
return _provider;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/core/receive.ts
|
|
386
|
+
async function getInbox(mailbox, options) {
|
|
387
|
+
const storage = await getStorage();
|
|
388
|
+
return storage.getEmails(mailbox, options);
|
|
389
|
+
}
|
|
390
|
+
async function getEmail(id) {
|
|
391
|
+
const storage = await getStorage();
|
|
392
|
+
return storage.getEmail(id);
|
|
393
|
+
}
|
|
394
|
+
async function waitForCode(mailbox, options) {
|
|
395
|
+
const storage = await getStorage();
|
|
396
|
+
return storage.getCode(mailbox, options);
|
|
397
|
+
}
|
|
398
|
+
export {
|
|
399
|
+
waitForCode,
|
|
400
|
+
setConfigValue,
|
|
401
|
+
send,
|
|
402
|
+
saveConfig,
|
|
403
|
+
loadConfig,
|
|
404
|
+
getStorage,
|
|
405
|
+
getInbox,
|
|
406
|
+
getEmail,
|
|
407
|
+
getConfigValue,
|
|
408
|
+
createSqliteProvider,
|
|
409
|
+
createResendProvider,
|
|
410
|
+
createDb9Provider
|
|
411
|
+
};
|
package/package.json
CHANGED
|
@@ -1,34 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mails",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"bin":
|
|
9
|
-
|
|
10
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Email infrastructure for AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"mails": "dist/cli.js"
|
|
11
10
|
},
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "bun build src/cli/index.ts --outfile dist/cli.js --target bun && bun build src/index.ts --outfile dist/index.js --target bun",
|
|
19
|
+
"build:compile": "bun build src/cli/index.ts --compile --outfile mails",
|
|
20
|
+
"dev": "bun run src/cli/index.ts",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"test": "bun test test/unit test/e2e/flow.test.ts",
|
|
23
|
+
"test:coverage": "bun test --coverage test/unit test/e2e/flow.test.ts",
|
|
24
|
+
"test:live": "bun test test/e2e/live.test.ts",
|
|
25
|
+
"test:all": "bun test"
|
|
15
26
|
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"skill.md"
|
|
30
|
+
],
|
|
16
31
|
"keywords": [
|
|
32
|
+
"email",
|
|
17
33
|
"mail",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
34
|
+
"agent",
|
|
35
|
+
"ai",
|
|
36
|
+
"resend",
|
|
37
|
+
"cloudflare",
|
|
38
|
+
"cli"
|
|
20
39
|
],
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
},
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"juice": "~0.4.0",
|
|
28
|
-
"optimist": "*",
|
|
29
|
-
"consoler": "*",
|
|
30
|
-
"mails-default": "*",
|
|
31
|
-
"nodemailer": "~0.5.7",
|
|
32
|
-
"pkghub-render": "*"
|
|
40
|
+
"author": "turing <o.u.turing@gmail.com>",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"dependencies": {},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/bun": "^1.2.0",
|
|
45
|
+
"typescript": "^5.7.0"
|
|
33
46
|
}
|
|
34
|
-
}
|
|
47
|
+
}
|
package/skill.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# mails — Email for AI Agents
|
|
2
|
+
|
|
3
|
+
Send and receive emails programmatically. Supports custom domains (self-hosted) or zero-config `@mails.dev` addresses (hosted).
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install
|
|
9
|
+
npm install -g mails # or: bunx mails
|
|
10
|
+
|
|
11
|
+
# Configure
|
|
12
|
+
mails config set resend_api_key re_YOUR_KEY
|
|
13
|
+
mails config set default_from "Agent <agent@yourdomain.com>"
|
|
14
|
+
|
|
15
|
+
# Send
|
|
16
|
+
mails send --to user@example.com --subject "Hello" --body "World"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Configuration
|
|
20
|
+
|
|
21
|
+
Config lives at `~/.mails/config.json`. Set values via CLI:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
mails config set <key> <value>
|
|
25
|
+
mails config get <key>
|
|
26
|
+
mails config # show all
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Required Keys
|
|
30
|
+
|
|
31
|
+
| Key | Description |
|
|
32
|
+
|-----|-------------|
|
|
33
|
+
| `resend_api_key` | Your Resend API key (get one at resend.com) |
|
|
34
|
+
| `default_from` | Default sender, e.g. `"Agent <agent@yourdomain.com>"` |
|
|
35
|
+
|
|
36
|
+
### Optional Keys
|
|
37
|
+
|
|
38
|
+
| Key | Default | Description |
|
|
39
|
+
|-----|---------|-------------|
|
|
40
|
+
| `mode` | `hosted` | `hosted` (use @mails.dev) or `selfhosted` (custom domain) |
|
|
41
|
+
| `domain` | `mails.dev` | Your email domain |
|
|
42
|
+
| `mailbox` | | Your receiving address |
|
|
43
|
+
| `storage_provider` | `sqlite` | `sqlite` (local) or `db9` (db9.ai cloud) |
|
|
44
|
+
| `db9_token` | | db9.ai API token |
|
|
45
|
+
| `db9_database_id` | | db9.ai database ID |
|
|
46
|
+
|
|
47
|
+
## Sending Emails
|
|
48
|
+
|
|
49
|
+
### CLI
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Plain text
|
|
53
|
+
mails send --to user@example.com --subject "Report" --body "Here is your report."
|
|
54
|
+
|
|
55
|
+
# HTML
|
|
56
|
+
mails send --to user@example.com --subject "Report" --html "<h1>Report</h1><p>Details...</p>"
|
|
57
|
+
|
|
58
|
+
# Custom sender
|
|
59
|
+
mails send --from "Bot <bot@mydomain.com>" --to user@example.com --subject "Hi" --body "Hello"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Programmatic (SDK)
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { send } from 'mails'
|
|
66
|
+
|
|
67
|
+
const result = await send({
|
|
68
|
+
to: 'user@example.com',
|
|
69
|
+
subject: 'Hello from agent',
|
|
70
|
+
text: 'This is a test email.',
|
|
71
|
+
})
|
|
72
|
+
console.log(result.id) // Resend message ID
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Programmatic (Direct Provider)
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { createResendProvider } from 'mails'
|
|
79
|
+
|
|
80
|
+
const provider = createResendProvider('re_YOUR_KEY')
|
|
81
|
+
const result = await provider.send({
|
|
82
|
+
from: 'Agent <agent@yourdomain.com>',
|
|
83
|
+
to: ['user@example.com'],
|
|
84
|
+
subject: 'Hello',
|
|
85
|
+
text: 'Direct provider usage.',
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Receiving Emails
|
|
90
|
+
|
|
91
|
+
Requires a Cloudflare Email Routing Worker or the mails.dev hosted service. Once configured:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# List inbox
|
|
95
|
+
mails inbox
|
|
96
|
+
mails inbox --mailbox agent@yourdomain.com
|
|
97
|
+
|
|
98
|
+
# Wait for verification code (long-poll)
|
|
99
|
+
mails code --to agent@yourdomain.com --timeout 30
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### SDK
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { getInbox, waitForCode } from 'mails'
|
|
106
|
+
|
|
107
|
+
// List recent emails
|
|
108
|
+
const emails = await getInbox('agent@yourdomain.com', { limit: 10 })
|
|
109
|
+
|
|
110
|
+
// Wait for a verification code
|
|
111
|
+
const result = await waitForCode('agent@yourdomain.com', { timeout: 30 })
|
|
112
|
+
if (result) {
|
|
113
|
+
console.log(result.code) // "123456"
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Cloud API (mails.dev)
|
|
118
|
+
|
|
119
|
+
For agents that need email without local CLI setup. Pay per use with USDC via x402.
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
Base URL: https://api.mails.dev
|
|
123
|
+
|
|
124
|
+
POST /v1/send Send an email ($0.001/email)
|
|
125
|
+
GET /v1/inbox?to=... List received emails (free)
|
|
126
|
+
GET /v1/code?to=... Wait for verification code (free)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Send via API
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
curl -X POST https://api.mails.dev/v1/send \
|
|
133
|
+
-H "Content-Type: application/json" \
|
|
134
|
+
-d '{
|
|
135
|
+
"from": "agent@mails.dev",
|
|
136
|
+
"to": ["user@example.com"],
|
|
137
|
+
"subject": "Hello",
|
|
138
|
+
"text": "Sent via mails.dev cloud API"
|
|
139
|
+
}'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
If no payment header is present, the API returns HTTP 402 with payment instructions.
|
|
143
|
+
Attach an `X-PAYMENT` header with a signed USDC payment to complete the request.
|
|
144
|
+
|
|
145
|
+
### Query Inbox
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# List inbox (free)
|
|
149
|
+
curl "https://api.mails.dev/v1/inbox?to=myagent@mails.dev&limit=10"
|
|
150
|
+
|
|
151
|
+
# Wait for verification code (free, long-poll up to 55s)
|
|
152
|
+
curl "https://api.mails.dev/v1/code?to=myagent@mails.dev&timeout=30"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Self-Hosted Setup
|
|
156
|
+
|
|
157
|
+
For custom domains, run the interactive setup:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
mails setup
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
This opens a browser-based wizard at `mails.dev/setup` that guides you through:
|
|
164
|
+
1. Cloudflare API token configuration
|
|
165
|
+
2. DNS record setup (MX, SPF, DKIM, DMARC)
|
|
166
|
+
3. Email Worker deployment
|
|
167
|
+
4. Send provider (Resend) configuration
|
|
168
|
+
5. Storage provider selection
|
|
169
|
+
|
|
170
|
+
## Storage Providers
|
|
171
|
+
|
|
172
|
+
### SQLite (default)
|
|
173
|
+
Local database at `~/.mails/mails.db`. Zero config. Good for development and single-agent use.
|
|
174
|
+
|
|
175
|
+
### db9.ai
|
|
176
|
+
Cloud PostgreSQL for agents. Enables multi-agent access to shared mailboxes.
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
mails config set storage_provider db9
|
|
180
|
+
mails config set db9_token YOUR_TOKEN
|
|
181
|
+
mails config set db9_database_id YOUR_DB_ID
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Email Schema
|
|
185
|
+
|
|
186
|
+
All storage providers use this schema:
|
|
187
|
+
|
|
188
|
+
```sql
|
|
189
|
+
CREATE TABLE emails (
|
|
190
|
+
id TEXT PRIMARY KEY,
|
|
191
|
+
mailbox TEXT NOT NULL,
|
|
192
|
+
from_address TEXT NOT NULL,
|
|
193
|
+
from_name TEXT DEFAULT '',
|
|
194
|
+
to_address TEXT NOT NULL,
|
|
195
|
+
subject TEXT DEFAULT '',
|
|
196
|
+
body_text TEXT DEFAULT '',
|
|
197
|
+
body_html TEXT DEFAULT '',
|
|
198
|
+
code TEXT,
|
|
199
|
+
headers TEXT DEFAULT '{}',
|
|
200
|
+
metadata TEXT DEFAULT '{}',
|
|
201
|
+
direction TEXT NOT NULL CHECK (direction IN ('inbound', 'outbound')),
|
|
202
|
+
status TEXT DEFAULT 'received' CHECK (status IN ('received', 'sent', 'failed', 'queued')),
|
|
203
|
+
received_at TEXT NOT NULL,
|
|
204
|
+
created_at TEXT NOT NULL
|
|
205
|
+
);
|
|
206
|
+
CREATE INDEX idx_emails_mailbox ON emails(mailbox, received_at DESC);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Links
|
|
210
|
+
|
|
211
|
+
- Website: https://mails.dev
|
|
212
|
+
- npm: https://www.npmjs.com/package/mails
|
|
213
|
+
- GitHub: https://github.com/user/mails
|