tossinbox 0.1.5 → 0.1.6
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 +18 -3
- package/dist/cli.js +59 -3
- package/dist/core/index.js +1 -0
- package/dist/core/providers/mailtm.js +21 -0
- package/dist/core/providers/tempmailio.js +28 -0
- package/dist/core/providers/tempmailplus.js +36 -4
- package/dist/core/save.js +41 -0
- package/dist/mcp.js +22 -4
- package/dist/version.js +1 -3
- package/llms.txt +11 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
**Disposable email inboxes for humans and AI agents. Spawn a temporary inbox, wait for the OTP verification code, toss it.**
|
|
8
8
|
|
|
9
9
|
[](https://github.com/mohamed-khairy-5i/tossinbox/actions/workflows/ci.yml)
|
|
10
|
+
[](https://www.npmjs.com/package/tossinbox)
|
|
11
|
+
[](https://www.npmjs.com/package/tossinbox?activeTab=versions)
|
|
10
12
|

|
|
11
13
|

|
|
12
14
|

|
|
@@ -55,7 +57,7 @@ $ tossinbox toss
|
|
|
55
57
|
| Documented exit codes | 0–4 | none | no |
|
|
56
58
|
| MCP server for agents | yes, built in | no | no |
|
|
57
59
|
| Runs headless / in CI | yes | no | partial |
|
|
58
|
-
| Upstream alive | 7 providers, 4 stacks
|
|
60
|
+
| Upstream alive | 7 providers, 4 stacks, attachments on read | varies | many wrap the dead 1secmail |
|
|
59
61
|
| Ads, trackers, popups | none | the business model | none |
|
|
60
62
|
|
|
61
63
|
Checked September 2026. If a cell is wrong, open an issue and win the argument.
|
|
@@ -109,7 +111,7 @@ tossinbox wait --code --json
|
|
|
109
111
|
|---|---|
|
|
110
112
|
| `spawn` | Create a new disposable inbox (`-p provider`, `-l label`). If the provider is down, another one is used automatically — `--no-failover` opts out |
|
|
111
113
|
| `list` | List messages (`-a address`) |
|
|
112
|
-
| `read <id>` | Read a full message, including any detected code |
|
|
114
|
+
| `read <id>` | Read a full message, including any detected code; lists attachments. `--html` prints the raw HTML body, `--save [dir]` downloads attachments + bodies to `<dir>/<message-id>/` |
|
|
113
115
|
| `wait` | Poll until a message arrives (`-f sender`, `-s subject`, `-c` extract code, `-t timeout` max 600s) |
|
|
114
116
|
| `watch` | Stream new messages until Ctrl-C — only new arrivals; `--json` = one JSON object per line (NDJSON) |
|
|
115
117
|
| `inboxes` | List locally saved inboxes |
|
|
@@ -155,7 +157,7 @@ TossInbox ships with an MCP server exposing four tools:
|
|
|
155
157
|
|---|---|
|
|
156
158
|
| `create_inbox` | Create a disposable inbox and return its address |
|
|
157
159
|
| `list_messages` | List messages in an inbox |
|
|
158
|
-
| `read_message` | Read a full message, including any detected code |
|
|
160
|
+
| `read_message` | Read a full message, including any detected code; reports attachment metadata and can save attachments + bodies to disk (`save_dir`) |
|
|
159
161
|
| `wait_for_code` | Poll until a message arrives and return its verification code |
|
|
160
162
|
|
|
161
163
|
### Claude Desktop / Cursor / any MCP client
|
|
@@ -226,6 +228,12 @@ machine-readable and kept up to date.
|
|
|
226
228
|
Adding a provider means implementing a small interface (`createInbox`,
|
|
227
229
|
`listMessages`, `readMessage`, optional `destroyInbox`) — PRs welcome.
|
|
228
230
|
|
|
231
|
+
**Attachments** (v0.1.6): full list + download on `mailtm`, `mailgw` and
|
|
232
|
+
`tempmailplus`; `tempmailio` shows them when its upstream returns them
|
|
233
|
+
(downloadable only if it exposes a URL); `tempmaillol`, `guerrillamail` and
|
|
234
|
+
`maildrop` do not expose attachments upstream — TossInbox tells you that
|
|
235
|
+
instead of guessing.
|
|
236
|
+
|
|
229
237
|
## FAQ
|
|
230
238
|
|
|
231
239
|
**Is it really free?**
|
|
@@ -245,6 +253,12 @@ providers and reports the switch (human mode prints a `⚠` warning and
|
|
|
245
253
|
`provider : mailtm (failover from mailgw)`; `--json` returns a `failover`
|
|
246
254
|
object). Use `--no-failover` if you need the chosen provider or nothing.
|
|
247
255
|
|
|
256
|
+
**Can I download attachments?**
|
|
257
|
+
Yes on `mailtm`, `mailgw` and `tempmailplus`; `tempmailio` shows them when its
|
|
258
|
+
upstream exposes them; the other three providers have no attachments upstream.
|
|
259
|
+
`tossinbox read <id> --save` writes them (plus the HTML/plain-text bodies) to
|
|
260
|
+
`./tossinbox-attachments/<message-id>/`.
|
|
261
|
+
|
|
248
262
|
**A site blocked my disposable address. What now?**
|
|
249
263
|
Some sites blocklist known disposable domains. Try the other provider:
|
|
250
264
|
`tossinbox spawn -p guerrillamail`. If both are blocked, the site wins that
|
|
@@ -269,6 +283,7 @@ round.
|
|
|
269
283
|
- [x] `mail.gw` provider (v0.1.3)
|
|
270
284
|
- [x] Four more providers: `tempmail.lol`, `temp-mail.io`, `tempmail.plus`, `maildrop.cc` (v0.1.4)
|
|
271
285
|
- [x] Provider failover: auto-switch when a provider is down (v0.1.5)
|
|
286
|
+
- [x] Attachments & HTML bodies: `read --save` / `--html` (v0.1.6)
|
|
272
287
|
- [ ] Homebrew core formula (after community adoption)
|
|
273
288
|
|
|
274
289
|
## Documentation
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command, CommanderError } from "commander";
|
|
3
|
-
import { DEFAULT_PROVIDER, createInboxWithFailover, getProvider, listProviders, listSavedInboxes, removeInbox, clearInboxes, resolveInbox, saveInbox, statePath, waitForMessage, sleep, htmlToText, ProviderError, } from "./core/index.js";
|
|
3
|
+
import { DEFAULT_PROVIDER, createInboxWithFailover, getProvider, listProviders, listSavedInboxes, removeInbox, clearInboxes, resolveInbox, saveInbox, saveMessageContent, statePath, waitForMessage, sleep, htmlToText, ProviderError, } from "./core/index.js";
|
|
4
4
|
import { VERSION } from "./version.js";
|
|
5
5
|
/* Documented exit codes:
|
|
6
6
|
* 0 success
|
|
@@ -58,8 +58,26 @@ function jsonMessage(message, includeHtml = false) {
|
|
|
58
58
|
code: message.code,
|
|
59
59
|
text: message.text ?? (message.html ? htmlToText(message.html) : undefined),
|
|
60
60
|
html: includeHtml ? message.html : undefined,
|
|
61
|
+
...(message.attachments
|
|
62
|
+
? {
|
|
63
|
+
attachments: message.attachments.map((a) => ({
|
|
64
|
+
filename: a.filename,
|
|
65
|
+
size: a.size,
|
|
66
|
+
contentType: a.contentType,
|
|
67
|
+
})),
|
|
68
|
+
}
|
|
69
|
+
: {}),
|
|
61
70
|
};
|
|
62
71
|
}
|
|
72
|
+
function formatSize(bytes) {
|
|
73
|
+
if (bytes === undefined)
|
|
74
|
+
return "?";
|
|
75
|
+
if (bytes < 1024)
|
|
76
|
+
return `${bytes} B`;
|
|
77
|
+
if (bytes < 1024 * 1024)
|
|
78
|
+
return `${(bytes / 1024).toFixed(1)} kB`;
|
|
79
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
80
|
+
}
|
|
63
81
|
function printMessageHuman(message, withBody) {
|
|
64
82
|
console.log(`✔ ${message.subject}`);
|
|
65
83
|
console.log(` from : ${message.fromName ? `${message.fromName} <${message.from}>` : message.from}`);
|
|
@@ -67,6 +85,12 @@ function printMessageHuman(message, withBody) {
|
|
|
67
85
|
console.log(` date : ${message.createdAt}`);
|
|
68
86
|
if (message.code)
|
|
69
87
|
console.log(` code : ${message.code}`);
|
|
88
|
+
if (message.attachments && message.attachments.length > 0) {
|
|
89
|
+
console.log(` attachments (${message.attachments.length}):`);
|
|
90
|
+
for (const a of message.attachments) {
|
|
91
|
+
console.log(` - ${a.filename} (${formatSize(a.size)}${a.contentType ? `, ${a.contentType}` : ""})`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
70
94
|
if (withBody) {
|
|
71
95
|
const body = message.text ?? (message.html ? htmlToText(message.html) : "");
|
|
72
96
|
if (body)
|
|
@@ -144,8 +168,10 @@ program
|
|
|
144
168
|
});
|
|
145
169
|
program
|
|
146
170
|
.command("read <id>")
|
|
147
|
-
.description("Read a full message by id")
|
|
171
|
+
.description("Read a full message by id (list attachments, optionally save them and the HTML body)")
|
|
148
172
|
.option("-a, --address <address>", "inbox address (defaults to the most recent inbox)")
|
|
173
|
+
.option("--html", "print the raw HTML body instead of the plain-text version")
|
|
174
|
+
.option("--save [dir]", "save attachments + body.html/body.txt into <dir>/<message-id>/ (default dir: ./tossinbox-attachments)")
|
|
149
175
|
.action(async (id, opts) => {
|
|
150
176
|
try {
|
|
151
177
|
const inbox = await resolveInbox(opts.address);
|
|
@@ -153,11 +179,38 @@ program
|
|
|
153
179
|
fail(new Error("No saved inbox found. Run: tossinbox spawn"), EXIT_NOT_FOUND);
|
|
154
180
|
const provider = providerOrExit(inbox.provider);
|
|
155
181
|
const message = await provider.readMessage(inbox, id);
|
|
182
|
+
let saved;
|
|
183
|
+
if (opts.save !== undefined) {
|
|
184
|
+
const dir = typeof opts.save === "string" && opts.save.length > 0 ? opts.save : "./tossinbox-attachments";
|
|
185
|
+
try {
|
|
186
|
+
saved = await saveMessageContent(provider, inbox, message, dir);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
fail(err);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
156
192
|
if (jsonMode()) {
|
|
157
|
-
out({
|
|
193
|
+
out({
|
|
194
|
+
ok: true,
|
|
195
|
+
inbox: inbox.address,
|
|
196
|
+
message: jsonMessage(message, true),
|
|
197
|
+
...(saved ? { saved } : {}),
|
|
198
|
+
});
|
|
158
199
|
return;
|
|
159
200
|
}
|
|
160
201
|
printMessageHuman(message, true);
|
|
202
|
+
if (opts.html) {
|
|
203
|
+
if (message.html) {
|
|
204
|
+
console.log(`\n${message.html}\n`);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
console.error("⚠ this message has no HTML body — showing the plain-text version above");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (saved) {
|
|
211
|
+
for (const f of saved)
|
|
212
|
+
console.log(` ✔ saved ${f.filename} → ${f.path}`);
|
|
213
|
+
}
|
|
161
214
|
}
|
|
162
215
|
catch (err) {
|
|
163
216
|
fail(err, err instanceof ProviderError && err.status === 404 ? EXIT_NOT_FOUND : EXIT_ERROR);
|
|
@@ -314,6 +367,9 @@ program
|
|
|
314
367
|
createdAt: summary.createdAt,
|
|
315
368
|
code: message?.code,
|
|
316
369
|
text: message?.text ?? (message?.html ? htmlToText(message.html) : undefined),
|
|
370
|
+
...(message?.attachments && message.attachments.length > 0
|
|
371
|
+
? { attachments: message.attachments.map((a) => ({ filename: a.filename, size: a.size })) }
|
|
372
|
+
: {}),
|
|
317
373
|
};
|
|
318
374
|
console.log(JSON.stringify(event));
|
|
319
375
|
}
|
package/dist/core/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export { extractCode, htmlToText } from "./otp.js";
|
|
|
9
9
|
export * from "./state.js";
|
|
10
10
|
export { waitForMessage, sleep } from "./wait.js";
|
|
11
11
|
export { createInboxWithFailover } from "./failover.js";
|
|
12
|
+
export { saveMessageContent, safeSegment } from "./save.js";
|
|
12
13
|
export const providers = {
|
|
13
14
|
[mailTm.name]: mailTm,
|
|
14
15
|
[mailGw.name]: mailGw,
|
|
@@ -133,6 +133,13 @@ function createMailTmLikeProvider(config) {
|
|
|
133
133
|
const res = await request("GET", `${base}/messages/${encodeURIComponent(id)}`, { token: inbox.token });
|
|
134
134
|
const m = await parseJson(res);
|
|
135
135
|
const html = m.html && m.html.length > 0 ? m.html.join("\n") : undefined;
|
|
136
|
+
const attachments = (m.attachments ?? []).map((a) => ({
|
|
137
|
+
id: a.id,
|
|
138
|
+
filename: a.filename || "attachment.bin",
|
|
139
|
+
contentType: a.contentType,
|
|
140
|
+
size: a.size,
|
|
141
|
+
contentId: a.disposition === "inline" ? a.id : undefined,
|
|
142
|
+
}));
|
|
136
143
|
const message = {
|
|
137
144
|
id: m.id,
|
|
138
145
|
from: m.from?.address ?? "unknown",
|
|
@@ -142,10 +149,24 @@ function createMailTmLikeProvider(config) {
|
|
|
142
149
|
createdAt: m.createdAt,
|
|
143
150
|
text: m.text,
|
|
144
151
|
html,
|
|
152
|
+
...(attachments.length > 0 ? { attachments } : {}),
|
|
145
153
|
};
|
|
146
154
|
message.code = extractCode(message.text) ?? extractCode(message.html);
|
|
147
155
|
return message;
|
|
148
156
|
},
|
|
157
|
+
async downloadAttachment(inbox, messageId, attachment) {
|
|
158
|
+
if (!inbox.token)
|
|
159
|
+
throw new ProviderError(providerName, "Inbox is missing its API token");
|
|
160
|
+
if (!attachment.id) {
|
|
161
|
+
throw new ProviderError(providerName, `Attachment "${attachment.filename}" has no id to download`);
|
|
162
|
+
}
|
|
163
|
+
const url = `${base}/messages/${encodeURIComponent(messageId)}/attachment/${encodeURIComponent(attachment.id)}`;
|
|
164
|
+
const res = await request("GET", url, { token: inbox.token });
|
|
165
|
+
if (!res.ok) {
|
|
166
|
+
throw new ProviderError(providerName, `HTTP ${res.status} while downloading "${attachment.filename}"`, res.status);
|
|
167
|
+
}
|
|
168
|
+
return Buffer.from(await res.arrayBuffer());
|
|
169
|
+
},
|
|
149
170
|
async destroyInbox(inbox) {
|
|
150
171
|
if (!inbox.token || !inbox.accountId)
|
|
151
172
|
return;
|
|
@@ -54,6 +54,16 @@ async function fetchMessages(address) {
|
|
|
54
54
|
throw err;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
/** The v3 API attachment objects vary between deployments; parse defensively. */
|
|
58
|
+
function toAttachment(a) {
|
|
59
|
+
return {
|
|
60
|
+
id: a.id !== undefined ? String(a.id) : undefined,
|
|
61
|
+
filename: a.filename ?? a.name ?? "attachment.bin",
|
|
62
|
+
contentType: a.contentType ?? a.content_type ?? a.type,
|
|
63
|
+
size: a.size,
|
|
64
|
+
url: a.url,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
57
67
|
export const tempmailIo = {
|
|
58
68
|
name: "tempmailio",
|
|
59
69
|
description: "temp-mail.io — disposable email with 10+ rotating domains, no API key required",
|
|
@@ -97,10 +107,28 @@ export const tempmailIo = {
|
|
|
97
107
|
createdAt: email.created_at,
|
|
98
108
|
text: email.body_text,
|
|
99
109
|
html: email.body_html,
|
|
110
|
+
...((email.attachments ?? []).length > 0
|
|
111
|
+
? { attachments: (email.attachments ?? []).map(toAttachment) }
|
|
112
|
+
: {}),
|
|
100
113
|
};
|
|
101
114
|
message.code = extractCode(message.text) ?? extractCode(message.html);
|
|
102
115
|
return message;
|
|
103
116
|
},
|
|
117
|
+
async downloadAttachment(_inbox, _messageId, attachment) {
|
|
118
|
+
if (!attachment.url) {
|
|
119
|
+
throw new ProviderError("tempmailio", `temp-mail.io did not expose a download URL for "${attachment.filename}" — open the message in the provider UI to retrieve it`);
|
|
120
|
+
}
|
|
121
|
+
const res = await fetch(attachment.url, {
|
|
122
|
+
headers: { Accept: "*/*", "User-Agent": `tossinbox/${VERSION}` },
|
|
123
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
124
|
+
}).catch((err) => {
|
|
125
|
+
throw networkError(err, "tempmailio", HOST, REQUEST_TIMEOUT_MS);
|
|
126
|
+
});
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
throw new ProviderError("tempmailio", `HTTP ${res.status} while downloading "${attachment.filename}"`, res.status);
|
|
129
|
+
}
|
|
130
|
+
return Buffer.from(await res.arrayBuffer());
|
|
131
|
+
},
|
|
104
132
|
async destroyInbox(inbox) {
|
|
105
133
|
if (!inbox.token)
|
|
106
134
|
return;
|
|
@@ -20,6 +20,11 @@ function randomUser(length = 12) {
|
|
|
20
20
|
function timeToIso(ts) {
|
|
21
21
|
if (!ts)
|
|
22
22
|
return undefined;
|
|
23
|
+
if (typeof ts === "string") {
|
|
24
|
+
// Some endpoints return "YYYY-MM-DD HH:mm:ss" strings instead of epochs
|
|
25
|
+
const d = new Date(ts.includes("T") ? ts : ts.replace(" ", "T") + "Z");
|
|
26
|
+
return isNaN(d.getTime()) ? undefined : d.toISOString();
|
|
27
|
+
}
|
|
23
28
|
// Guard against seconds vs milliseconds epochs
|
|
24
29
|
return new Date(ts < 1e12 ? ts * 1000 : ts).toISOString();
|
|
25
30
|
}
|
|
@@ -75,8 +80,8 @@ export const tempmailPlus = {
|
|
|
75
80
|
async listMessages(inbox) {
|
|
76
81
|
const data = await callJson("GET", `/api/mails/?email=${encodeURIComponent(inbox.address)}&first_id=0`);
|
|
77
82
|
return (data.mail_list ?? []).map((m) => ({
|
|
78
|
-
id: String(m.id),
|
|
79
|
-
from: m.from ?? "unknown",
|
|
83
|
+
id: String(m.mail_id ?? m.id),
|
|
84
|
+
from: m.from_mail ?? m.from ?? "unknown",
|
|
80
85
|
fromName: m.from_name,
|
|
81
86
|
subject: m.subject ?? "(no subject)",
|
|
82
87
|
createdAt: timeToIso(m.time),
|
|
@@ -87,18 +92,45 @@ export const tempmailPlus = {
|
|
|
87
92
|
if (!data.result) {
|
|
88
93
|
throw new ProviderError("tempmailplus", "Message not found — re-list messages to see what is currently in the inbox");
|
|
89
94
|
}
|
|
95
|
+
const attachments = (data.attachments ?? []).map((a) => ({
|
|
96
|
+
id: a.attachment_id !== undefined ? String(a.attachment_id) : undefined,
|
|
97
|
+
filename: a.name || "attachment.bin",
|
|
98
|
+
size: a.size,
|
|
99
|
+
contentId: a.content_id || undefined,
|
|
100
|
+
}));
|
|
90
101
|
const message = {
|
|
91
102
|
id,
|
|
92
|
-
from: data.from ?? "unknown",
|
|
103
|
+
from: data.from_mail ?? data.from ?? "unknown",
|
|
93
104
|
fromName: data.from_name,
|
|
94
105
|
subject: data.subject ?? "(no subject)",
|
|
95
|
-
createdAt: timeToIso(data.time),
|
|
106
|
+
createdAt: timeToIso(data.time) ?? data.date,
|
|
96
107
|
text: data.text,
|
|
97
108
|
html: data.html,
|
|
109
|
+
...(attachments.length > 0 ? { attachments } : {}),
|
|
98
110
|
};
|
|
99
111
|
message.code = extractCode(message.text) ?? extractCode(message.html);
|
|
100
112
|
return message;
|
|
101
113
|
},
|
|
114
|
+
async downloadAttachment(inbox, messageId, attachment) {
|
|
115
|
+
if (!attachment.id) {
|
|
116
|
+
throw new ProviderError("tempmailplus", `Attachment "${attachment.filename}" has no id to download`);
|
|
117
|
+
}
|
|
118
|
+
// Download endpoint as used by the tempmail.plus web client:
|
|
119
|
+
// /api/mails/{mailId}/attachments/{attachment_id}?email={address}&epin={pin?}
|
|
120
|
+
const url = `${BASE}/api/mails/${encodeURIComponent(messageId)}` +
|
|
121
|
+
`/attachments/${encodeURIComponent(attachment.id)}` +
|
|
122
|
+
`?email=${encodeURIComponent(inbox.address)}&epin=${encodeURIComponent(inbox.session ?? "")}`;
|
|
123
|
+
const res = await fetch(url, {
|
|
124
|
+
headers: { Accept: "*/*", "User-Agent": `tossinbox/${VERSION}` },
|
|
125
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
126
|
+
}).catch((err) => {
|
|
127
|
+
throw networkError(err, "tempmailplus", HOST, REQUEST_TIMEOUT_MS);
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok) {
|
|
130
|
+
throw new ProviderError("tempmailplus", `HTTP ${res.status} while downloading "${attachment.filename}"`, res.status);
|
|
131
|
+
}
|
|
132
|
+
return Buffer.from(await res.arrayBuffer());
|
|
133
|
+
},
|
|
102
134
|
async destroyInbox(inbox) {
|
|
103
135
|
try {
|
|
104
136
|
const data = await callJson("GET", `/api/mails/?email=${encodeURIComponent(inbox.address)}&first_id=0`);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
/** Filesystem-safe single path segment: strips separators, traversal and
|
|
4
|
+
* control characters, collapses blanks. */
|
|
5
|
+
export function safeSegment(name, fallback) {
|
|
6
|
+
const cleaned = name
|
|
7
|
+
.replace(/[^A-Za-z0-9._ ()\[\]-]/g, "_")
|
|
8
|
+
.replace(/\.{2,}/g, "_")
|
|
9
|
+
.replace(/^_+|_+$/g, "")
|
|
10
|
+
.trim();
|
|
11
|
+
return cleaned.length > 0 && cleaned !== "." && cleaned !== ".." ? cleaned.slice(0, 120) : fallback;
|
|
12
|
+
}
|
|
13
|
+
/** Save everything a message carries to disk: every attachment (when the
|
|
14
|
+
* provider supports downloads) plus body.html / body.txt when present.
|
|
15
|
+
* Files land in `<dir>/<message-id>/`. Shared by the CLI and the MCP server. */
|
|
16
|
+
export async function saveMessageContent(provider, inbox, message, dir) {
|
|
17
|
+
const messageDir = path.join(path.resolve(dir), safeSegment(message.id, "message"));
|
|
18
|
+
fs.mkdirSync(messageDir, { recursive: true });
|
|
19
|
+
const saved = [];
|
|
20
|
+
for (const attachment of message.attachments ?? []) {
|
|
21
|
+
if (typeof provider.downloadAttachment !== "function") {
|
|
22
|
+
throw new Error(`provider "${provider.name}" does not support attachment downloads — try another provider`);
|
|
23
|
+
}
|
|
24
|
+
const bytes = await provider.downloadAttachment(inbox, message.id, attachment);
|
|
25
|
+
const filename = safeSegment(attachment.filename, "attachment.bin");
|
|
26
|
+
const target = path.join(messageDir, filename);
|
|
27
|
+
fs.writeFileSync(target, bytes);
|
|
28
|
+
saved.push({ kind: "attachment", filename, path: target, size: bytes.length });
|
|
29
|
+
}
|
|
30
|
+
if (message.html !== undefined) {
|
|
31
|
+
const target = path.join(messageDir, "body.html");
|
|
32
|
+
fs.writeFileSync(target, message.html, "utf8");
|
|
33
|
+
saved.push({ kind: "body.html", filename: "body.html", path: target, size: Buffer.byteLength(message.html) });
|
|
34
|
+
}
|
|
35
|
+
if (message.text !== undefined) {
|
|
36
|
+
const target = path.join(messageDir, "body.txt");
|
|
37
|
+
fs.writeFileSync(target, message.text, "utf8");
|
|
38
|
+
saved.push({ kind: "body.txt", filename: "body.txt", path: target, size: Buffer.byteLength(message.text) });
|
|
39
|
+
}
|
|
40
|
+
return saved;
|
|
41
|
+
}
|
package/dist/mcp.js
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
7
|
import { z } from "zod";
|
|
8
|
-
import { DEFAULT_PROVIDER, createInboxWithFailover, getProvider, resolveInbox, saveInbox, waitForMessage, htmlToText, } from "./core/index.js";
|
|
8
|
+
import { DEFAULT_PROVIDER, createInboxWithFailover, getProvider, resolveInbox, saveInbox, saveMessageContent, waitForMessage, htmlToText, } from "./core/index.js";
|
|
9
9
|
import { VERSION } from "./version.js";
|
|
10
10
|
function text(result, isError = false) {
|
|
11
11
|
return {
|
|
@@ -23,6 +23,15 @@ function messageJson(message, includeHtml = false) {
|
|
|
23
23
|
code: message.code,
|
|
24
24
|
text: message.text ?? (message.html ? htmlToText(message.html) : undefined),
|
|
25
25
|
html: includeHtml ? message.html : undefined,
|
|
26
|
+
...(message.attachments
|
|
27
|
+
? {
|
|
28
|
+
attachments: message.attachments.map((a) => ({
|
|
29
|
+
filename: a.filename,
|
|
30
|
+
size: a.size,
|
|
31
|
+
contentType: a.contentType,
|
|
32
|
+
})),
|
|
33
|
+
}
|
|
34
|
+
: {}),
|
|
26
35
|
};
|
|
27
36
|
}
|
|
28
37
|
async function requireInbox(address) {
|
|
@@ -86,19 +95,28 @@ export async function startMcpServer() {
|
|
|
86
95
|
});
|
|
87
96
|
server.registerTool("read_message", {
|
|
88
97
|
title: "Read a message",
|
|
89
|
-
description: "Read the full body of a message by id, including any verification code detected in it.",
|
|
98
|
+
description: "Read the full body of a message by id, including any verification code detected in it. Lists attachment metadata; pass save_dir to also save the attachments and the HTML body to disk (where the provider supports downloads).",
|
|
90
99
|
inputSchema: {
|
|
91
100
|
id: z.string().describe("Message id (from list_messages)"),
|
|
92
101
|
address: z.string().optional().describe("Inbox address; defaults to the most recent inbox"),
|
|
102
|
+
save_dir: z.string().optional().describe('Directory to save attachments and body.html/body.txt into, written to <save_dir>/<message-id>/ (e.g. "/tmp")'),
|
|
93
103
|
},
|
|
94
|
-
}, async ({ id, address }) => {
|
|
104
|
+
}, async ({ id, address, save_dir }) => {
|
|
95
105
|
try {
|
|
96
106
|
const inbox = await requireInbox(address);
|
|
97
107
|
if (!inbox)
|
|
98
108
|
return text({ ok: false, error: "No saved inbox found. Call create_inbox first." }, true);
|
|
99
109
|
const p = getProvider(inbox.provider);
|
|
100
110
|
const message = await p.readMessage(inbox, id);
|
|
101
|
-
|
|
111
|
+
let saved;
|
|
112
|
+
if (save_dir) {
|
|
113
|
+
saved = await saveMessageContent(p, inbox, message, save_dir);
|
|
114
|
+
}
|
|
115
|
+
return text({
|
|
116
|
+
ok: true,
|
|
117
|
+
message: messageJson(message, false),
|
|
118
|
+
...(saved ? { saved } : {}),
|
|
119
|
+
});
|
|
102
120
|
}
|
|
103
121
|
catch (err) {
|
|
104
122
|
return text({ ok: false, error: err instanceof Error ? err.message : String(err) }, true);
|
package/dist/version.js
CHANGED
package/llms.txt
CHANGED
|
@@ -27,10 +27,13 @@ inboxes when done. It is designed agent-first: every CLI command supports
|
|
|
27
27
|
- `tossinbox spawn` — create a new disposable inbox (flags: `-p provider`, `-l label`);
|
|
28
28
|
if the provider is down, another one is used automatically (`--no-failover` opts out)
|
|
29
29
|
- `tossinbox list` — list messages in an inbox (flag: `-a address`)
|
|
30
|
-
- `tossinbox read <id>` — read a full message by id
|
|
30
|
+
- `tossinbox read <id>` — read a full message by id; `--save [dir]` saves every attachment plus the
|
|
31
|
+
HTML/plain-text bodies to `<dir>/<message-id>/` (default dir `./tossinbox-attachments`),
|
|
32
|
+
`--html` prints the raw HTML body
|
|
31
33
|
- `tossinbox wait --code` — poll until a message arrives and print its verification code
|
|
32
34
|
(flags: `-f sender`, `-s subject`, `-t timeout`, `-i interval`)
|
|
33
|
-
- `tossinbox watch` — stream new messages as they arrive until Ctrl-C (NDJSON with `--json
|
|
35
|
+
- `tossinbox watch` — stream new messages as they arrive until Ctrl-C (NDJSON with `--json`; events
|
|
36
|
+
include attachment names when a new message has them)
|
|
34
37
|
- `tossinbox inboxes` — list locally saved inboxes
|
|
35
38
|
- `tossinbox toss` — delete an inbox server-side and wipe local state (`--all` for all)
|
|
36
39
|
- `tossinbox providers` — list providers (default: `mailtm`; also `mailgw`, `guerrillamail`, `tempmaillol`, `tempmailio`, `tempmailplus`, `maildrop`)
|
|
@@ -44,7 +47,8 @@ Stdio MCP server exposing four tools:
|
|
|
44
47
|
|
|
45
48
|
- `create_inbox` — create a disposable inbox, returns its email address
|
|
46
49
|
- `list_messages` — list messages in an inbox
|
|
47
|
-
- `read_message` — read a full message including any detected code
|
|
50
|
+
- `read_message` — read a full message including any detected code; reports attachment metadata and
|
|
51
|
+
accepts an optional `save_dir` input that saves attachments + bodies to `<save_dir>/<message-id>/`
|
|
48
52
|
- `wait_for_code` — poll until a message arrives and return its verification code
|
|
49
53
|
|
|
50
54
|
Typical agent flow: `create_inbox` -> use the address in a signup form ->
|
|
@@ -64,6 +68,10 @@ npx tossinbox@latest spawn
|
|
|
64
68
|
- Providers: `mailtm` (default), `mailgw`, `guerrillamail`, `tempmaillol`,
|
|
65
69
|
`tempmailio`, `tempmailplus`, `maildrop`; no API keys required. When a
|
|
66
70
|
provider is down, `spawn` fails over to another one automatically.
|
|
71
|
+
- Attachments: full list + download on `mailtm`, `mailgw` and `tempmailplus`;
|
|
72
|
+
`tempmailio` shows them when its upstream returns them (download only if it
|
|
73
|
+
exposes a URL); `tempmaillol`, `guerrillamail` and `maildrop` have no
|
|
74
|
+
attachment support upstream.
|
|
67
75
|
- Local state is stored in `~/.tossinbox/state.json` (override with the
|
|
68
76
|
`TOSSINBOX_STATE` environment variable) with 0600 permissions.
|
|
69
77
|
- TossInbox intentionally has no bulk mode; use it for privacy and testing and
|