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 +21 -0
- package/README.md +271 -0
- package/dist/cli.js +311 -0
- package/dist/core/index.js +25 -0
- package/dist/core/otp.js +56 -0
- package/dist/core/providers/guerrillamail.js +82 -0
- package/dist/core/providers/mailtm.js +150 -0
- package/dist/core/state.js +69 -0
- package/dist/core/types.js +10 -0
- package/dist/core/wait.js +51 -0
- package/dist/mcp.js +168 -0
- package/dist/version.js +3 -0
- package/llms.txt +52 -0
- package/package.json +63 -0
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
|
+
[](https://github.com/mohamed-khairy-5i/tossinbox/actions/workflows/ci.yml)
|
|
10
|
+

|
|
11
|
+

|
|
12
|
+

|
|
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
|
+
}
|
package/dist/core/otp.js
ADDED
|
@@ -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(/ /gi, " ")
|
|
19
|
+
.replace(/&/gi, "&")
|
|
20
|
+
.replace(/</gi, "<")
|
|
21
|
+
.replace(/>/gi, ">")
|
|
22
|
+
.replace(/"/gi, '"')
|
|
23
|
+
.replace(/�?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,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
|
+
}
|
package/dist/version.js
ADDED
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
|
+
}
|