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 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
  [![CI](https://github.com/mohamed-khairy-5i/tossinbox/actions/workflows/ci.yml/badge.svg)](https://github.com/mohamed-khairy-5i/tossinbox/actions/workflows/ci.yml)
10
+ [![npm](https://img.shields.io/npm/v/tossinbox?color=cb3837&label=npm)](https://www.npmjs.com/package/tossinbox)
11
+ [![npm downloads](https://img.shields.io/npm/dm/tossinbox?color=cb3837&label=downloads)](https://www.npmjs.com/package/tossinbox?activeTab=versions)
10
12
  ![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)
11
13
  ![Node](https://img.shields.io/badge/node-18%2B-34d399.svg)
12
14
  ![MCP](https://img.shields.io/badge/MCP-server-222a39.svg)
@@ -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 | varies | many wrap the dead 1secmail |
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({ ok: true, inbox: inbox.address, message: jsonMessage(message, true) });
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
  }
@@ -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
- return text({ ok: true, message: messageJson(message, false) });
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
@@ -1,3 +1 @@
1
- /** Single source of truth for the runtime version string.
2
- * Keep in sync with package.json — bump both on release. */
3
- export const VERSION = "0.1.5";
1
+ export const VERSION = "0.1.6";
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tossinbox",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Disposable email inboxes for humans and AI agents. Spawn an inbox, wait for the OTP, toss it.",
5
5
  "type": "module",
6
6
  "license": "MIT",