tossinbox 0.1.4 → 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
@@ -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) |
@@ -239,6 +239,12 @@ ships no bulk-send or bulk-signup mode.
239
239
  Yes, anywhere Node.js 18+ runs. `npx tossinbox@latest spawn`
240
240
  works in PowerShell exactly the same.
241
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
+
242
248
  **A site blocked my disposable address. What now?**
243
249
  Some sites blocklist known disposable domains. Try the other provider:
244
250
  `tossinbox spawn -p guerrillamail`. If both are blocked, the site wins that
@@ -260,9 +266,9 @@ round.
260
266
  - [x] Homebrew tap: `brew install mohamed-khairy-5i/tap/tossinbox`
261
267
  - [x] Project website at [tossinbox.pages.dev](https://tossinbox.pages.dev/)
262
268
  - [x] Publish `tossinbox` + `tossinbox-mcp` to the [npm registry](https://www.npmjs.com/package/tossinbox)
263
- - [ ] `mail.gw` provider (mail.tm-compatible API — small lift)
264
- - [ ] `tempmail.lol` provider (free API)
265
- - [ ] 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)
266
272
  - [ ] Homebrew core formula (after community adoption)
267
273
 
268
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
+ }
@@ -8,6 +8,7 @@ export * from "./types.js";
8
8
  export { extractCode, htmlToText } from "./otp.js";
9
9
  export * from "./state.js";
10
10
  export { waitForMessage, sleep } from "./wait.js";
11
+ export { createInboxWithFailover } from "./failover.js";
11
12
  export const providers = {
12
13
  [mailTm.name]: mailTm,
13
14
  [mailGw.name]: mailGw,
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.4";
3
+ export const VERSION = "0.1.5";
package/llms.txt CHANGED
@@ -24,11 +24,13 @@ 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
36
  - `tossinbox providers` — list providers (default: `mailtm`; also `mailgw`, `guerrillamail`, `tempmaillol`, `tempmailio`, `tempmailplus`, `maildrop`)
@@ -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.4",
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",