tossinbox 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,7 +55,7 @@ $ tossinbox toss
55
55
  | Documented exit codes | 0–4 | none | no |
56
56
  | MCP server for agents | yes, built in | no | no |
57
57
  | Runs headless / in CI | yes | no | partial |
58
- | Upstream alive | mail.tm + mail.gw + GuerrillaMail | varies | many wrap the dead 1secmail |
58
+ | Upstream alive | 7 providers, 4 stacks | varies | many wrap the dead 1secmail |
59
59
  | Ads, trackers, popups | none | the business model | none |
60
60
 
61
61
  Checked September 2026. If a cell is wrong, open an issue and win the argument.
@@ -107,7 +107,7 @@ tossinbox wait --code --json
107
107
 
108
108
  | Command | Description |
109
109
  |---|---|
110
- | `spawn` | Create a new disposable inbox (`-p provider`, `-l label`) |
110
+ | `spawn` | Create a new disposable inbox (`-p provider`, `-l label`). If the provider is down, another one is used automatically — `--no-failover` opts out |
111
111
  | `list` | List messages (`-a address`) |
112
112
  | `read <id>` | Read a full message, including any detected code |
113
113
  | `wait` | Poll until a message arrives (`-f sender`, `-s subject`, `-c` extract code, `-t timeout` max 600s) |
@@ -218,6 +218,10 @@ machine-readable and kept up to date.
218
218
  | `mailtm` (default) | not required | mail.tm — reliable, fast |
219
219
  | `mailgw` | not required | mail.gw — mail.tm-compatible API on independent infrastructure |
220
220
  | `guerrillamail` | not required | GuerrillaMail — classic fallback |
221
+ | `tempmaillol` | not required | tempmail.lol — random inbox on rotating domains |
222
+ | `tempmailio` | not required | temp-mail.io — server-generated address, `toss` deletes server-side |
223
+ | `tempmailplus` | not required | tempmail.plus — pick-your-name inbox on 9 public domains |
224
+ | `maildrop` | not required | maildrop.cc — public inbox on one stable domain |
221
225
 
222
226
  Adding a provider means implementing a small interface (`createInbox`,
223
227
  `listMessages`, `readMessage`, optional `destroyInbox`) — PRs welcome.
@@ -225,7 +229,7 @@ Adding a provider means implementing a small interface (`createInbox`,
225
229
  ## FAQ
226
230
 
227
231
  **Is it really free?**
228
- Yes. MIT-licensed, and all three upstream providers are free with no API keys.
232
+ Yes. MIT-licensed, and all seven upstream providers are free with no API keys.
229
233
 
230
234
  **Can it send email?**
231
235
  No — receive-only by design. TossInbox exists for privacy and testing and
@@ -235,6 +239,12 @@ ships no bulk-send or bulk-signup mode.
235
239
  Yes, anywhere Node.js 18+ runs. `npx tossinbox@latest spawn`
236
240
  works in PowerShell exactly the same.
237
241
 
242
+ **What if a provider is down?**
243
+ `spawn` fails over automatically: it retries the create against the remaining
244
+ providers and reports the switch (human mode prints a `⚠` warning and
245
+ `provider : mailtm (failover from mailgw)`; `--json` returns a `failover`
246
+ object). Use `--no-failover` if you need the chosen provider or nothing.
247
+
238
248
  **A site blocked my disposable address. What now?**
239
249
  Some sites blocklist known disposable domains. Try the other provider:
240
250
  `tossinbox spawn -p guerrillamail`. If both are blocked, the site wins that
@@ -256,9 +266,9 @@ round.
256
266
  - [x] Homebrew tap: `brew install mohamed-khairy-5i/tap/tossinbox`
257
267
  - [x] Project website at [tossinbox.pages.dev](https://tossinbox.pages.dev/)
258
268
  - [x] Publish `tossinbox` + `tossinbox-mcp` to the [npm registry](https://www.npmjs.com/package/tossinbox)
259
- - [ ] `mail.gw` provider (mail.tm-compatible API — small lift)
260
- - [ ] `tempmail.lol` provider (free API)
261
- - [ ] Provider failover: auto-switch when a provider is down
269
+ - [x] `mail.gw` provider (v0.1.3)
270
+ - [x] Four more providers: `tempmail.lol`, `temp-mail.io`, `tempmail.plus`, `maildrop.cc` (v0.1.4)
271
+ - [x] Provider failover: auto-switch when a provider is down (v0.1.5)
262
272
  - [ ] Homebrew core formula (after community adoption)
263
273
 
264
274
  ## Documentation
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command, CommanderError } from "commander";
3
- import { DEFAULT_PROVIDER, getProvider, listProviders, listSavedInboxes, removeInbox, clearInboxes, resolveInbox, saveInbox, statePath, waitForMessage, sleep, htmlToText, ProviderError, } from "./core/index.js";
3
+ import { DEFAULT_PROVIDER, createInboxWithFailover, getProvider, listProviders, listSavedInboxes, removeInbox, clearInboxes, resolveInbox, saveInbox, statePath, waitForMessage, sleep, htmlToText, ProviderError, } from "./core/index.js";
4
4
  import { VERSION } from "./version.js";
5
5
  /* Documented exit codes:
6
6
  * 0 success
@@ -78,20 +78,32 @@ function printMessageHuman(message, withBody) {
78
78
  /* ------------------------------------------------------------------ */
79
79
  program
80
80
  .command("spawn")
81
- .description("Create a new disposable inbox")
81
+ .description("Create a new disposable inbox (automatically falls back to another provider when the chosen one is down)")
82
82
  .option("-p, --provider <name>", "email provider (see: providers)", DEFAULT_PROVIDER)
83
83
  .option("-l, --label <label>", "optional label to identify this inbox")
84
+ .option("--no-failover", "fail if the chosen provider is down instead of falling back to another one")
84
85
  .action(async (opts) => {
85
86
  try {
86
- const provider = providerOrExit(opts.provider);
87
- const inbox = await provider.createInbox({ label: opts.label });
87
+ providerOrExit(opts.provider); // unknown name = usage error before any network call
88
+ const { inbox, switched, warnings } = await createInboxWithFailover({
89
+ requested: opts.provider,
90
+ label: opts.label,
91
+ failover: opts.failover,
92
+ });
88
93
  await saveInbox(inbox);
89
94
  if (jsonMode()) {
90
- out({ ok: true, inbox });
95
+ out({
96
+ ok: true,
97
+ inbox,
98
+ ...(switched ? { failover: { requested: opts.provider, used: inbox.provider } } : {}),
99
+ ...(warnings.length > 0 ? { warnings } : {}),
100
+ });
91
101
  return;
92
102
  }
103
+ for (const warning of warnings)
104
+ console.error(`⚠ ${warning}`);
93
105
  console.log(`✔ Inbox ready : ${inbox.address}`);
94
- console.log(` provider : ${inbox.provider}`);
106
+ console.log(` provider : ${inbox.provider}${switched ? ` (failover from ${opts.provider})` : ""}`);
95
107
  if (inbox.label)
96
108
  console.log(` label : ${inbox.label}`);
97
109
  console.log(` state file : ${statePath()}`);
@@ -0,0 +1,66 @@
1
+ import { providers } from "./index.js";
2
+ import { ProviderError } from "./types.js";
3
+ /** A failure that automatic failover is allowed to recover from: network-level
4
+ * errors (no status) and server-side trouble (5xx) or throttling (429).
5
+ * A plain 4xx is a real request problem — switching providers cannot fix it. */
6
+ function isTransient(err) {
7
+ if (!(err instanceof ProviderError))
8
+ return true; // network-level → transient
9
+ const { status } = err;
10
+ return status === undefined || status === 429 || status >= 500;
11
+ }
12
+ function describe(err) {
13
+ return err instanceof Error ? err.message : String(err);
14
+ }
15
+ /**
16
+ * Create an inbox, falling back to other providers when the requested one is
17
+ * down. The requested provider is always tried first; the remaining providers
18
+ * follow registration order (best default first). Every failed attempt is
19
+ * recorded as a warning so humans and agents can see exactly what happened.
20
+ */
21
+ export async function createInboxWithFailover(options = {}) {
22
+ const failover = options.failover !== false;
23
+ const requestedName = options.requested ?? Object.keys(providers)[0];
24
+ const requested = providers[requestedName];
25
+ if (!requested) {
26
+ const known = Object.keys(providers).join(", ");
27
+ throw new Error(`Unknown provider "${requestedName}". Available providers: ${known}`);
28
+ }
29
+ // Registration order, requested provider first.
30
+ const candidates = [
31
+ requested,
32
+ ...Object.values(providers).filter((p) => p.name !== requested.name),
33
+ ];
34
+ const warnings = [];
35
+ let firstError;
36
+ for (let i = 0; i < candidates.length; i++) {
37
+ const candidate = candidates[i];
38
+ try {
39
+ const inbox = await candidate.createInbox({ label: options.label });
40
+ return {
41
+ inbox,
42
+ switched: i > 0,
43
+ warnings,
44
+ };
45
+ }
46
+ catch (err) {
47
+ firstError ??= err;
48
+ warnings.push(`${candidate.name}: ${describe(err)}`);
49
+ // A non-transient 4xx on the REQUESTED provider is a real request
50
+ // problem (bad payload, blocked domain…) — retrying others would just
51
+ // mask it. Only when the user did not explicitly pick a provider do we
52
+ // fall through anyway, because they never asked for this one by name.
53
+ if (!isTransient(err) && options.requested)
54
+ throw err;
55
+ if (!failover)
56
+ throw err;
57
+ }
58
+ }
59
+ // Every candidate failed. Re-throw the requested provider's original error
60
+ // (it names the provider the user actually asked for) with a failover note.
61
+ const base = describe(firstError);
62
+ const others = candidates.length - 1;
63
+ throw new ProviderError(requestedName, others > 0
64
+ ? `${base} — failover also tried ${others} other provider(s) without success`
65
+ : base);
66
+ }
@@ -1,13 +1,22 @@
1
1
  import { mailTm, mailGw } from "./providers/mailtm.js";
2
2
  import { guerrillaMail } from "./providers/guerrillamail.js";
3
+ import { tempmailLol } from "./providers/tempmaillol.js";
4
+ import { tempmailIo } from "./providers/tempmailio.js";
5
+ import { tempmailPlus } from "./providers/tempmailplus.js";
6
+ import { maildrop } from "./providers/maildrop.js";
3
7
  export * from "./types.js";
4
8
  export { extractCode, htmlToText } from "./otp.js";
5
9
  export * from "./state.js";
6
10
  export { waitForMessage, sleep } from "./wait.js";
11
+ export { createInboxWithFailover } from "./failover.js";
7
12
  export const providers = {
8
13
  [mailTm.name]: mailTm,
9
14
  [mailGw.name]: mailGw,
10
15
  [guerrillaMail.name]: guerrillaMail,
16
+ [tempmailLol.name]: tempmailLol,
17
+ [tempmailIo.name]: tempmailIo,
18
+ [tempmailPlus.name]: tempmailPlus,
19
+ [maildrop.name]: maildrop,
11
20
  };
12
21
  export const DEFAULT_PROVIDER = mailTm.name;
13
22
  export function getProvider(name) {
@@ -0,0 +1,87 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { ProviderError } from "../types.js";
3
+ import { extractCode } from "../otp.js";
4
+ import { networkError } from "../net.js";
5
+ import { VERSION } from "../../version.js";
6
+ const BASE = "https://api.maildrop.cc/graphql/graphql";
7
+ const HOST = "api.maildrop.cc";
8
+ const REQUEST_TIMEOUT_MS = 20_000;
9
+ function randomMailbox(length = 10) {
10
+ const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
11
+ const bytes = randomBytes(length);
12
+ let out = "";
13
+ for (let i = 0; i < length; i++)
14
+ out += alphabet[bytes[i] % alphabet.length];
15
+ return out;
16
+ }
17
+ async function gql(query, variables) {
18
+ const res = await fetch(BASE, {
19
+ method: "POST",
20
+ headers: {
21
+ Accept: "application/json",
22
+ "Content-Type": "application/json",
23
+ "User-Agent": `tossinbox/${VERSION}`,
24
+ },
25
+ body: JSON.stringify({ query, variables }),
26
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
27
+ }).catch((err) => {
28
+ throw networkError(err, "maildrop", HOST, REQUEST_TIMEOUT_MS);
29
+ });
30
+ if (res.status >= 500) {
31
+ throw new ProviderError("maildrop", `HTTP ${res.status} from ${HOST} — provider is down or having trouble; retry, or switch with --provider`, res.status);
32
+ }
33
+ const body = (await res.json().catch(() => undefined));
34
+ if (!body)
35
+ throw new ProviderError("maildrop", `HTTP ${res.status} — unexpected response`, res.status);
36
+ const firstError = body.errors?.[0]?.message;
37
+ if (firstError)
38
+ throw new ProviderError("maildrop", firstError, res.ok ? undefined : res.status);
39
+ if (!res.ok || !body.data) {
40
+ throw new ProviderError("maildrop", `HTTP ${res.status} — unexpected response`, res.status);
41
+ }
42
+ return body.data;
43
+ }
44
+ const INBOX_QUERY = `query ($mailbox: String!) { inbox(mailbox: $mailbox) { id headerfrom subject date } }`;
45
+ const MESSAGE_QUERY = `query ($mailbox: String!, $id: String!) { message(mailbox: $mailbox, id: $id) { id headerfrom subject date html } }`;
46
+ /** maildrop.cc inboxes are deterministic: every mailbox @maildrop.cc exists.
47
+ * Spawn mints a fresh random mailbox name; no account or auth is involved. */
48
+ export const maildrop = {
49
+ name: "maildrop",
50
+ description: "maildrop.cc — public inbox on one stable domain, no API key required",
51
+ async createInbox(options) {
52
+ const inbox = {
53
+ provider: this.name,
54
+ address: `${randomMailbox()}@maildrop.cc`,
55
+ label: options?.label,
56
+ createdAt: new Date().toISOString(),
57
+ };
58
+ return inbox;
59
+ },
60
+ async listMessages(inbox) {
61
+ const mailbox = inbox.address.split("@")[0] ?? "";
62
+ const data = await gql(INBOX_QUERY, { mailbox });
63
+ return (data.inbox ?? []).map((m) => ({
64
+ id: m.id,
65
+ from: m.headerfrom ?? "unknown",
66
+ subject: m.subject ?? "(no subject)",
67
+ createdAt: m.date,
68
+ }));
69
+ },
70
+ async readMessage(inbox, id) {
71
+ const mailbox = inbox.address.split("@")[0] ?? "";
72
+ const data = await gql(MESSAGE_QUERY, { mailbox, id });
73
+ const m = data.message;
74
+ if (!m) {
75
+ throw new ProviderError("maildrop", "Message not found — re-list messages to see what is currently in the inbox");
76
+ }
77
+ const message = {
78
+ id,
79
+ from: m.headerfrom ?? "unknown",
80
+ subject: m.subject ?? "(no subject)",
81
+ createdAt: m.date,
82
+ html: m.html,
83
+ };
84
+ message.code = extractCode(message.html);
85
+ return message;
86
+ },
87
+ };
@@ -0,0 +1,116 @@
1
+ import { ProviderError } from "../types.js";
2
+ import { extractCode } from "../otp.js";
3
+ import { networkError } from "../net.js";
4
+ import { VERSION } from "../../version.js";
5
+ const BASE = "https://api.internal.temp-mail.io";
6
+ const HOST = "api.internal.temp-mail.io";
7
+ const REQUEST_TIMEOUT_MS = 20_000;
8
+ async function call(method, path, body) {
9
+ const res = await fetch(`${BASE}${path}`, {
10
+ method,
11
+ headers: {
12
+ Accept: "application/json",
13
+ "User-Agent": `tossinbox/${VERSION}`,
14
+ ...(body ? { "Content-Type": "application/json" } : {}),
15
+ },
16
+ body: body ? JSON.stringify(body) : undefined,
17
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
18
+ }).catch((err) => {
19
+ throw networkError(err, "tempmailio", HOST, REQUEST_TIMEOUT_MS);
20
+ });
21
+ if (res.status >= 500) {
22
+ throw new ProviderError("tempmailio", `HTTP ${res.status} from ${HOST} — provider is down or having trouble; retry, or switch with --provider`, res.status);
23
+ }
24
+ return res;
25
+ }
26
+ async function callJson(method, path, body) {
27
+ const res = await call(method, path, body);
28
+ if (!res.ok) {
29
+ let detail = "";
30
+ try {
31
+ const data = (await res.json());
32
+ if (typeof data.message === "string")
33
+ detail = `: ${data.message}`;
34
+ }
35
+ catch {
36
+ // ignore body parse errors
37
+ }
38
+ throw new ProviderError("tempmailio", `HTTP ${res.status}${detail}`, res.status);
39
+ }
40
+ return (await res.json());
41
+ }
42
+ /** The messages endpoint returns full bodies, so readMessage re-lists and picks.
43
+ * A brand-new address can transiently 400 with "Email not found" right after
44
+ * creation (eventual consistency upstream) — retry once before giving up. */
45
+ async function fetchMessages(address) {
46
+ try {
47
+ return await callJson("GET", `/api/v3/email/${encodeURIComponent(address)}/messages`);
48
+ }
49
+ catch (err) {
50
+ if (err instanceof ProviderError && err.status === 400) {
51
+ await new Promise((r) => setTimeout(r, 1200));
52
+ return callJson("GET", `/api/v3/email/${encodeURIComponent(address)}/messages`);
53
+ }
54
+ throw err;
55
+ }
56
+ }
57
+ export const tempmailIo = {
58
+ name: "tempmailio",
59
+ description: "temp-mail.io — disposable email with 10+ rotating domains, no API key required",
60
+ async createInbox(options) {
61
+ const data = await callJson("POST", "/api/v3/email/new", { min_name_length: 10, max_name_length: 10 });
62
+ if (!data.email || !data.token) {
63
+ throw new ProviderError("tempmailio", "Unexpected response while creating inbox");
64
+ }
65
+ const inbox = {
66
+ provider: this.name,
67
+ address: data.email,
68
+ label: options?.label,
69
+ token: data.token,
70
+ createdAt: new Date().toISOString(),
71
+ };
72
+ return inbox;
73
+ },
74
+ async listMessages(inbox) {
75
+ const list = await fetchMessages(inbox.address);
76
+ return list.map((m, i) => ({
77
+ id: String(m.mail_id ?? m.id ?? i),
78
+ from: m.from ?? "unknown",
79
+ fromName: m.from_name,
80
+ subject: m.subject ?? "(no subject)",
81
+ intro: m.body_text ? m.body_text.slice(0, 120) : undefined,
82
+ createdAt: m.created_at,
83
+ }));
84
+ },
85
+ async readMessage(inbox, id) {
86
+ const list = await fetchMessages(inbox.address);
87
+ const email = list.find((m, i) => String(m.mail_id ?? m.id ?? i) === id);
88
+ if (!email) {
89
+ throw new ProviderError("tempmailio", "Message not found — re-list messages to see what is currently in the inbox");
90
+ }
91
+ const message = {
92
+ id,
93
+ from: email.from ?? "unknown",
94
+ fromName: email.from_name,
95
+ subject: email.subject ?? "(no subject)",
96
+ intro: email.body_text ? email.body_text.slice(0, 120) : undefined,
97
+ createdAt: email.created_at,
98
+ text: email.body_text,
99
+ html: email.body_html,
100
+ };
101
+ message.code = extractCode(message.text) ?? extractCode(message.html);
102
+ return message;
103
+ },
104
+ async destroyInbox(inbox) {
105
+ if (!inbox.token)
106
+ return;
107
+ try {
108
+ await call("DELETE", `/api/v3/email/${encodeURIComponent(inbox.address)}`, {
109
+ token: inbox.token,
110
+ });
111
+ }
112
+ catch {
113
+ // best effort: local removal always happens regardless
114
+ }
115
+ },
116
+ };
@@ -0,0 +1,99 @@
1
+ import { ProviderError } from "../types.js";
2
+ import { extractCode } from "../otp.js";
3
+ import { networkError } from "../net.js";
4
+ import { VERSION } from "../../version.js";
5
+ const BASE = "https://api.tempmail.lol";
6
+ const HOST = "api.tempmail.lol";
7
+ const REQUEST_TIMEOUT_MS = 20_000;
8
+ async function call(method, path, body) {
9
+ const res = await fetch(`${BASE}${path}`, {
10
+ method,
11
+ headers: {
12
+ Accept: "application/json",
13
+ "User-Agent": `tossinbox/${VERSION}`,
14
+ ...(body ? { "Content-Type": "application/json" } : {}),
15
+ },
16
+ body: body ? JSON.stringify(body) : undefined,
17
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
18
+ }).catch((err) => {
19
+ throw networkError(err, "tempmaillol", HOST, REQUEST_TIMEOUT_MS);
20
+ });
21
+ if (res.status >= 500) {
22
+ throw new ProviderError("tempmaillol", `HTTP ${res.status} from ${HOST} — provider is down or having trouble; retry, or switch with --provider`, res.status);
23
+ }
24
+ return res;
25
+ }
26
+ async function callJson(method, path, body) {
27
+ const res = await call(method, path, body);
28
+ if (!res.ok) {
29
+ let detail = "";
30
+ try {
31
+ const data = (await res.json());
32
+ if (typeof data.message === "string")
33
+ detail = `: ${data.message}`;
34
+ }
35
+ catch {
36
+ // ignore body parse errors
37
+ }
38
+ throw new ProviderError("tempmaillol", `HTTP ${res.status}${detail}`, res.status);
39
+ }
40
+ return (await res.json());
41
+ }
42
+ /** tempmail.lol list responses may omit per-email ids; synthesize a stable one */
43
+ function synthesizedId(email, index) {
44
+ return email.id ?? `${email.date ?? "undated"}#${index}`;
45
+ }
46
+ export const tempmailLol = {
47
+ name: "tempmaillol",
48
+ description: "tempmail.lol — random inbox with rotating domains, no API key required",
49
+ async createInbox(options) {
50
+ const data = await callJson("POST", "/v2/inbox/create");
51
+ if (!data.address || !data.token) {
52
+ throw new ProviderError("tempmaillol", "Unexpected response while creating inbox");
53
+ }
54
+ const inbox = {
55
+ provider: this.name,
56
+ address: data.address,
57
+ label: options?.label,
58
+ token: data.token,
59
+ createdAt: new Date().toISOString(),
60
+ };
61
+ return inbox;
62
+ },
63
+ async listMessages(inbox) {
64
+ if (!inbox.token)
65
+ throw new ProviderError("tempmaillol", "Inbox is missing its API token");
66
+ const data = await callJson("GET", `/v2/inbox?token=${encodeURIComponent(inbox.token)}`);
67
+ if (data.expired) {
68
+ throw new ProviderError("tempmaillol", "Inbox has expired on tempmail.lol — spawn a new one");
69
+ }
70
+ return (data.emails ?? []).map((e, i) => ({
71
+ id: synthesizedId(e, i),
72
+ from: e.from ?? "unknown",
73
+ subject: e.subject ?? "(no subject)",
74
+ createdAt: e.date,
75
+ }));
76
+ },
77
+ async readMessage(inbox, id) {
78
+ if (!inbox.token)
79
+ throw new ProviderError("tempmaillol", "Inbox is missing its API token");
80
+ const data = await callJson("GET", `/v2/inbox?token=${encodeURIComponent(inbox.token)}`);
81
+ if (data.expired) {
82
+ throw new ProviderError("tempmaillol", "Inbox has expired on tempmail.lol — spawn a new one");
83
+ }
84
+ const email = (data.emails ?? []).find((e, i) => synthesizedId(e, i) === id);
85
+ if (!email) {
86
+ throw new ProviderError("tempmaillol", "Message not found — it may have been dropped when the inbox rolled over; re-list messages");
87
+ }
88
+ const message = {
89
+ id,
90
+ from: email.from ?? "unknown",
91
+ subject: email.subject ?? "(no subject)",
92
+ createdAt: email.date,
93
+ text: email.body,
94
+ html: email.html,
95
+ };
96
+ message.code = extractCode(message.text) ?? extractCode(message.html);
97
+ return message;
98
+ },
99
+ };
@@ -0,0 +1,119 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { ProviderError } from "../types.js";
3
+ import { extractCode } from "../otp.js";
4
+ import { networkError } from "../net.js";
5
+ import { VERSION } from "../../version.js";
6
+ const BASE = "https://tempmail.plus";
7
+ const HOST = "tempmail.plus";
8
+ const REQUEST_TIMEOUT_MS = 20_000;
9
+ /** Public domains advertised by the tempmail.plus web client. The first one
10
+ * is the service default and is the one TossInbox spawns on. */
11
+ const DOMAIN = "mailto.plus";
12
+ function randomUser(length = 12) {
13
+ const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
14
+ const bytes = randomBytes(length);
15
+ let out = "";
16
+ for (let i = 0; i < length; i++)
17
+ out += alphabet[bytes[i] % alphabet.length];
18
+ return out;
19
+ }
20
+ function timeToIso(ts) {
21
+ if (!ts)
22
+ return undefined;
23
+ // Guard against seconds vs milliseconds epochs
24
+ return new Date(ts < 1e12 ? ts * 1000 : ts).toISOString();
25
+ }
26
+ async function call(method, path, body) {
27
+ const res = await fetch(`${BASE}${path}`, {
28
+ method,
29
+ headers: {
30
+ Accept: "application/json",
31
+ "User-Agent": `tossinbox/${VERSION}`,
32
+ ...(body ? { "Content-Type": "application/json" } : {}),
33
+ },
34
+ body: body ? JSON.stringify(body) : undefined,
35
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
36
+ }).catch((err) => {
37
+ throw networkError(err, "tempmailplus", HOST, REQUEST_TIMEOUT_MS);
38
+ });
39
+ if (res.status >= 500) {
40
+ throw new ProviderError("tempmailplus", `HTTP ${res.status} from ${HOST} — provider is down or having trouble; retry, or switch with --provider`, res.status);
41
+ }
42
+ return res;
43
+ }
44
+ async function callJson(method, path, body) {
45
+ const res = await call(method, path, body);
46
+ if (!res.ok) {
47
+ let detail = "";
48
+ try {
49
+ const data = (await res.json());
50
+ if (typeof data.message === "string")
51
+ detail = `: ${data.message}`;
52
+ }
53
+ catch {
54
+ // ignore body parse errors
55
+ }
56
+ throw new ProviderError("tempmailplus", `HTTP ${res.status}${detail}`, res.status);
57
+ }
58
+ return (await res.json());
59
+ }
60
+ /** tempmail.plus inboxes are deterministic: any address on a public domain
61
+ * exists and can be read by anyone who knows it. Spawn mints a fresh random
62
+ * local part; messages are then fetched by address. */
63
+ export const tempmailPlus = {
64
+ name: "tempmailplus",
65
+ description: "tempmail.plus — pick-your-name inbox on 9 public domains, no API key required",
66
+ async createInbox(options) {
67
+ const inbox = {
68
+ provider: this.name,
69
+ address: `${randomUser()}@${DOMAIN}`,
70
+ label: options?.label,
71
+ createdAt: new Date().toISOString(),
72
+ };
73
+ return inbox;
74
+ },
75
+ async listMessages(inbox) {
76
+ const data = await callJson("GET", `/api/mails/?email=${encodeURIComponent(inbox.address)}&first_id=0`);
77
+ return (data.mail_list ?? []).map((m) => ({
78
+ id: String(m.id),
79
+ from: m.from ?? "unknown",
80
+ fromName: m.from_name,
81
+ subject: m.subject ?? "(no subject)",
82
+ createdAt: timeToIso(m.time),
83
+ }));
84
+ },
85
+ async readMessage(inbox, id) {
86
+ const data = await callJson("GET", `/api/mails/${encodeURIComponent(id)}?email=${encodeURIComponent(inbox.address)}&first_id=-1`);
87
+ if (!data.result) {
88
+ throw new ProviderError("tempmailplus", "Message not found — re-list messages to see what is currently in the inbox");
89
+ }
90
+ const message = {
91
+ id,
92
+ from: data.from ?? "unknown",
93
+ fromName: data.from_name,
94
+ subject: data.subject ?? "(no subject)",
95
+ createdAt: timeToIso(data.time),
96
+ text: data.text,
97
+ html: data.html,
98
+ };
99
+ message.code = extractCode(message.text) ?? extractCode(message.html);
100
+ return message;
101
+ },
102
+ async destroyInbox(inbox) {
103
+ try {
104
+ const data = await callJson("GET", `/api/mails/?email=${encodeURIComponent(inbox.address)}&first_id=0`);
105
+ const list = data.mail_list ?? [];
106
+ if (list.length === 0)
107
+ return;
108
+ await call("DELETE", "/api/mails", {
109
+ email: inbox.address,
110
+ first_id: data.first_id ?? 0,
111
+ last_id: data.last_id ?? 0,
112
+ force: true,
113
+ });
114
+ }
115
+ catch {
116
+ // best effort: local removal always happens regardless
117
+ }
118
+ },
119
+ };
package/dist/mcp.js CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
7
  import { z } from "zod";
8
- import { DEFAULT_PROVIDER, getProvider, resolveInbox, saveInbox, waitForMessage, htmlToText, } from "./core/index.js";
8
+ import { DEFAULT_PROVIDER, createInboxWithFailover, getProvider, resolveInbox, saveInbox, waitForMessage, htmlToText, } from "./core/index.js";
9
9
  import { VERSION } from "./version.js";
10
10
  function text(result, isError = false) {
11
11
  return {
@@ -40,17 +40,24 @@ export async function startMcpServer() {
40
40
  title: "Create a disposable inbox",
41
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
42
  inputSchema: {
43
- provider: z.string().optional().describe(`Provider name (default: "${DEFAULT_PROVIDER}", see the providers list)`),
43
+ provider: z.string().optional().describe(`Provider name (default: "${DEFAULT_PROVIDER}", see the providers list). If the provider is down, another one is used automatically unless no_failover is set`),
44
44
  label: z.string().optional().describe("Optional label to identify this inbox"),
45
+ no_failover: z.boolean().optional().describe("Fail when the chosen provider is down instead of falling back to another one"),
45
46
  },
46
- }, async ({ provider, label }) => {
47
+ }, async ({ provider, label, no_failover }) => {
47
48
  try {
48
- const p = getProvider(provider);
49
- const inbox = await p.createInbox({ label });
49
+ getProvider(provider); // unknown name = clean error before any network call
50
+ const { inbox, switched, warnings } = await createInboxWithFailover({
51
+ requested: provider,
52
+ label,
53
+ failover: !no_failover,
54
+ });
50
55
  await saveInbox(inbox);
51
56
  return text({
52
57
  ok: true,
53
58
  inbox: { address: inbox.address, provider: inbox.provider, label: inbox.label },
59
+ ...(switched ? { failover: { requested: provider ?? DEFAULT_PROVIDER, used: inbox.provider } } : {}),
60
+ ...(warnings.length > 0 ? { warnings } : {}),
54
61
  hint: `Use address "${inbox.address}" in the sign-up form, then call wait_for_code after submitting it.`,
55
62
  });
56
63
  }
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  /** Single source of truth for the runtime version string.
2
2
  * Keep in sync with package.json — bump both on release. */
3
- export const VERSION = "0.1.3";
3
+ export const VERSION = "0.1.5";
package/llms.txt CHANGED
@@ -24,14 +24,16 @@ inboxes when done. It is designed agent-first: every CLI command supports
24
24
 
25
25
  ## CLI (binary: `tossinbox`)
26
26
 
27
- - `tossinbox spawn` — create a new disposable inbox (flags: `-p provider`, `-l label`)
27
+ - `tossinbox spawn` — create a new disposable inbox (flags: `-p provider`, `-l label`);
28
+ if the provider is down, another one is used automatically (`--no-failover` opts out)
28
29
  - `tossinbox list` — list messages in an inbox (flag: `-a address`)
29
30
  - `tossinbox read <id>` — read a full message by id
30
31
  - `tossinbox wait --code` — poll until a message arrives and print its verification code
31
32
  (flags: `-f sender`, `-s subject`, `-t timeout`, `-i interval`)
33
+ - `tossinbox watch` — stream new messages as they arrive until Ctrl-C (NDJSON with `--json`)
32
34
  - `tossinbox inboxes` — list locally saved inboxes
33
35
  - `tossinbox toss` — delete an inbox server-side and wipe local state (`--all` for all)
34
- - `tossinbox providers` — list providers (default: `mailtm`, also `guerrillamail`)
36
+ - `tossinbox providers` — list providers (default: `mailtm`; also `mailgw`, `guerrillamail`, `tempmaillol`, `tempmailio`, `tempmailplus`, `maildrop`)
35
37
 
36
38
  All commands accept `--json`. Exit codes: 0 success, 1 error, 2 timeout,
37
39
  3 not found, 4 usage error (bad flags / unknown provider).
@@ -59,7 +61,9 @@ npx tossinbox@latest spawn
59
61
 
60
62
  ## Notes
61
63
 
62
- - Providers: `mailtm` (default) and `guerrillamail`; no API keys required.
64
+ - Providers: `mailtm` (default), `mailgw`, `guerrillamail`, `tempmaillol`,
65
+ `tempmailio`, `tempmailplus`, `maildrop`; no API keys required. When a
66
+ provider is down, `spawn` fails over to another one automatically.
63
67
  - Local state is stored in `~/.tossinbox/state.json` (override with the
64
68
  `TOSSINBOX_STATE` environment variable) with 0600 permissions.
65
69
  - TossInbox intentionally has no bulk mode; use it for privacy and testing and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tossinbox",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Disposable email inboxes for humans and AI agents. Spawn an inbox, wait for the OTP, toss it.",
5
5
  "type": "module",
6
6
  "license": "MIT",