tossinbox 0.1.2 → 0.1.4
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 +8 -2
- package/dist/cli.js +116 -1
- package/dist/core/index.js +11 -2
- package/dist/core/otp.js +28 -2
- package/dist/core/providers/maildrop.js +87 -0
- package/dist/core/providers/mailtm.js +137 -118
- package/dist/core/providers/tempmailio.js +116 -0
- package/dist/core/providers/tempmaillol.js +99 -0
- package/dist/core/providers/tempmailplus.js +119 -0
- package/dist/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
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 |
|
|
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.
|
|
@@ -111,6 +111,7 @@ tossinbox wait --code --json
|
|
|
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) |
|
|
114
|
+
| `watch` | Stream new messages until Ctrl-C — only new arrivals; `--json` = one JSON object per line (NDJSON) |
|
|
114
115
|
| `inboxes` | List locally saved inboxes |
|
|
115
116
|
| `toss` | Delete an inbox server-side and remove it from local state (`--all` for every inbox) |
|
|
116
117
|
| `clear` | Remove all inboxes from local state only |
|
|
@@ -215,7 +216,12 @@ machine-readable and kept up to date.
|
|
|
215
216
|
| Provider | API key | Notes |
|
|
216
217
|
|---|---|---|
|
|
217
218
|
| `mailtm` (default) | not required | mail.tm — reliable, fast |
|
|
219
|
+
| `mailgw` | not required | mail.gw — mail.tm-compatible API on independent infrastructure |
|
|
218
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 |
|
|
219
225
|
|
|
220
226
|
Adding a provider means implementing a small interface (`createInbox`,
|
|
221
227
|
`listMessages`, `readMessage`, optional `destroyInbox`) — PRs welcome.
|
|
@@ -223,7 +229,7 @@ Adding a provider means implementing a small interface (`createInbox`,
|
|
|
223
229
|
## FAQ
|
|
224
230
|
|
|
225
231
|
**Is it really free?**
|
|
226
|
-
Yes. MIT-licensed, and
|
|
232
|
+
Yes. MIT-licensed, and all seven upstream providers are free with no API keys.
|
|
227
233
|
|
|
228
234
|
**Can it send email?**
|
|
229
235
|
No — receive-only by design. TossInbox exists for privacy and testing and
|
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, htmlToText, ProviderError, } from "./core/index.js";
|
|
3
|
+
import { DEFAULT_PROVIDER, 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
|
|
@@ -212,6 +212,121 @@ program
|
|
|
212
212
|
fail(err);
|
|
213
213
|
}
|
|
214
214
|
});
|
|
215
|
+
program
|
|
216
|
+
.command("watch")
|
|
217
|
+
.description("Stream new messages as they arrive — prints each message (and its code when found) and keeps polling until Ctrl-C")
|
|
218
|
+
.option("-a, --address <address>", "inbox address (defaults to the most recent inbox)")
|
|
219
|
+
.option("-f, --from <sender>", "only report messages from this sender (substring)")
|
|
220
|
+
.option("-s, --subject <text>", "only report messages whose subject contains this text")
|
|
221
|
+
.option("-i, --interval <seconds>", "poll interval in seconds", "5")
|
|
222
|
+
.action(async (opts) => {
|
|
223
|
+
try {
|
|
224
|
+
const intervalSeconds = Number(opts.interval);
|
|
225
|
+
if (!Number.isFinite(intervalSeconds) || intervalSeconds < 1 || intervalSeconds > 60) {
|
|
226
|
+
fail(new Error(`Invalid --interval "${opts.interval}" (expected seconds between 1 and 60)`), EXIT_USAGE);
|
|
227
|
+
}
|
|
228
|
+
const inbox = await resolveInbox(opts.address);
|
|
229
|
+
if (!inbox)
|
|
230
|
+
fail(new Error("No saved inbox found. Run: tossinbox spawn"), EXIT_NOT_FOUND);
|
|
231
|
+
const provider = providerOrExit(inbox.provider);
|
|
232
|
+
const matches = (m) => {
|
|
233
|
+
if (opts.from) {
|
|
234
|
+
const hay = `${m.from} ${m.fromName ?? ""}`.toLowerCase();
|
|
235
|
+
if (!hay.includes(opts.from.toLowerCase()))
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
if (opts.subject && !m.subject.toLowerCase().includes(opts.subject.toLowerCase()))
|
|
239
|
+
return false;
|
|
240
|
+
return true;
|
|
241
|
+
};
|
|
242
|
+
// Snapshot what is already in the inbox so only NEW arrivals are
|
|
243
|
+
// reported — re-watching an inbox after a wait must not replay history.
|
|
244
|
+
const seen = new Set();
|
|
245
|
+
try {
|
|
246
|
+
for (const m of await provider.listMessages(inbox))
|
|
247
|
+
seen.add(m.id);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// A failed first poll should not blind the watcher — start empty.
|
|
251
|
+
}
|
|
252
|
+
let stopped = false;
|
|
253
|
+
const stop = () => {
|
|
254
|
+
stopped = true;
|
|
255
|
+
};
|
|
256
|
+
process.on("SIGINT", stop);
|
|
257
|
+
process.on("SIGTERM", stop);
|
|
258
|
+
if (jsonMode()) {
|
|
259
|
+
// Streaming mode: one compact JSON object per line (NDJSON).
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
console.error(`👀 watching ${inbox.address} — Ctrl-C to stop`);
|
|
263
|
+
}
|
|
264
|
+
while (!stopped) {
|
|
265
|
+
let summaries;
|
|
266
|
+
try {
|
|
267
|
+
summaries = await provider.listMessages(inbox);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// Transient provider/network errors: keep polling until stopped.
|
|
271
|
+
}
|
|
272
|
+
if (summaries) {
|
|
273
|
+
for (const summary of summaries) {
|
|
274
|
+
if (seen.has(summary.id) || !matches(summary))
|
|
275
|
+
continue;
|
|
276
|
+
seen.add(summary.id);
|
|
277
|
+
// One retry — a transient read error must not swallow a message
|
|
278
|
+
// that may have taken minutes to arrive.
|
|
279
|
+
let message;
|
|
280
|
+
try {
|
|
281
|
+
message = await provider.readMessage(inbox, summary.id);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
await sleep(1500);
|
|
285
|
+
try {
|
|
286
|
+
message = await provider.readMessage(inbox, summary.id);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
// Report from the summary rather than dropping the event.
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (jsonMode()) {
|
|
293
|
+
const event = {
|
|
294
|
+
ok: true,
|
|
295
|
+
event: "message",
|
|
296
|
+
inbox: inbox.address,
|
|
297
|
+
provider: inbox.provider,
|
|
298
|
+
id: summary.id,
|
|
299
|
+
from: summary.from,
|
|
300
|
+
fromName: summary.fromName,
|
|
301
|
+
subject: summary.subject,
|
|
302
|
+
createdAt: summary.createdAt,
|
|
303
|
+
code: message?.code,
|
|
304
|
+
text: message?.text ?? (message?.html ? htmlToText(message.html) : undefined),
|
|
305
|
+
};
|
|
306
|
+
console.log(JSON.stringify(event));
|
|
307
|
+
}
|
|
308
|
+
else if (message) {
|
|
309
|
+
printMessageHuman(message, false);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
console.log(`✔ new message: ${summary.subject}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Sleep in small slices so Ctrl-C feels instant.
|
|
317
|
+
const deadline = Date.now() + intervalSeconds * 1000;
|
|
318
|
+
while (!stopped && Date.now() < deadline) {
|
|
319
|
+
await sleep(Math.min(200, deadline - Date.now()));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (!jsonMode())
|
|
323
|
+
console.error("✔ watch stopped");
|
|
324
|
+
exitWith(EXIT_OK);
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
fail(err);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
215
330
|
program
|
|
216
331
|
.command("inboxes")
|
|
217
332
|
.description("List locally saved inboxes")
|
package/dist/core/index.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import { mailTm } from "./providers/mailtm.js";
|
|
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
|
-
export { waitForMessage } from "./wait.js";
|
|
10
|
+
export { waitForMessage, sleep } from "./wait.js";
|
|
7
11
|
export const providers = {
|
|
8
12
|
[mailTm.name]: mailTm,
|
|
13
|
+
[mailGw.name]: mailGw,
|
|
9
14
|
[guerrillaMail.name]: guerrillaMail,
|
|
15
|
+
[tempmailLol.name]: tempmailLol,
|
|
16
|
+
[tempmailIo.name]: tempmailIo,
|
|
17
|
+
[tempmailPlus.name]: tempmailPlus,
|
|
18
|
+
[maildrop.name]: maildrop,
|
|
10
19
|
};
|
|
11
20
|
export const DEFAULT_PROVIDER = mailTm.name;
|
|
12
21
|
export function getProvider(name) {
|
package/dist/core/otp.js
CHANGED
|
@@ -6,8 +6,34 @@
|
|
|
6
6
|
* looks like a code (4-8 digits, or 5-8 uppercase alphanumeric chars
|
|
7
7
|
* containing both letters and digits).
|
|
8
8
|
* 2. Fall back to the first standalone 4-8 digit number in the text.
|
|
9
|
+
*
|
|
10
|
+
* Keywords cover English plus the languages verification mail actually
|
|
11
|
+
* arrives in: Arabic, French, Spanish, German, Portuguese, Italian, Russian,
|
|
12
|
+
* Turkish, Chinese, Japanese, and Korean. Latin-script words use \b
|
|
13
|
+
* boundaries; scripts where \b is meaningless (Arabic, Cyrillic, CJK) match
|
|
14
|
+
* bare, same as the existing Arabic handling.
|
|
9
15
|
*/
|
|
10
|
-
const CODE_KEYWORDS = /\b(?:code|otp|passcode|pin|password.?code|verification|verify|confirm|activation|active.?code|one.?time)\b
|
|
16
|
+
const CODE_KEYWORDS = /\b(?:code|otp|passcode|pin|password.?code|verification|verify|confirm|activation|active.?code|one.?time|code.?de.?confirmation)\b/ // en + fr "code de confirmation"
|
|
17
|
+
.source +
|
|
18
|
+
"|" +
|
|
19
|
+
[
|
|
20
|
+
// Arabic
|
|
21
|
+
"رمز", "كود", "تفعيل", "تحقق", "الرمز",
|
|
22
|
+
// French
|
|
23
|
+
"vérification", "vérifier", "confirmer", "confirmation",
|
|
24
|
+
// Spanish / Portuguese / Italian (shared words folded)
|
|
25
|
+
"código", "verificación", "verificar", "confirme", "confirmação", "verificação",
|
|
26
|
+
"codice", "verifica", "conferma",
|
|
27
|
+
// German
|
|
28
|
+
"bestätigung", "verifizierung", "bestätigungscode",
|
|
29
|
+
// Russian (Cyrillic — no \b)
|
|
30
|
+
"код", "подтверждение", "верификация",
|
|
31
|
+
// Turkish
|
|
32
|
+
"doğrulama", "onay.?kodu",
|
|
33
|
+
// Chinese / Japanese / Korean (CJK — no \b)
|
|
34
|
+
"验证码", "校验码", "确认码", "確認コード", "認証コード", "検証コード", "認証番号", "인증코드", "인증 번호",
|
|
35
|
+
].join("|");
|
|
36
|
+
const CODE_KEYWORDS_RE = new RegExp(CODE_KEYWORDS, "i");
|
|
11
37
|
export function htmlToText(html) {
|
|
12
38
|
return html
|
|
13
39
|
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
@@ -33,7 +59,7 @@ export function extractCode(input) {
|
|
|
33
59
|
const lines = text.split(/\r?\n/);
|
|
34
60
|
// 1) Keyword line -> prefer a code-shaped token on that line
|
|
35
61
|
for (const line of lines) {
|
|
36
|
-
if (!
|
|
62
|
+
if (!CODE_KEYWORDS_RE.test(line))
|
|
37
63
|
continue;
|
|
38
64
|
// Case-insensitive on purpose: many services send lowercase codes (f4x9k2).
|
|
39
65
|
const tokens = line.match(/(?<![\w-])[A-Za-z0-9]{4,10}(?![\w-])/g);
|
|
@@ -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
|
+
};
|
|
@@ -3,7 +3,6 @@ import { ProviderError } from "../types.js";
|
|
|
3
3
|
import { extractCode } from "../otp.js";
|
|
4
4
|
import { networkError } from "../net.js";
|
|
5
5
|
import { VERSION } from "../../version.js";
|
|
6
|
-
const BASE = "https://api.mail.tm";
|
|
7
6
|
const REQUEST_TIMEOUT_MS = 20_000;
|
|
8
7
|
function randomString(length, alphabet) {
|
|
9
8
|
const bytes = randomBytes(length);
|
|
@@ -28,128 +27,148 @@ function headers(token) {
|
|
|
28
27
|
h.Authorization = `Bearer ${token}`;
|
|
29
28
|
return h;
|
|
30
29
|
}
|
|
31
|
-
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const init = {
|
|
42
|
-
method,
|
|
43
|
-
headers: { ...headers(options.token), ...(options.body ? { "Content-Type": "application/json" } : {}) },
|
|
44
|
-
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
45
|
-
};
|
|
46
|
-
let res = await fetchJson(url, init);
|
|
47
|
-
if (res.status === 429) {
|
|
48
|
-
await new Promise((r) => setTimeout(r, 1200));
|
|
49
|
-
res = await fetchJson(url, init);
|
|
30
|
+
/** mail.tm and mail.gw expose the identical API (mail.gw is an independent
|
|
31
|
+
* infrastructure running the same software), so one factory serves both.
|
|
32
|
+
* Every request gets a hard timeout and raw network failures are translated
|
|
33
|
+
* into readable, actionable ProviderErrors naming the right provider. */
|
|
34
|
+
function createMailTmLikeProvider(config) {
|
|
35
|
+
const { name: providerName, base, host } = config;
|
|
36
|
+
function fetchJson(url, init) {
|
|
37
|
+
return fetch(url, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) }).catch((err) => {
|
|
38
|
+
throw networkError(err, providerName, host, REQUEST_TIMEOUT_MS);
|
|
39
|
+
});
|
|
50
40
|
}
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
41
|
+
/** These APIs allow ~8 requests per second; retry once on 429. */
|
|
42
|
+
async function request(method, url, options = {}) {
|
|
43
|
+
const init = {
|
|
44
|
+
method,
|
|
45
|
+
headers: { ...headers(options.token), ...(options.body ? { "Content-Type": "application/json" } : {}) },
|
|
46
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
47
|
+
};
|
|
48
|
+
let res = await fetchJson(url, init);
|
|
49
|
+
if (res.status === 429) {
|
|
50
|
+
await new Promise((r) => setTimeout(r, 1200));
|
|
51
|
+
res = await fetchJson(url, init);
|
|
61
52
|
}
|
|
62
|
-
|
|
63
|
-
|
|
53
|
+
return res;
|
|
54
|
+
}
|
|
55
|
+
async function parseJson(res) {
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
// 5xx = the upstream itself is failing — say so and point at the fix.
|
|
58
|
+
if (res.status >= 500) {
|
|
59
|
+
throw new ProviderError(providerName, `HTTP ${res.status} from ${host} — provider is down or having trouble; retry, or switch with --provider`, res.status);
|
|
60
|
+
}
|
|
61
|
+
let detail = "";
|
|
62
|
+
try {
|
|
63
|
+
const body = (await res.json());
|
|
64
|
+
const msg = body["hydra:description"] ?? body.message ?? body.detail;
|
|
65
|
+
if (typeof msg === "string")
|
|
66
|
+
detail = `: ${msg}`;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// ignore body parse errors
|
|
70
|
+
}
|
|
71
|
+
throw new ProviderError(providerName, `HTTP ${res.status}${detail}`, res.status);
|
|
64
72
|
}
|
|
65
|
-
|
|
73
|
+
return (await res.json());
|
|
66
74
|
}
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
/** These APIs return a plain array with Accept: application/json and a hydra
|
|
76
|
+
* collection with Accept: application/ld+json — normalize both. */
|
|
77
|
+
async function parseCollection(res) {
|
|
78
|
+
const data = await parseJson(res);
|
|
79
|
+
if (Array.isArray(data))
|
|
80
|
+
return data;
|
|
81
|
+
if (data && typeof data === "object") {
|
|
82
|
+
const obj = data;
|
|
83
|
+
const member = obj["hydra:member"] ?? obj.member;
|
|
84
|
+
if (Array.isArray(member))
|
|
85
|
+
return member;
|
|
86
|
+
}
|
|
87
|
+
throw new ProviderError(providerName, "Unexpected collection response shape");
|
|
80
88
|
}
|
|
81
|
-
|
|
89
|
+
return {
|
|
90
|
+
name: providerName,
|
|
91
|
+
description: config.description,
|
|
92
|
+
async createInbox(options) {
|
|
93
|
+
const domainsRes = await request("GET", `${base}/domains?page=1`);
|
|
94
|
+
const domains = await parseCollection(domainsRes);
|
|
95
|
+
const domain = domains.find((d) => d.isActive && !d.isPrivate)?.domain;
|
|
96
|
+
if (!domain) {
|
|
97
|
+
throw new ProviderError(providerName, `No active public domain available on ${host}`);
|
|
98
|
+
}
|
|
99
|
+
const address = `${randomUser()}@${domain}`;
|
|
100
|
+
const password = randomPassword();
|
|
101
|
+
const accountRes = await request("POST", `${base}/accounts`, { body: { address, password } });
|
|
102
|
+
const account = await parseJson(accountRes);
|
|
103
|
+
const tokenRes = await request("POST", `${base}/token`, { body: { address, password } });
|
|
104
|
+
const auth = await parseJson(tokenRes);
|
|
105
|
+
const inbox = {
|
|
106
|
+
provider: providerName,
|
|
107
|
+
address: account.address || address,
|
|
108
|
+
label: options?.label,
|
|
109
|
+
token: auth.token,
|
|
110
|
+
password,
|
|
111
|
+
accountId: account.id || auth.id,
|
|
112
|
+
createdAt: new Date().toISOString(),
|
|
113
|
+
};
|
|
114
|
+
return inbox;
|
|
115
|
+
},
|
|
116
|
+
async listMessages(inbox) {
|
|
117
|
+
if (!inbox.token)
|
|
118
|
+
throw new ProviderError(providerName, "Inbox is missing its API token");
|
|
119
|
+
const res = await request("GET", `${base}/messages?page=1`, { token: inbox.token });
|
|
120
|
+
const data = await parseCollection(res);
|
|
121
|
+
return data.map((m) => ({
|
|
122
|
+
id: m.id,
|
|
123
|
+
from: m.from?.address ?? "unknown",
|
|
124
|
+
fromName: m.from?.name,
|
|
125
|
+
subject: m.subject ?? "(no subject)",
|
|
126
|
+
intro: m.intro,
|
|
127
|
+
createdAt: m.createdAt,
|
|
128
|
+
}));
|
|
129
|
+
},
|
|
130
|
+
async readMessage(inbox, id) {
|
|
131
|
+
if (!inbox.token)
|
|
132
|
+
throw new ProviderError(providerName, "Inbox is missing its API token");
|
|
133
|
+
const res = await request("GET", `${base}/messages/${encodeURIComponent(id)}`, { token: inbox.token });
|
|
134
|
+
const m = await parseJson(res);
|
|
135
|
+
const html = m.html && m.html.length > 0 ? m.html.join("\n") : undefined;
|
|
136
|
+
const message = {
|
|
137
|
+
id: m.id,
|
|
138
|
+
from: m.from?.address ?? "unknown",
|
|
139
|
+
fromName: m.from?.name,
|
|
140
|
+
subject: m.subject ?? "(no subject)",
|
|
141
|
+
intro: m.intro,
|
|
142
|
+
createdAt: m.createdAt,
|
|
143
|
+
text: m.text,
|
|
144
|
+
html,
|
|
145
|
+
};
|
|
146
|
+
message.code = extractCode(message.text) ?? extractCode(message.html);
|
|
147
|
+
return message;
|
|
148
|
+
},
|
|
149
|
+
async destroyInbox(inbox) {
|
|
150
|
+
if (!inbox.token || !inbox.accountId)
|
|
151
|
+
return;
|
|
152
|
+
try {
|
|
153
|
+
await request("DELETE", `${base}/accounts/${encodeURIComponent(inbox.accountId)}`, {
|
|
154
|
+
token: inbox.token,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// best effort: local removal always happens regardless
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
};
|
|
82
162
|
}
|
|
83
|
-
export const mailTm = {
|
|
163
|
+
export const mailTm = createMailTmLikeProvider({
|
|
84
164
|
name: "mailtm",
|
|
165
|
+
base: "https://api.mail.tm",
|
|
166
|
+
host: "api.mail.tm",
|
|
85
167
|
description: "mail.tm — free disposable email, no API key required",
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const address = `${randomUser()}@${domain}`;
|
|
94
|
-
const password = randomPassword();
|
|
95
|
-
const accountRes = await request("POST", `${BASE}/accounts`, { body: { address, password } });
|
|
96
|
-
const account = await parseJson(accountRes, this.name);
|
|
97
|
-
const tokenRes = await request("POST", `${BASE}/token`, { body: { address, password } });
|
|
98
|
-
const auth = await parseJson(tokenRes, this.name);
|
|
99
|
-
const inbox = {
|
|
100
|
-
provider: this.name,
|
|
101
|
-
address: account.address || address,
|
|
102
|
-
label: options?.label,
|
|
103
|
-
token: auth.token,
|
|
104
|
-
password,
|
|
105
|
-
accountId: account.id || auth.id,
|
|
106
|
-
createdAt: new Date().toISOString(),
|
|
107
|
-
};
|
|
108
|
-
return inbox;
|
|
109
|
-
},
|
|
110
|
-
async listMessages(inbox) {
|
|
111
|
-
if (!inbox.token)
|
|
112
|
-
throw new ProviderError(this.name, "Inbox is missing its API token");
|
|
113
|
-
const res = await request("GET", `${BASE}/messages?page=1`, { token: inbox.token });
|
|
114
|
-
const data = await parseCollection(res, this.name);
|
|
115
|
-
return data.map((m) => ({
|
|
116
|
-
id: m.id,
|
|
117
|
-
from: m.from?.address ?? "unknown",
|
|
118
|
-
fromName: m.from?.name,
|
|
119
|
-
subject: m.subject ?? "(no subject)",
|
|
120
|
-
intro: m.intro,
|
|
121
|
-
createdAt: m.createdAt,
|
|
122
|
-
}));
|
|
123
|
-
},
|
|
124
|
-
async readMessage(inbox, id) {
|
|
125
|
-
if (!inbox.token)
|
|
126
|
-
throw new ProviderError(this.name, "Inbox is missing its API token");
|
|
127
|
-
const res = await request("GET", `${BASE}/messages/${encodeURIComponent(id)}`, { token: inbox.token });
|
|
128
|
-
const m = await parseJson(res, this.name);
|
|
129
|
-
const html = m.html && m.html.length > 0 ? m.html.join("\n") : undefined;
|
|
130
|
-
const message = {
|
|
131
|
-
id: m.id,
|
|
132
|
-
from: m.from?.address ?? "unknown",
|
|
133
|
-
fromName: m.from?.name,
|
|
134
|
-
subject: m.subject ?? "(no subject)",
|
|
135
|
-
intro: m.intro,
|
|
136
|
-
createdAt: m.createdAt,
|
|
137
|
-
text: m.text,
|
|
138
|
-
html,
|
|
139
|
-
};
|
|
140
|
-
message.code = extractCode(message.text) ?? extractCode(message.html);
|
|
141
|
-
return message;
|
|
142
|
-
},
|
|
143
|
-
async destroyInbox(inbox) {
|
|
144
|
-
if (!inbox.token || !inbox.accountId)
|
|
145
|
-
return;
|
|
146
|
-
try {
|
|
147
|
-
await request("DELETE", `${BASE}/accounts/${encodeURIComponent(inbox.accountId)}`, {
|
|
148
|
-
token: inbox.token,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
catch {
|
|
152
|
-
// best effort: local removal always happens regardless
|
|
153
|
-
}
|
|
154
|
-
},
|
|
155
|
-
};
|
|
168
|
+
});
|
|
169
|
+
export const mailGw = createMailTmLikeProvider({
|
|
170
|
+
name: "mailgw",
|
|
171
|
+
base: "https://api.mail.gw",
|
|
172
|
+
host: "api.mail.gw",
|
|
173
|
+
description: "mail.gw — mail.tm-compatible API on independent infrastructure",
|
|
174
|
+
});
|
|
@@ -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/version.js
CHANGED
package/llms.txt
CHANGED
|
@@ -31,7 +31,7 @@ inboxes when done. It is designed agent-first: every CLI command supports
|
|
|
31
31
|
(flags: `-f sender`, `-s subject`, `-t timeout`, `-i interval`)
|
|
32
32
|
- `tossinbox inboxes` — list locally saved inboxes
|
|
33
33
|
- `tossinbox toss` — delete an inbox server-side and wipe local state (`--all` for all)
|
|
34
|
-
- `tossinbox providers` — list providers (default: `mailtm
|
|
34
|
+
- `tossinbox providers` — list providers (default: `mailtm`; also `mailgw`, `guerrillamail`, `tempmaillol`, `tempmailio`, `tempmailplus`, `maildrop`)
|
|
35
35
|
|
|
36
36
|
All commands accept `--json`. Exit codes: 0 success, 1 error, 2 timeout,
|
|
37
37
|
3 not found, 4 usage error (bad flags / unknown provider).
|