tossinbox 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mohamed Khairy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,271 @@
1
+ # TossInbox — Disposable Email CLI & MCP Server for AI Agents
2
+
3
+ <p align="center">
4
+ <img src="docs/logo.png" width="140" alt="TossInbox logo: an envelope tossed into a trash bin">
5
+ </p>
6
+
7
+ **Disposable email inboxes for humans and AI agents. Spawn a temporary inbox, wait for the OTP verification code, toss it.**
8
+
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
+ ![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)
11
+ ![Node](https://img.shields.io/badge/node-18%2B-34d399.svg)
12
+ ![MCP](https://img.shields.io/badge/MCP-server-222a39.svg)
13
+
14
+ > **Built by [Mohamed Khairy](https://github.com/mohamed-khairy-5i).**
15
+ > If TossInbox saved you a signup form, consider starring the repo — it helps more people find it.
16
+
17
+ TossInbox is a temp-mail CLI and MCP server: it creates a brand-new throwaway
18
+ email address in one command, waits for the email verification to land, extracts
19
+ the OTP code, and deletes the inbox when you are done — server-side and locally.
20
+ Use it for signups, QA email flows, and test automation — or let your **AI
21
+ agent** do all of it through the built-in **MCP server**. No sign-up, no ads,
22
+ no browser, no API keys. Humans read the output; agents parse the `--json`.
23
+
24
+ <a href="https://tossinbox.pages.dev/"><strong>Website & docs → tossinbox.pages.dev</strong></a>
25
+
26
+ ## Demo
27
+
28
+ ```text
29
+ $ tossinbox spawn
30
+ ✔ Inbox ready : qwd6996p1lbc@uberip.com
31
+ provider : mailtm
32
+
33
+ $ tossinbox wait --code --from noreply@github.com --timeout 120
34
+ ✔ Verify your device
35
+ from : GitHub <noreply@github.com>
36
+ code : 9378412
37
+
38
+ $ tossinbox toss
39
+ ✔ tossed qwd6996p1lbc@uberip.com
40
+ ```
41
+
42
+ ## Why TossInbox
43
+
44
+ - **For humans** — stop exposing your real address to every signup form.
45
+ - **For agents** — built agent-first from day one:
46
+ - `--json` output on every command
47
+ - documented exit codes, no interactive prompts
48
+ - an [MCP server](https://modelcontextprotocol.io) so Claude, Cursor, and any
49
+ MCP client can create inboxes and read verification codes as native tools
50
+ - an `llms.txt` at the repository root for LLM-friendly onboarding
51
+
52
+ | | TossInbox | temp-mail websites | tmpmail-era CLIs |
53
+ |------------------------|-------------------------|---------------------|-----------------------------|
54
+ | JSON on every command | `--json` | no | rarely |
55
+ | Documented exit codes | 0–4 | none | no |
56
+ | MCP server for agents | yes, built in | no | no |
57
+ | Runs headless / in CI | yes | no | partial |
58
+ | Upstream alive | mail.tm + GuerrillaMail | varies | many wrap the dead 1secmail |
59
+ | Ads, trackers, popups | none | the business model | none |
60
+
61
+ Checked September 2026. If a cell is wrong, open an issue and win the argument.
62
+
63
+ ## Install
64
+
65
+ Requires Node.js 18+.
66
+
67
+ ```bash
68
+ # Homebrew (macOS, Linux)
69
+ brew install mohamed-khairy-5i/tap/tossinbox
70
+
71
+ # npm from GitHub (npm registry publish coming soon)
72
+ npm install -g github:mohamed-khairy-5i/tossinbox
73
+
74
+ # Or run without installing
75
+ npx github:mohamed-khairy-5i/tossinbox spawn
76
+ ```
77
+
78
+ From source:
79
+
80
+ ```bash
81
+ git clone https://github.com/mohamed-khairy-5i/tossinbox.git
82
+ cd tossinbox
83
+ npm install
84
+ npm run build
85
+ node dist/cli.js --help
86
+ ```
87
+
88
+ ## Quickstart
89
+
90
+ Four commands from zero to a tossed inbox:
91
+
92
+ ```bash
93
+ tossinbox providers # sanity check: install + network work
94
+ tossinbox spawn # create an inbox (saved locally)
95
+ tossinbox wait --code # block until a message arrives, print its OTP
96
+ tossinbox toss # delete the inbox server-side + wipe local state
97
+ ```
98
+
99
+ Every command also accepts `--json` for machine-readable output:
100
+
101
+ ```bash
102
+ tossinbox spawn --json
103
+ tossinbox wait --code --json
104
+ ```
105
+
106
+ ## CLI Reference
107
+
108
+ | Command | Description |
109
+ |---|---|
110
+ | `spawn` | Create a new disposable inbox (`-p provider`, `-l label`) |
111
+ | `list` | List messages (`-a address`) |
112
+ | `read <id>` | Read a full message, including any detected code |
113
+ | `wait` | Poll until a message arrives (`-f sender`, `-s subject`, `-c` extract code, `-t timeout` max 600s) |
114
+ | `inboxes` | List locally saved inboxes |
115
+ | `toss` | Delete an inbox server-side and remove it from local state (`--all` for every inbox) |
116
+ | `clear` | Remove all inboxes from local state only |
117
+ | `providers` | List available email providers |
118
+ | `mcp` | Run the MCP server over stdio |
119
+
120
+ ### Exit codes
121
+
122
+ A stable contract: agents script against these, not against stdout.
123
+
124
+ | Code | Meaning |
125
+ |---|---|
126
+ | `0` | Success |
127
+ | `1` | Error (provider / network / unexpected) |
128
+ | `2` | Timeout (`wait` expired without a matching message) |
129
+ | `3` | Not found (no saved inbox, unknown address, or message missing) |
130
+ | `4` | Usage error (bad flags, unknown command, or unknown provider) |
131
+
132
+ ## GitHub Action (email verification in CI)
133
+
134
+ Use TossInbox directly in your workflows to test real signup / verification
135
+ email flows:
136
+
137
+ ```yaml
138
+ - uses: mohamed-khairy-5i/tossinbox@v1
139
+ id: mail
140
+ with:
141
+ args: "wait --code --json"
142
+ timeout: "180"
143
+
144
+ - run: echo "Verification code: ${{ steps.mail.outputs.code }}"
145
+ ```
146
+
147
+ Outputs: `address` (the disposable inbox) and `code` (the extracted OTP).
148
+
149
+ ## MCP Server (for AI agents)
150
+
151
+ TossInbox ships with an MCP server exposing four tools:
152
+
153
+ | Tool | Description |
154
+ |---|---|
155
+ | `create_inbox` | Create a disposable inbox and return its address |
156
+ | `list_messages` | List messages in an inbox |
157
+ | `read_message` | Read a full message, including any detected code |
158
+ | `wait_for_code` | Poll until a message arrives and return its verification code |
159
+
160
+ ### Claude Desktop / Cursor / any MCP client
161
+
162
+ ```json
163
+ {
164
+ "mcpServers": {
165
+ "tossinbox": {
166
+ "command": "npx",
167
+ "args": ["-y", "github:mohamed-khairy-5i/tossinbox", "mcp"]
168
+ }
169
+ }
170
+ }
171
+ ```
172
+
173
+ Or after a global install, simply use `tossinbox-mcp` as the command.
174
+
175
+ ### Works with any AI agent
176
+
177
+ TossInbox is deliberately agent-agnostic — no lock-in to one vendor:
178
+
179
+ - **Any MCP client**: Claude Desktop, Claude Code, Cursor, Windsurf, Cline,
180
+ Codex CLI, Gemini CLI, and every other MCP-compatible client
181
+ - **Any shell-capable agent**: the CLI itself is the interface — `--json` on
182
+ every command, exit codes `0–4` documented, zero interactive prompts
183
+ - **CI/CD**: the GitHub Action above needs no agent at all
184
+
185
+ ## Using TossInbox with an agent (copy-paste flow)
186
+
187
+ ```text
188
+ 1. "Create a disposable inbox" -> tool: create_inbox
189
+ 2. "Sign up at example.com with this address"
190
+ 3. "Wait for the verification code from example.com"
191
+ -> tool: wait_for_code
192
+ 4. "Use code 482913 to finish the signup"
193
+ 5. "Toss the inbox when done" -> CLI: tossinbox toss
194
+ ```
195
+
196
+ ## Agent discovery surfaces
197
+
198
+ If you are an AI agent or LLM reading this: everything below is
199
+ machine-readable and kept up to date.
200
+
201
+ - [llms.txt](./llms.txt) at the repository root — full onboarding in one file
202
+ - [llms.txt on the website](https://tossinbox.pages.dev/llms.txt)
203
+ - [MCP Server Card](https://tossinbox.pages.dev/.well-known/mcp/server-card.json)
204
+ - [Agent skill (SKILL.md)](https://tossinbox.pages.dev/.well-known/agent-skills/tossinbox/SKILL.md)
205
+ · [skills index](https://tossinbox.pages.dev/.well-known/agent-skills/index.json)
206
+ - [API catalog (RFC 9727)](https://tossinbox.pages.dev/.well-known/api-catalog)
207
+ - [ARD manifest](https://tossinbox.pages.dev/.well-known/ard.json)
208
+ - Markdown mirrors of every docs page: send `Accept: text/markdown` to
209
+ [tossinbox.pages.dev](https://tossinbox.pages.dev/) or fetch `/index.md`,
210
+ `/quickstart.md`, `/cli.md`, `/agents.md`, `/faq.md`
211
+
212
+ ## Providers
213
+
214
+ | Provider | API key | Notes |
215
+ |---|---|---|
216
+ | `mailtm` (default) | not required | mail.tm — reliable, fast |
217
+ | `guerrillamail` | not required | GuerrillaMail — classic fallback |
218
+
219
+ Adding a provider means implementing a small interface (`createInbox`,
220
+ `listMessages`, `readMessage`, optional `destroyInbox`) — PRs welcome.
221
+
222
+ ## FAQ
223
+
224
+ **Is it really free?**
225
+ Yes. MIT-licensed, and both upstream providers are free with no API keys.
226
+
227
+ **Can it send email?**
228
+ No — receive-only by design. TossInbox exists for privacy and testing and
229
+ ships no bulk-send or bulk-signup mode.
230
+
231
+ **Does it work on Windows?**
232
+ Yes, anywhere Node.js 18+ runs. `npx github:mohamed-khairy-5i/tossinbox spawn`
233
+ works in PowerShell exactly the same.
234
+
235
+ **A site blocked my disposable address. What now?**
236
+ Some sites blocklist known disposable domains. Try the other provider:
237
+ `tossinbox spawn -p guerrillamail`. If both are blocked, the site wins that
238
+ round.
239
+
240
+ ## Privacy and safety
241
+
242
+ - The local state file (`~/.tossinbox/state.json`, override with
243
+ `TOSSINBOX_STATE`) contains provider tokens and is written with `0600`
244
+ permissions.
245
+ - `toss` deletes the account on the provider when supported, then wipes local
246
+ state.
247
+ - Disposable email is for privacy and testing — not for abuse. Please respect
248
+ each provider's terms of service.
249
+
250
+ ## Roadmap
251
+
252
+ - [x] GitHub Action: `mohamed-khairy-5i/tossinbox@v1`
253
+ - [x] Homebrew tap: `brew install mohamed-khairy-5i/tap/tossinbox`
254
+ - [x] Project website at [tossinbox.pages.dev](https://tossinbox.pages.dev/)
255
+ - [ ] Publish `tossinbox` + `tossinbox-mcp` to the npm registry
256
+ - [ ] `mail.gw` provider (mail.tm-compatible API — small lift)
257
+ - [ ] `tempmail.lol` provider (free API)
258
+ - [ ] Provider failover: auto-switch when a provider is down
259
+ - [ ] Homebrew core formula (after community adoption)
260
+
261
+ ## Documentation
262
+
263
+ - [Quickstart](https://tossinbox.pages.dev/quickstart) — first inbox in four commands
264
+ - [CLI reference](https://tossinbox.pages.dev/cli) — every command, flag, and exit code
265
+ - [Agents & MCP](https://tossinbox.pages.dev/agents) — setup for every MCP client
266
+ - [FAQ](https://tossinbox.pages.dev/faq) — privacy, providers, troubleshooting
267
+ - [Changelog](./CHANGELOG.md) · [Contributing](./CONTRIBUTING.md) · [Security](./SECURITY.md)
268
+
269
+ ## License
270
+
271
+ [MIT](./LICENSE) © Mohamed Khairy
package/dist/cli.js ADDED
@@ -0,0 +1,311 @@
1
+ #!/usr/bin/env node
2
+ import { Command, CommanderError } from "commander";
3
+ import { DEFAULT_PROVIDER, getProvider, listProviders, listSavedInboxes, removeInbox, clearInboxes, resolveInbox, saveInbox, statePath, waitForMessage, htmlToText, ProviderError, } from "./core/index.js";
4
+ import { VERSION } from "./version.js";
5
+ /* Documented exit codes:
6
+ * 0 success
7
+ * 1 error (provider / network / unexpected)
8
+ * 2 timeout (wait expired without a matching message)
9
+ * 3 not found (no saved inbox, unknown address, or message missing)
10
+ * 4 usage error (bad flags / unknown command / unknown provider)
11
+ */
12
+ const EXIT_OK = 0;
13
+ const EXIT_ERROR = 1;
14
+ const EXIT_TIMEOUT = 2;
15
+ const EXIT_NOT_FOUND = 3;
16
+ const EXIT_USAGE = 4;
17
+ const program = new Command();
18
+ program
19
+ .name("tossinbox")
20
+ .description("Disposable email inboxes for humans and AI agents. Spawn an inbox, wait for the OTP, toss it.")
21
+ .version(VERSION)
22
+ .option("--json", "machine-readable JSON output (agent-friendly)");
23
+ function jsonMode() {
24
+ return Boolean(program.opts().json);
25
+ }
26
+ function out(data) {
27
+ console.log(JSON.stringify(data, null, 2));
28
+ }
29
+ /** Resolve a provider by name; an unknown provider is a usage error (exit 4). */
30
+ function providerOrExit(name) {
31
+ try {
32
+ return getProvider(name);
33
+ }
34
+ catch (err) {
35
+ fail(err, EXIT_USAGE);
36
+ }
37
+ }
38
+ function fail(err, exitCode = EXIT_ERROR) {
39
+ const message = err instanceof Error ? err.message : String(err);
40
+ if (jsonMode()) {
41
+ out({ ok: false, error: message });
42
+ }
43
+ else {
44
+ console.error(`✖ ${message}`);
45
+ }
46
+ process.exit(exitCode);
47
+ }
48
+ function exitWith(code) {
49
+ process.exit(code);
50
+ }
51
+ function jsonMessage(message, includeHtml = false) {
52
+ return {
53
+ id: message.id,
54
+ from: message.from,
55
+ fromName: message.fromName,
56
+ subject: message.subject,
57
+ createdAt: message.createdAt,
58
+ code: message.code,
59
+ text: message.text ?? (message.html ? htmlToText(message.html) : undefined),
60
+ html: includeHtml ? message.html : undefined,
61
+ };
62
+ }
63
+ function printMessageHuman(message, withBody) {
64
+ console.log(`✔ ${message.subject}`);
65
+ console.log(` from : ${message.fromName ? `${message.fromName} <${message.from}>` : message.from}`);
66
+ if (message.createdAt)
67
+ console.log(` date : ${message.createdAt}`);
68
+ if (message.code)
69
+ console.log(` code : ${message.code}`);
70
+ if (withBody) {
71
+ const body = message.text ?? (message.html ? htmlToText(message.html) : "");
72
+ if (body)
73
+ console.log(`\n${body}\n`);
74
+ }
75
+ }
76
+ /* ------------------------------------------------------------------ */
77
+ /* commands */
78
+ /* ------------------------------------------------------------------ */
79
+ program
80
+ .command("spawn")
81
+ .description("Create a new disposable inbox")
82
+ .option("-p, --provider <name>", "email provider (see: providers)", DEFAULT_PROVIDER)
83
+ .option("-l, --label <label>", "optional label to identify this inbox")
84
+ .action(async (opts) => {
85
+ try {
86
+ const provider = providerOrExit(opts.provider);
87
+ const inbox = await provider.createInbox({ label: opts.label });
88
+ await saveInbox(inbox);
89
+ if (jsonMode()) {
90
+ out({ ok: true, inbox });
91
+ return;
92
+ }
93
+ console.log(`✔ Inbox ready : ${inbox.address}`);
94
+ console.log(` provider : ${inbox.provider}`);
95
+ if (inbox.label)
96
+ console.log(` label : ${inbox.label}`);
97
+ console.log(` state file : ${statePath()}`);
98
+ }
99
+ catch (err) {
100
+ if (err instanceof ProviderError && err.status && err.status >= 400 && err.status < 500 && err.status !== 429) {
101
+ fail(err, EXIT_ERROR);
102
+ }
103
+ fail(err);
104
+ }
105
+ });
106
+ program
107
+ .command("list")
108
+ .description("List messages in an inbox")
109
+ .option("-a, --address <address>", "inbox address (defaults to the most recent inbox)")
110
+ .action(async (opts) => {
111
+ try {
112
+ const inbox = await resolveInbox(opts.address);
113
+ if (!inbox)
114
+ fail(new Error("No saved inbox found. Run: tossinbox spawn"), EXIT_NOT_FOUND);
115
+ const provider = providerOrExit(inbox.provider);
116
+ const messages = await provider.listMessages(inbox);
117
+ if (jsonMode()) {
118
+ out({ ok: true, inbox: inbox.address, provider: inbox.provider, count: messages.length, messages });
119
+ return;
120
+ }
121
+ console.log(`✔ ${messages.length} message(s) in ${inbox.address}`);
122
+ messages.forEach((m, i) => {
123
+ console.log(` ${i + 1}) [${m.id}] ${m.subject}`);
124
+ console.log(` from: ${m.fromName ? `${m.fromName} <${m.from}>` : m.from}`);
125
+ if (m.intro)
126
+ console.log(` ${m.intro.slice(0, 100)}`);
127
+ });
128
+ }
129
+ catch (err) {
130
+ fail(err);
131
+ }
132
+ });
133
+ program
134
+ .command("read <id>")
135
+ .description("Read a full message by id")
136
+ .option("-a, --address <address>", "inbox address (defaults to the most recent inbox)")
137
+ .action(async (id, opts) => {
138
+ try {
139
+ const inbox = await resolveInbox(opts.address);
140
+ if (!inbox)
141
+ fail(new Error("No saved inbox found. Run: tossinbox spawn"), EXIT_NOT_FOUND);
142
+ const provider = providerOrExit(inbox.provider);
143
+ const message = await provider.readMessage(inbox, id);
144
+ if (jsonMode()) {
145
+ out({ ok: true, inbox: inbox.address, message: jsonMessage(message, true) });
146
+ return;
147
+ }
148
+ printMessageHuman(message, true);
149
+ }
150
+ catch (err) {
151
+ fail(err, err instanceof ProviderError && err.status === 404 ? EXIT_NOT_FOUND : EXIT_ERROR);
152
+ }
153
+ });
154
+ program
155
+ .command("wait")
156
+ .description("Wait for a message to arrive (optionally extract the OTP code)")
157
+ .option("-a, --address <address>", "inbox address (defaults to the most recent inbox)")
158
+ .option("-f, --from <sender>", "only match messages from this sender (substring)")
159
+ .option("-s, --subject <text>", "only match messages whose subject contains this text")
160
+ .option("-c, --code", "extract the verification code / OTP from the message")
161
+ .option("-t, --timeout <seconds>", "give up after this many seconds (max 600)", "120")
162
+ .option("-i, --interval <seconds>", "poll interval in seconds", "5")
163
+ .action(async (opts) => {
164
+ try {
165
+ const timeoutSeconds = Number(opts.timeout);
166
+ const intervalSeconds = Number(opts.interval);
167
+ if (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 0) {
168
+ fail(new Error(`Invalid --timeout "${opts.timeout}" (expected a number of seconds)`), EXIT_USAGE);
169
+ }
170
+ if (!Number.isFinite(intervalSeconds) || intervalSeconds < 0) {
171
+ fail(new Error(`Invalid --interval "${opts.interval}" (expected a number of seconds)`), EXIT_USAGE);
172
+ }
173
+ const inbox = await resolveInbox(opts.address);
174
+ if (!inbox)
175
+ fail(new Error("No saved inbox found. Run: tossinbox spawn"), EXIT_NOT_FOUND);
176
+ const provider = providerOrExit(inbox.provider);
177
+ const { timedOut, message } = await waitForMessage(provider, inbox, {
178
+ timeoutSeconds,
179
+ intervalSeconds,
180
+ from: opts.from,
181
+ subject: opts.subject,
182
+ });
183
+ if (timedOut || !message) {
184
+ const detail = `No matching message within ${opts.timeout}s`;
185
+ if (jsonMode())
186
+ out({ ok: false, error: "timeout", detail, inbox: inbox.address });
187
+ else
188
+ console.error(`✖ ${detail}`);
189
+ exitWith(EXIT_TIMEOUT);
190
+ }
191
+ if (jsonMode()) {
192
+ if (opts.code) {
193
+ if (message.code)
194
+ out({ ok: true, code: message.code, inbox: inbox.address, message: jsonMessage(message) });
195
+ else
196
+ out({ ok: false, error: "message arrived but no code was detected", message: jsonMessage(message) });
197
+ }
198
+ else {
199
+ out({ ok: true, inbox: inbox.address, message: jsonMessage(message) });
200
+ }
201
+ }
202
+ else {
203
+ printMessageHuman(message, false);
204
+ if (opts.code && !message.code) {
205
+ console.error("✖ Message arrived but no verification code was detected");
206
+ exitWith(EXIT_ERROR);
207
+ }
208
+ }
209
+ exitWith(opts.code && !message.code ? EXIT_ERROR : EXIT_OK);
210
+ }
211
+ catch (err) {
212
+ fail(err);
213
+ }
214
+ });
215
+ program
216
+ .command("inboxes")
217
+ .description("List locally saved inboxes")
218
+ .action(async () => {
219
+ const inboxes = await listSavedInboxes();
220
+ if (jsonMode()) {
221
+ out({ ok: true, count: inboxes.length, inboxes });
222
+ return;
223
+ }
224
+ console.log(`✔ ${inboxes.length} saved inbox(es)`);
225
+ inboxes.forEach((i) => {
226
+ console.log(` - ${i.address} (${i.provider}${i.label ? `, ${i.label}` : ""})`);
227
+ });
228
+ });
229
+ program
230
+ .command("toss")
231
+ .description("Delete an inbox (server-side when supported) and remove it from local state")
232
+ .option("-a, --address <address>", "inbox address (defaults to the most recent inbox)")
233
+ .option("--all", "toss every saved inbox")
234
+ .action(async (opts) => {
235
+ try {
236
+ const targets = [];
237
+ if (opts.all) {
238
+ targets.push(...(await listSavedInboxes()));
239
+ }
240
+ else {
241
+ const inbox = await resolveInbox(opts.address);
242
+ if (inbox)
243
+ targets.push(inbox);
244
+ }
245
+ if (targets.length === 0) {
246
+ fail(new Error("Nothing to toss"), EXIT_NOT_FOUND);
247
+ }
248
+ for (const inbox of targets) {
249
+ const provider = providerOrExit(inbox.provider);
250
+ if (provider.destroyInbox) {
251
+ await provider.destroyInbox(inbox);
252
+ }
253
+ await removeInbox(inbox.address);
254
+ }
255
+ if (jsonMode()) {
256
+ out({ ok: true, tossed: targets.map((t) => t.address) });
257
+ return;
258
+ }
259
+ targets.forEach((t) => console.log(`✔ tossed ${t.address}`));
260
+ exitWith(EXIT_OK);
261
+ }
262
+ catch (err) {
263
+ fail(err);
264
+ }
265
+ });
266
+ program
267
+ .command("clear")
268
+ .description("Remove all inboxes from local state (no server-side deletion)")
269
+ .action(async () => {
270
+ const count = await clearInboxes();
271
+ if (jsonMode())
272
+ out({ ok: true, removed: count });
273
+ else
274
+ console.log(`✔ removed ${count} inbox(es) from local state`);
275
+ exitWith(EXIT_OK);
276
+ });
277
+ program
278
+ .command("providers")
279
+ .description("List available email providers")
280
+ .action(async () => {
281
+ const all = listProviders();
282
+ if (jsonMode()) {
283
+ out({ ok: true, default: DEFAULT_PROVIDER, providers: all.map((p) => ({ name: p.name, description: p.description })) });
284
+ return;
285
+ }
286
+ console.log(`✔ available providers (default: ${DEFAULT_PROVIDER})`);
287
+ all.forEach((p) => console.log(` - ${p.name.padEnd(14)} ${p.description}`));
288
+ exitWith(EXIT_OK);
289
+ });
290
+ program
291
+ .command("mcp")
292
+ .description("Run the TossInbox MCP server over stdio (for Claude, Cursor, and other MCP clients)")
293
+ .action(async () => {
294
+ const { startMcpServer } = await import("./mcp.js");
295
+ await startMcpServer();
296
+ });
297
+ program.exitOverride();
298
+ // exitOverride is per-command: apply it to every subcommand too, so bad flags
299
+ // surface as CommanderError (-> exit 4) instead of commander's default exit 1.
300
+ for (const cmd of program.commands)
301
+ cmd.exitOverride();
302
+ program.parseAsync(process.argv).catch((err) => {
303
+ if (err instanceof CommanderError) {
304
+ // --help / --version exit cleanly even under exitOverride
305
+ if (err.code === "commander.helpDisplayed" || err.code === "commander.help" || err.code === "commander.version") {
306
+ exitWith(EXIT_OK);
307
+ }
308
+ fail(err, EXIT_USAGE); // bad flags / unknown command = usage error
309
+ }
310
+ fail(err);
311
+ });
@@ -0,0 +1,25 @@
1
+ import { mailTm } from "./providers/mailtm.js";
2
+ import { guerrillaMail } from "./providers/guerrillamail.js";
3
+ export * from "./types.js";
4
+ export { extractCode, htmlToText } from "./otp.js";
5
+ export * from "./state.js";
6
+ export { waitForMessage } from "./wait.js";
7
+ export const providers = {
8
+ [mailTm.name]: mailTm,
9
+ [guerrillaMail.name]: guerrillaMail,
10
+ };
11
+ export const DEFAULT_PROVIDER = mailTm.name;
12
+ export function getProvider(name) {
13
+ const key = (name || DEFAULT_PROVIDER).toLowerCase();
14
+ const provider = providers[key];
15
+ if (!provider) {
16
+ const known = Object.values(providers)
17
+ .map((p) => p.name)
18
+ .join(", ");
19
+ throw new Error(`Unknown provider "${name}". Available providers: ${known}`);
20
+ }
21
+ return provider;
22
+ }
23
+ export function listProviders() {
24
+ return Object.values(providers);
25
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Extract OTP / verification codes from message text or HTML.
3
+ *
4
+ * Strategy (in order):
5
+ * 1. On a line containing a code keyword, prefer a standalone token that
6
+ * looks like a code (4-8 digits, or 5-8 uppercase alphanumeric chars
7
+ * containing both letters and digits).
8
+ * 2. Fall back to the first standalone 4-8 digit number in the text.
9
+ */
10
+ const CODE_KEYWORDS = /\b(?:code|otp|passcode|pin|password.?code|verification|verify|confirm|activation|active.?code|one.?time)\b|رمز|كود|تفعيل|تحقق|الرمز/i;
11
+ export function htmlToText(html) {
12
+ return html
13
+ .replace(/<style[\s\S]*?<\/style>/gi, " ")
14
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
15
+ .replace(/<br\s*\/?>/gi, "\n")
16
+ .replace(/<\/(p|div|tr|h[1-6]|li)>/gi, "\n")
17
+ .replace(/<[^>]+>/g, " ")
18
+ .replace(/&nbsp;/gi, " ")
19
+ .replace(/&amp;/gi, "&")
20
+ .replace(/&lt;/gi, "<")
21
+ .replace(/&gt;/gi, ">")
22
+ .replace(/&quot;/gi, '"')
23
+ .replace(/&#0?39;/g, "'")
24
+ .replace(/[ \t]+/g, " ")
25
+ .trim();
26
+ }
27
+ export function extractCode(input) {
28
+ if (!input)
29
+ return undefined;
30
+ const text = /<[a-z!][\s\S]*>/i.test(input) ? htmlToText(input) : input;
31
+ if (!text)
32
+ return undefined;
33
+ const lines = text.split(/\r?\n/);
34
+ // 1) Keyword line -> prefer a code-shaped token on that line
35
+ for (const line of lines) {
36
+ if (!CODE_KEYWORDS.test(line))
37
+ continue;
38
+ // Case-insensitive on purpose: many services send lowercase codes (f4x9k2).
39
+ const tokens = line.match(/(?<![\w-])[A-Za-z0-9]{4,10}(?![\w-])/g);
40
+ if (!tokens)
41
+ continue;
42
+ for (const token of tokens) {
43
+ if (/^\d{4,8}$/.test(token))
44
+ return token;
45
+ const upper = token.toUpperCase();
46
+ if (/^[A-Z0-9]{5,8}$/.test(upper) && /\d/.test(upper) && /[A-Z]/.test(upper)) {
47
+ return upper;
48
+ }
49
+ }
50
+ }
51
+ // 2) Fallback: first standalone 4-8 digit number anywhere
52
+ const numeric = text.match(/(?<![\w-])(\d{4,8})(?![\w-])/);
53
+ if (numeric)
54
+ return numeric[1];
55
+ return undefined;
56
+ }
@@ -0,0 +1,82 @@
1
+ import { ProviderError } from "../types.js";
2
+ import { extractCode } from "../otp.js";
3
+ import { VERSION } from "../../version.js";
4
+ const BASE = "https://api.guerrillamail.com/ajax.php";
5
+ const REQUEST_TIMEOUT_MS = 20_000;
6
+ function timestampToIso(ts) {
7
+ if (!ts)
8
+ return undefined;
9
+ // guerrillamail timestamps are in seconds
10
+ return new Date(ts * 1000).toISOString();
11
+ }
12
+ async function call(params) {
13
+ const url = new URL(BASE);
14
+ for (const [k, v] of Object.entries(params))
15
+ url.searchParams.set(k, v);
16
+ // Hard timeout on every request — an agent must never hang forever.
17
+ const res = await fetch(url, {
18
+ headers: { Accept: "application/json", "User-Agent": `tossinbox/${VERSION}` },
19
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
20
+ });
21
+ if (!res.ok)
22
+ throw new ProviderError("guerrillamail", `HTTP ${res.status}`, res.status);
23
+ return (await res.json());
24
+ }
25
+ export const guerrillaMail = {
26
+ name: "guerrillamail",
27
+ description: "GuerrillaMail — classic disposable email, no API key required",
28
+ async createInbox(options) {
29
+ const data = await call({ f: "get_email_address", lang: "en" });
30
+ const addr = data;
31
+ if (!addr.email_addr || !addr.sid_token) {
32
+ throw new ProviderError(this.name, "Unexpected response while creating inbox");
33
+ }
34
+ const inbox = {
35
+ provider: this.name,
36
+ address: addr.email_addr,
37
+ label: options?.label,
38
+ session: addr.sid_token,
39
+ createdAt: new Date().toISOString(),
40
+ };
41
+ return inbox;
42
+ },
43
+ async listMessages(inbox) {
44
+ if (!inbox.session)
45
+ throw new ProviderError(this.name, "Inbox is missing its session token");
46
+ const data = await call({ f: "get_email_list", offset: "0", sid_token: inbox.session });
47
+ const list = data.list?.list ?? [];
48
+ return list.map((m) => ({
49
+ id: String(m.mail_id),
50
+ from: m.mail_from ?? "unknown",
51
+ subject: m.mail_subject ?? "(no subject)",
52
+ intro: m.mail_excerpt,
53
+ createdAt: timestampToIso(m.mail_timestamp),
54
+ }));
55
+ },
56
+ async readMessage(inbox, id) {
57
+ if (!inbox.session)
58
+ throw new ProviderError(this.name, "Inbox is missing its session token");
59
+ const data = await call({ f: "fetch_email", sid_token: inbox.session, email_id: id });
60
+ const m = data;
61
+ const message = {
62
+ id: String(m.mail_id ?? id),
63
+ from: m.mail_from ?? "unknown",
64
+ subject: m.mail_subject ?? "(no subject)",
65
+ intro: m.mail_excerpt,
66
+ createdAt: timestampToIso(m.mail_timestamp),
67
+ html: m.mail_body,
68
+ };
69
+ message.code = extractCode(message.html);
70
+ return message;
71
+ },
72
+ async destroyInbox(inbox) {
73
+ if (!inbox.session)
74
+ return;
75
+ try {
76
+ await call({ f: "forget_me", email_addr: inbox.address, sid_token: inbox.session });
77
+ }
78
+ catch {
79
+ // best effort
80
+ }
81
+ },
82
+ };
@@ -0,0 +1,150 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { ProviderError } from "../types.js";
3
+ import { extractCode } from "../otp.js";
4
+ import { VERSION } from "../../version.js";
5
+ const BASE = "https://api.mail.tm";
6
+ const REQUEST_TIMEOUT_MS = 20_000;
7
+ function randomString(length, alphabet) {
8
+ const bytes = randomBytes(length);
9
+ let out = "";
10
+ for (let i = 0; i < length; i++) {
11
+ out += alphabet[bytes[i] % alphabet.length];
12
+ }
13
+ return out;
14
+ }
15
+ function randomUser(length = 12) {
16
+ return randomString(length, "abcdefghijklmnopqrstuvwxyz0123456789");
17
+ }
18
+ function randomPassword(length = 16) {
19
+ return randomString(length, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789");
20
+ }
21
+ function headers(token) {
22
+ const h = {
23
+ Accept: "application/json",
24
+ "User-Agent": `tossinbox/${VERSION}`,
25
+ };
26
+ if (token)
27
+ h.Authorization = `Bearer ${token}`;
28
+ return h;
29
+ }
30
+ /** Hard timeout on every request — an agent must never hang forever. */
31
+ function fetchJson(url, init) {
32
+ return fetch(url, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
33
+ }
34
+ /** mail.tm allows ~8 requests per second; retry once on 429. */
35
+ async function request(method, url, options = {}) {
36
+ const init = {
37
+ method,
38
+ headers: { ...headers(options.token), ...(options.body ? { "Content-Type": "application/json" } : {}) },
39
+ body: options.body ? JSON.stringify(options.body) : undefined,
40
+ };
41
+ let res = await fetchJson(url, init);
42
+ if (res.status === 429) {
43
+ await new Promise((r) => setTimeout(r, 1200));
44
+ res = await fetchJson(url, init);
45
+ }
46
+ return res;
47
+ }
48
+ async function parseJson(res, provider) {
49
+ if (!res.ok) {
50
+ let detail = "";
51
+ try {
52
+ const body = (await res.json());
53
+ const msg = body["hydra:description"] ?? body.message ?? body.detail;
54
+ if (typeof msg === "string")
55
+ detail = `: ${msg}`;
56
+ }
57
+ catch {
58
+ // ignore body parse errors
59
+ }
60
+ throw new ProviderError(provider, `HTTP ${res.status}${detail}`, res.status);
61
+ }
62
+ return (await res.json());
63
+ }
64
+ /** mail.tm returns a plain array with Accept: application/json and a hydra
65
+ * collection with Accept: application/ld+json — normalize both. */
66
+ async function parseCollection(res, provider) {
67
+ const data = await parseJson(res, provider);
68
+ if (Array.isArray(data))
69
+ return data;
70
+ if (data && typeof data === "object") {
71
+ const obj = data;
72
+ const member = obj["hydra:member"] ?? obj.member;
73
+ if (Array.isArray(member))
74
+ return member;
75
+ }
76
+ throw new ProviderError(provider, "Unexpected collection response shape");
77
+ }
78
+ export const mailTm = {
79
+ name: "mailtm",
80
+ description: "mail.tm — free disposable email, no API key required",
81
+ async createInbox(options) {
82
+ const domainsRes = await request("GET", `${BASE}/domains?page=1`);
83
+ const domains = await parseCollection(domainsRes, this.name);
84
+ const domain = domains.find((d) => d.isActive && !d.isPrivate)?.domain;
85
+ if (!domain) {
86
+ throw new ProviderError(this.name, "No active public domain available on mail.tm");
87
+ }
88
+ const address = `${randomUser()}@${domain}`;
89
+ const password = randomPassword();
90
+ const accountRes = await request("POST", `${BASE}/accounts`, { body: { address, password } });
91
+ const account = await parseJson(accountRes, this.name);
92
+ const tokenRes = await request("POST", `${BASE}/token`, { body: { address, password } });
93
+ const auth = await parseJson(tokenRes, this.name);
94
+ const inbox = {
95
+ provider: this.name,
96
+ address: account.address || address,
97
+ label: options?.label,
98
+ token: auth.token,
99
+ password,
100
+ accountId: account.id || auth.id,
101
+ createdAt: new Date().toISOString(),
102
+ };
103
+ return inbox;
104
+ },
105
+ async listMessages(inbox) {
106
+ if (!inbox.token)
107
+ throw new ProviderError(this.name, "Inbox is missing its API token");
108
+ const res = await request("GET", `${BASE}/messages?page=1`, { token: inbox.token });
109
+ const data = await parseCollection(res, this.name);
110
+ return data.map((m) => ({
111
+ id: m.id,
112
+ from: m.from?.address ?? "unknown",
113
+ fromName: m.from?.name,
114
+ subject: m.subject ?? "(no subject)",
115
+ intro: m.intro,
116
+ createdAt: m.createdAt,
117
+ }));
118
+ },
119
+ async readMessage(inbox, id) {
120
+ if (!inbox.token)
121
+ throw new ProviderError(this.name, "Inbox is missing its API token");
122
+ const res = await request("GET", `${BASE}/messages/${encodeURIComponent(id)}`, { token: inbox.token });
123
+ const m = await parseJson(res, this.name);
124
+ const html = m.html && m.html.length > 0 ? m.html.join("\n") : undefined;
125
+ const message = {
126
+ id: m.id,
127
+ from: m.from?.address ?? "unknown",
128
+ fromName: m.from?.name,
129
+ subject: m.subject ?? "(no subject)",
130
+ intro: m.intro,
131
+ createdAt: m.createdAt,
132
+ text: m.text,
133
+ html,
134
+ };
135
+ message.code = extractCode(message.text) ?? extractCode(message.html);
136
+ return message;
137
+ },
138
+ async destroyInbox(inbox) {
139
+ if (!inbox.token || !inbox.accountId)
140
+ return;
141
+ try {
142
+ await request("DELETE", `${BASE}/accounts/${encodeURIComponent(inbox.accountId)}`, {
143
+ token: inbox.token,
144
+ });
145
+ }
146
+ catch {
147
+ // best effort: local removal always happens regardless
148
+ }
149
+ },
150
+ };
@@ -0,0 +1,69 @@
1
+ import { promises as fs } from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ export function statePath() {
5
+ return process.env.TOSSINBOX_STATE || path.join(os.homedir(), ".tossinbox", "state.json");
6
+ }
7
+ export async function loadState() {
8
+ let raw;
9
+ try {
10
+ raw = await fs.readFile(statePath(), "utf8");
11
+ }
12
+ catch (err) {
13
+ // Only a missing file means "no state yet" — anything else must be loud,
14
+ // otherwise the next write would silently destroy saved inboxes.
15
+ const code = err.code;
16
+ if (code === "ENOENT")
17
+ return { inboxes: [] };
18
+ throw new Error(`Cannot read state file ${statePath()} (${err.message}). ` +
19
+ "Fix its permissions or point TOSSINBOX_STATE at a writable path.");
20
+ }
21
+ try {
22
+ const parsed = JSON.parse(raw);
23
+ return { inboxes: Array.isArray(parsed.inboxes) ? parsed.inboxes : [] };
24
+ }
25
+ catch {
26
+ throw new Error(`State file ${statePath()} is not valid JSON. ` +
27
+ "Refusing to overwrite it — fix or delete the file manually (it may contain inboxes you still need).");
28
+ }
29
+ }
30
+ export async function saveInbox(inbox) {
31
+ const state = await loadState();
32
+ const filtered = state.inboxes.filter((i) => i.address !== inbox.address);
33
+ filtered.push(inbox);
34
+ await writeState({ inboxes: filtered });
35
+ }
36
+ export async function removeInbox(address) {
37
+ const state = await loadState();
38
+ const before = state.inboxes.length;
39
+ const filtered = state.inboxes.filter((i) => i.address !== address);
40
+ await writeState({ inboxes: filtered });
41
+ return filtered.length < before;
42
+ }
43
+ export async function clearInboxes() {
44
+ const state = await loadState();
45
+ const count = state.inboxes.length;
46
+ await writeState({ inboxes: [] });
47
+ return count;
48
+ }
49
+ export async function listSavedInboxes() {
50
+ const state = await loadState();
51
+ return [...state.inboxes].sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1));
52
+ }
53
+ /** Resolve the inbox to operate on: an explicit address, or the most recent one. */
54
+ export async function resolveInbox(address) {
55
+ const inboxes = await listSavedInboxes();
56
+ if (address) {
57
+ const needle = address.trim().toLowerCase();
58
+ return inboxes.find((i) => i.address.toLowerCase() === needle);
59
+ }
60
+ return inboxes.length > 0 ? inboxes[inboxes.length - 1] : undefined;
61
+ }
62
+ async function writeState(state) {
63
+ const file = statePath();
64
+ // The state file contains provider tokens — keep it private.
65
+ await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
66
+ await fs.writeFile(file, JSON.stringify(state, null, 2) + "\n", "utf8");
67
+ // writeFile's mode option only applies at creation — enforce on every write.
68
+ await fs.chmod(file, 0o600);
69
+ }
@@ -0,0 +1,10 @@
1
+ export class ProviderError extends Error {
2
+ status;
3
+ provider;
4
+ constructor(provider, message, status) {
5
+ super(message);
6
+ this.name = "ProviderError";
7
+ this.provider = provider;
8
+ this.status = status;
9
+ }
10
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Poll the inbox until a matching message arrives or the timeout expires.
3
+ * Transient provider errors are swallowed and retried until the deadline.
4
+ */
5
+ export async function waitForMessage(provider, inbox, options = {}) {
6
+ const timeoutSeconds = Math.max(1, Math.min(options.timeoutSeconds ?? 120, 600));
7
+ const intervalSeconds = Math.max(1, Math.min(options.intervalSeconds ?? 5, 60));
8
+ const deadline = Date.now() + timeoutSeconds * 1000;
9
+ while (Date.now() < deadline) {
10
+ let summaries;
11
+ try {
12
+ summaries = await provider.listMessages(inbox);
13
+ }
14
+ catch {
15
+ await sleep(Math.min(intervalSeconds * 1000, deadline - Date.now()));
16
+ continue;
17
+ }
18
+ const matched = summaries.find((m) => {
19
+ if (options.from) {
20
+ const hay = `${m.from} ${m.fromName ?? ""}`.toLowerCase();
21
+ if (!hay.includes(options.from.toLowerCase()))
22
+ return false;
23
+ }
24
+ if (options.subject && !m.subject.toLowerCase().includes(options.subject.toLowerCase())) {
25
+ return false;
26
+ }
27
+ return true;
28
+ });
29
+ if (matched) {
30
+ // One retry after a short delay — a transient read error should not
31
+ // throw away a wait that may have taken minutes.
32
+ try {
33
+ const message = await provider.readMessage(inbox, matched.id);
34
+ return { timedOut: false, message };
35
+ }
36
+ catch {
37
+ await sleep(1500);
38
+ const message = await provider.readMessage(inbox, matched.id);
39
+ return { timedOut: false, message };
40
+ }
41
+ }
42
+ const remaining = deadline - Date.now();
43
+ if (remaining <= 0)
44
+ break;
45
+ await sleep(Math.min(intervalSeconds * 1000, remaining));
46
+ }
47
+ return { timedOut: true };
48
+ }
49
+ export function sleep(ms) {
50
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
51
+ }
package/dist/mcp.js ADDED
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { z } from "zod";
8
+ import { DEFAULT_PROVIDER, getProvider, resolveInbox, saveInbox, waitForMessage, htmlToText, } from "./core/index.js";
9
+ import { VERSION } from "./version.js";
10
+ function text(result, isError = false) {
11
+ return {
12
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
13
+ ...(isError ? { isError: true } : {}),
14
+ };
15
+ }
16
+ function messageJson(message, includeHtml = false) {
17
+ return {
18
+ id: message.id,
19
+ from: message.from,
20
+ fromName: message.fromName,
21
+ subject: message.subject,
22
+ createdAt: message.createdAt,
23
+ code: message.code,
24
+ text: message.text ?? (message.html ? htmlToText(message.html) : undefined),
25
+ html: includeHtml ? message.html : undefined,
26
+ };
27
+ }
28
+ async function requireInbox(address) {
29
+ const inbox = await resolveInbox(address);
30
+ if (inbox)
31
+ return inbox;
32
+ return null;
33
+ }
34
+ export async function startMcpServer() {
35
+ const server = new McpServer({
36
+ name: "tossinbox",
37
+ version: VERSION,
38
+ });
39
+ server.registerTool("create_inbox", {
40
+ title: "Create a disposable inbox",
41
+ description: "Create a brand new disposable email inbox. The inbox is saved locally so the other tools can use it. Returns the full email address to use in sign-up forms.",
42
+ inputSchema: {
43
+ provider: z.string().optional().describe(`Provider name (default: "${DEFAULT_PROVIDER}", see the providers list)`),
44
+ label: z.string().optional().describe("Optional label to identify this inbox"),
45
+ },
46
+ }, async ({ provider, label }) => {
47
+ try {
48
+ const p = getProvider(provider);
49
+ const inbox = await p.createInbox({ label });
50
+ await saveInbox(inbox);
51
+ return text({
52
+ ok: true,
53
+ inbox: { address: inbox.address, provider: inbox.provider, label: inbox.label },
54
+ hint: `Use address "${inbox.address}" in the sign-up form, then call wait_for_code after submitting it.`,
55
+ });
56
+ }
57
+ catch (err) {
58
+ return text({ ok: false, error: err instanceof Error ? err.message : String(err) }, true);
59
+ }
60
+ });
61
+ server.registerTool("list_messages", {
62
+ title: "List inbox messages",
63
+ description: "List the messages currently in a disposable inbox (defaults to the most recently created inbox).",
64
+ inputSchema: {
65
+ address: z.string().optional().describe("Inbox address; defaults to the most recent inbox"),
66
+ },
67
+ }, async ({ address }) => {
68
+ try {
69
+ const inbox = await requireInbox(address);
70
+ if (!inbox)
71
+ return text({ ok: false, error: "No saved inbox found. Call create_inbox first." }, true);
72
+ const p = getProvider(inbox.provider);
73
+ const messages = await p.listMessages(inbox);
74
+ return text({ ok: true, inbox: inbox.address, count: messages.length, messages });
75
+ }
76
+ catch (err) {
77
+ return text({ ok: false, error: err instanceof Error ? err.message : String(err) }, true);
78
+ }
79
+ });
80
+ server.registerTool("read_message", {
81
+ title: "Read a message",
82
+ description: "Read the full body of a message by id, including any verification code detected in it.",
83
+ inputSchema: {
84
+ id: z.string().describe("Message id (from list_messages)"),
85
+ address: z.string().optional().describe("Inbox address; defaults to the most recent inbox"),
86
+ },
87
+ }, async ({ id, address }) => {
88
+ try {
89
+ const inbox = await requireInbox(address);
90
+ if (!inbox)
91
+ return text({ ok: false, error: "No saved inbox found. Call create_inbox first." }, true);
92
+ const p = getProvider(inbox.provider);
93
+ const message = await p.readMessage(inbox, id);
94
+ return text({ ok: true, message: messageJson(message, false) });
95
+ }
96
+ catch (err) {
97
+ return text({ ok: false, error: err instanceof Error ? err.message : String(err) }, true);
98
+ }
99
+ });
100
+ server.registerTool("wait_for_code", {
101
+ title: "Wait for a verification code",
102
+ description: "Poll a disposable inbox until a message arrives, then return the verification code (OTP) found in it. Ideal right after submitting a sign-up form. Times out gracefully.",
103
+ inputSchema: {
104
+ address: z.string().optional().describe("Inbox address; defaults to the most recent inbox"),
105
+ timeout_seconds: z.number().int().min(5).max(600).optional().describe("Max seconds to wait (default 120)"),
106
+ from: z.string().optional().describe("Only match messages from this sender (substring)"),
107
+ subject: z.string().optional().describe("Only match messages whose subject contains this text"),
108
+ },
109
+ }, async ({ address, timeout_seconds, from, subject }) => {
110
+ try {
111
+ const inbox = await requireInbox(address);
112
+ if (!inbox)
113
+ return text({ ok: false, error: "No saved inbox found. Call create_inbox first." }, true);
114
+ const p = getProvider(inbox.provider);
115
+ const { timedOut, message } = await waitForMessage(p, inbox, {
116
+ timeoutSeconds: timeout_seconds,
117
+ from,
118
+ subject,
119
+ });
120
+ if (timedOut || !message) {
121
+ return text({
122
+ ok: false,
123
+ error: "timeout",
124
+ detail: `No matching message within ${timeout_seconds ?? 120}s`,
125
+ inbox: inbox.address,
126
+ }, true);
127
+ }
128
+ if (!message.code) {
129
+ return text({
130
+ ok: false,
131
+ error: "message arrived but no code was detected",
132
+ message: messageJson(message, false),
133
+ }, true);
134
+ }
135
+ return text({ ok: true, code: message.code, inbox: inbox.address, message: messageJson(message, false) });
136
+ }
137
+ catch (err) {
138
+ return text({ ok: false, error: err instanceof Error ? err.message : String(err) }, true);
139
+ }
140
+ });
141
+ const transport = new StdioServerTransport();
142
+ await server.connect(transport);
143
+ // stdout is reserved for the MCP protocol; status goes to stderr.
144
+ console.error("tossinbox MCP server running on stdio");
145
+ }
146
+ /* When executed directly, start the server. This must also work when the file
147
+ * is launched through an npm/Homebrew bin symlink — resolve both paths before
148
+ * comparing, and fall back to the script name for exotic shim wrappers. */
149
+ function isDirectRun() {
150
+ if (!process.argv[1])
151
+ return false;
152
+ try {
153
+ const arg = fs.realpathSync(process.argv[1]);
154
+ const self = fs.realpathSync(fileURLToPath(import.meta.url));
155
+ if (arg === self)
156
+ return true;
157
+ return path.basename(arg) === "mcp.js";
158
+ }
159
+ catch {
160
+ return false;
161
+ }
162
+ }
163
+ if (isDirectRun()) {
164
+ startMcpServer().catch((err) => {
165
+ console.error(err);
166
+ process.exit(1);
167
+ });
168
+ }
@@ -0,0 +1,3 @@
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.1";
package/llms.txt ADDED
@@ -0,0 +1,52 @@
1
+ # TossInbox
2
+
3
+ > Disposable email inboxes for humans and AI agents. Spawn an inbox, wait for the OTP, toss it.
4
+
5
+ TossInbox creates disposable email addresses from the command line or through
6
+ an MCP server, collects verification codes (OTP) from them, and deletes the
7
+ inboxes when done. It is designed agent-first: every CLI command supports
8
+ `--json`, exit codes are documented, and there are no interactive prompts.
9
+
10
+ ## CLI (binary: `tossinbox`)
11
+
12
+ - `tossinbox spawn` — create a new disposable inbox (flags: `-p provider`, `-l label`)
13
+ - `tossinbox list` — list messages in an inbox (flag: `-a address`)
14
+ - `tossinbox read <id>` — read a full message by id
15
+ - `tossinbox wait --code` — poll until a message arrives and print its verification code
16
+ (flags: `-f sender`, `-s subject`, `-t timeout`, `-i interval`)
17
+ - `tossinbox inboxes` — list locally saved inboxes
18
+ - `tossinbox toss` — delete an inbox server-side and wipe local state (`--all` for all)
19
+ - `tossinbox providers` — list providers (default: `mailtm`, also `guerrillamail`)
20
+
21
+ All commands accept `--json`. Exit codes: 0 success, 1 error, 2 timeout,
22
+ 3 not found, 4 usage error (bad flags / unknown provider).
23
+
24
+ ## MCP server (binary: `tossinbox-mcp`, or `tossinbox mcp`)
25
+
26
+ Stdio MCP server exposing four tools:
27
+
28
+ - `create_inbox` — create a disposable inbox, returns its email address
29
+ - `list_messages` — list messages in an inbox
30
+ - `read_message` — read a full message including any detected code
31
+ - `wait_for_code` — poll until a message arrives and return its verification code
32
+
33
+ Typical agent flow: `create_inbox` -> use the address in a signup form ->
34
+ `wait_for_code` -> submit the code -> delete the inbox from the CLI with
35
+ `tossinbox toss`.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ npm install -g github:mohamed-khairy-5i/tossinbox
41
+ # or run without installing:
42
+ npx github:mohamed-khairy-5i/tossinbox spawn
43
+ ```
44
+
45
+ ## Notes
46
+
47
+ - Providers: `mailtm` (default) and `guerrillamail`; no API keys required.
48
+ - Local state is stored in `~/.tossinbox/state.json` (override with the
49
+ `TOSSINBOX_STATE` environment variable) with 0600 permissions.
50
+ - TossInbox intentionally has no bulk mode; use it for privacy and testing and
51
+ respect the providers' terms of service.
52
+ - License: MIT. Repository: https://github.com/mohamed-khairy-5i/tossinbox
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "tossinbox",
3
+ "version": "0.1.1",
4
+ "description": "Disposable email inboxes for humans and AI agents. Spawn an inbox, wait for the OTP, toss it.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Mohamed Khairy <mohamed-khairy-5i@users.noreply.github.com>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/mohamed-khairy-5i/tossinbox.git"
11
+ },
12
+ "homepage": "https://github.com/mohamed-khairy-5i/tossinbox#readme",
13
+ "bugs": "https://github.com/mohamed-khairy-5i/tossinbox/issues",
14
+ "keywords": [
15
+ "disposable-email",
16
+ "temporary-email",
17
+ "temp-mail",
18
+ "temp-email",
19
+ "throwaway-email",
20
+ "disposable-inbox",
21
+ "otp",
22
+ "otp-verification",
23
+ "verification-code",
24
+ "email-verification",
25
+ "email-automation",
26
+ "mcp",
27
+ "mcp-server",
28
+ "model-context-protocol",
29
+ "ai-agents",
30
+ "cli",
31
+ "cli-tool",
32
+ "developer-tools",
33
+ "privacy",
34
+ "testing"
35
+ ],
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "bin": {
40
+ "tossinbox": "dist/cli.js",
41
+ "tossinbox-mcp": "dist/mcp.js"
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "README.md",
46
+ "LICENSE",
47
+ "llms.txt"
48
+ ],
49
+ "scripts": {
50
+ "build": "tsc -p tsconfig.json",
51
+ "typecheck": "tsc -p tsconfig.json --noEmit",
52
+ "prepare": "npm run build"
53
+ },
54
+ "dependencies": {
55
+ "@modelcontextprotocol/sdk": "^1.12.0",
56
+ "commander": "^12.1.0",
57
+ "zod": "^3.24.0"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "^20.14.0",
61
+ "typescript": "^5.5.4"
62
+ }
63
+ }