tempmail-sdk 1.1.2 → 1.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 +43 -38
- package/demo/poll-emails.ts +290 -28
- package/dist/config.d.ts +16 -0
- package/dist/config.js +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +33 -16
- package/dist/providers/awamail.js +4 -3
- package/dist/providers/chatgpt-org-uk.d.ts +1 -1
- package/dist/providers/chatgpt-org-uk.js +188 -20
- package/dist/providers/dropmail.js +135 -3
- package/dist/providers/guerrillamail.js +4 -3
- package/dist/providers/linshi-email.d.ts +1 -1
- package/dist/providers/linshi-email.js +19 -7
- package/dist/providers/linshi-token.d.ts +25 -0
- package/dist/providers/linshi-token.js +69 -0
- package/dist/providers/mail-tm.js +43 -25
- package/dist/providers/maildrop.js +3 -2
- package/dist/providers/smail-pw.d.ts +9 -0
- package/dist/providers/smail-pw.js +356 -0
- package/dist/providers/temp-mail-io.js +5 -4
- package/dist/providers/tempmail-lol.js +4 -3
- package/dist/providers/tempmail.js +4 -3
- package/dist/retry.d.ts +2 -10
- package/dist/retry.js +41 -10
- package/dist/types.d.ts +6 -1
- package/dist/types.js +1 -1
- package/package.json +1 -1
- package/src/config.ts +16 -0
- package/src/index.ts +31 -14
- package/src/providers/awamail.ts +3 -2
- package/src/providers/chatgpt-org-uk.ts +213 -22
- package/src/providers/dropmail.ts +162 -2
- package/src/providers/guerrillamail.ts +3 -2
- package/src/providers/linshi-email.ts +24 -7
- package/src/providers/linshi-token.ts +86 -0
- package/src/providers/mail-tm.ts +43 -24
- package/src/providers/maildrop.ts +2 -1
- package/src/providers/smail-pw.ts +382 -0
- package/src/providers/temp-mail-io.ts +4 -3
- package/src/providers/tempmail-lol.ts +3 -2
- package/src/providers/tempmail.ts +3 -2
- package/src/retry.ts +42 -9
- package/src/types.ts +6 -1
- package/test/example.ts +183 -4
- package/dist/providers/tempmail-la.d.ts +0 -15
- package/dist/providers/tempmail-la.js +0 -89
- package/src/providers/tempmail-la.ts +0 -99
|
@@ -1,47 +1,161 @@
|
|
|
1
1
|
import { InternalEmailInfo, Email, Channel } from '../types';
|
|
2
2
|
import { normalizeEmail } from '../normalize';
|
|
3
|
+
import { fetchWithTimeout } from '../retry';
|
|
3
4
|
|
|
4
5
|
const CHANNEL: Channel = 'chatgpt-org-uk';
|
|
5
6
|
const BASE_URL = 'https://mail.chatgpt.org.uk/api';
|
|
7
|
+
const HOME_URL = 'https://mail.chatgpt.org.uk/';
|
|
6
8
|
|
|
7
9
|
const DEFAULT_HEADERS = {
|
|
8
|
-
'User-Agent':
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
'sec-ch-ua-platform': '"Windows"',
|
|
15
|
-
'DNT': '1',
|
|
10
|
+
'User-Agent':
|
|
11
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
|
|
12
|
+
Accept: '*/*',
|
|
13
|
+
Referer: 'https://mail.chatgpt.org.uk/',
|
|
14
|
+
Origin: 'https://mail.chatgpt.org.uk',
|
|
15
|
+
DNT: '1',
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
/** 从首页 HTML 解析 window.__BROWSER_AUTH(服务端注入的会话 JWT,供 X-Inbox-Token) */
|
|
19
|
+
function extractBrowserAuthToken(html: string): string {
|
|
20
|
+
const m = html.match(/__BROWSER_AUTH\s*=\s*(\{[\s\S]*?\})\s*;/);
|
|
21
|
+
if (!m) {
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const o = JSON.parse(m[1]) as { token?: string };
|
|
26
|
+
return typeof o.token === 'string' ? o.token : '';
|
|
27
|
+
} catch {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractGmSid(response: Response): string {
|
|
33
|
+
const h = response.headers as Headers & { getSetCookie?: () => string[] };
|
|
34
|
+
if (typeof h.getSetCookie === 'function') {
|
|
35
|
+
for (const line of h.getSetCookie()) {
|
|
36
|
+
const match = line.match(/^gm_sid=([^;]+)/);
|
|
37
|
+
if (match) {
|
|
38
|
+
return match[1];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const setCookie = response.headers.get('set-cookie') || '';
|
|
43
|
+
const match = setCookie.match(/gm_sid=([^;]+)/);
|
|
44
|
+
return match ? match[1] : '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function fetchHomeSession(): Promise<{ gmSid: string; browserToken: string }> {
|
|
48
|
+
const response = await fetchWithTimeout(HOME_URL, {
|
|
20
49
|
method: 'GET',
|
|
21
50
|
headers: DEFAULT_HEADERS,
|
|
22
51
|
});
|
|
23
52
|
|
|
24
53
|
if (!response.ok) {
|
|
25
|
-
throw new Error(`Failed to
|
|
54
|
+
throw new Error(`Failed to load mail.chatgpt.org.uk: ${response.status}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const html = await response.text();
|
|
58
|
+
const gmSid = extractGmSid(response);
|
|
59
|
+
const browserToken = extractBrowserAuthToken(html);
|
|
60
|
+
|
|
61
|
+
if (!gmSid) {
|
|
62
|
+
throw new Error('Failed to extract gm_sid cookie');
|
|
63
|
+
}
|
|
64
|
+
if (!browserToken) {
|
|
65
|
+
throw new Error('Failed to extract __BROWSER_AUTH from homepage (API now requires browser session)');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { gmSid, browserToken };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function fetchHomeSessionWithRetry(): Promise<{ gmSid: string; browserToken: string }> {
|
|
72
|
+
try {
|
|
73
|
+
return await fetchHomeSession();
|
|
74
|
+
} catch (error: any) {
|
|
75
|
+
const message = String(error?.message || error || '').toLowerCase();
|
|
76
|
+
if (message.includes('401') || message.includes('429') || message.includes('extract')) {
|
|
77
|
+
return await fetchHomeSession();
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sleepMs(ms: number): Promise<void> {
|
|
84
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function fetchInboxToken(email: string, gmSid: string): Promise<string> {
|
|
88
|
+
const response = await fetchWithTimeout(`${BASE_URL}/inbox-token`, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: {
|
|
91
|
+
...DEFAULT_HEADERS,
|
|
92
|
+
'Content-Type': 'application/json',
|
|
93
|
+
Cookie: `gm_sid=${gmSid}`,
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify({ email }),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
throw new Error(`Failed to get inbox token: ${response.status}`);
|
|
26
100
|
}
|
|
27
101
|
|
|
28
102
|
const data = await response.json();
|
|
29
|
-
|
|
30
|
-
if (!
|
|
31
|
-
throw new Error('Failed to
|
|
103
|
+
const token = data?.auth?.token;
|
|
104
|
+
if (!token) {
|
|
105
|
+
throw new Error('Failed to get inbox token');
|
|
32
106
|
}
|
|
33
107
|
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
108
|
+
return token;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function fetchInboxTokenWithRetry(email: string, gmSid: string): Promise<string> {
|
|
112
|
+
try {
|
|
113
|
+
return await fetchInboxToken(email, gmSid);
|
|
114
|
+
} catch (error: any) {
|
|
115
|
+
const message = String(error?.message || error || '').toLowerCase();
|
|
116
|
+
if (message.includes('401')) {
|
|
117
|
+
const { gmSid: sid } = await fetchHomeSessionWithRetry();
|
|
118
|
+
return await fetchInboxToken(email, sid);
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
38
122
|
}
|
|
39
123
|
|
|
40
|
-
|
|
124
|
+
/** 列表接口需同时带 Cookie gm_sid 与 x-inbox-token,否则返回 401 */
|
|
125
|
+
function parseChatgptPackedToken(packed: string): { gmSid: string; inbox: string } {
|
|
126
|
+
const t = packed.trim();
|
|
127
|
+
if (t.startsWith('{')) {
|
|
128
|
+
try {
|
|
129
|
+
const o = JSON.parse(t) as { gmSid?: string; inbox?: string };
|
|
130
|
+
if (typeof o.gmSid === 'string' && typeof o.inbox === 'string') {
|
|
131
|
+
return { gmSid: o.gmSid, inbox: o.inbox };
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
/* ignore */
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { gmSid: '', inbox: packed };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function fetchEmails(
|
|
141
|
+
inboxToken: string,
|
|
142
|
+
email: string,
|
|
143
|
+
gmSid: string,
|
|
144
|
+
): Promise<Email[]> {
|
|
145
|
+
if (!inboxToken) {
|
|
146
|
+
throw new Error('internal error: token missing for chatgpt-org-uk');
|
|
147
|
+
}
|
|
148
|
+
if (!gmSid) {
|
|
149
|
+
throw new Error('internal error: gm_sid missing for chatgpt-org-uk');
|
|
150
|
+
}
|
|
41
151
|
const encodedEmail = encodeURIComponent(email);
|
|
42
|
-
const response = await
|
|
152
|
+
const response = await fetchWithTimeout(`${BASE_URL}/emails?email=${encodedEmail}`, {
|
|
43
153
|
method: 'GET',
|
|
44
|
-
headers:
|
|
154
|
+
headers: {
|
|
155
|
+
...DEFAULT_HEADERS,
|
|
156
|
+
Cookie: `gm_sid=${gmSid}`,
|
|
157
|
+
'x-inbox-token': inboxToken,
|
|
158
|
+
},
|
|
45
159
|
});
|
|
46
160
|
|
|
47
161
|
if (!response.ok) {
|
|
@@ -49,7 +163,7 @@ export async function getEmails(email: string): Promise<Email[]> {
|
|
|
49
163
|
}
|
|
50
164
|
|
|
51
165
|
const data = await response.json();
|
|
52
|
-
|
|
166
|
+
|
|
53
167
|
if (!data.success) {
|
|
54
168
|
throw new Error('Failed to get emails');
|
|
55
169
|
}
|
|
@@ -57,3 +171,80 @@ export async function getEmails(email: string): Promise<Email[]> {
|
|
|
57
171
|
const rawEmails = data.data?.emails || [];
|
|
58
172
|
return rawEmails.map((raw: any) => normalizeEmail(raw, email));
|
|
59
173
|
}
|
|
174
|
+
|
|
175
|
+
export async function generateEmail(): Promise<InternalEmailInfo> {
|
|
176
|
+
/*
|
|
177
|
+
* generate-email 常返回 429;每次重试前重新拉首页以换新 gm_sid / __BROWSER_AUTH,并做指数退避。
|
|
178
|
+
*/
|
|
179
|
+
const maxAttempts = 6;
|
|
180
|
+
let lastStatus = 0;
|
|
181
|
+
|
|
182
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
183
|
+
const { gmSid, browserToken } = await fetchHomeSessionWithRetry();
|
|
184
|
+
|
|
185
|
+
const response = await fetchWithTimeout(`${BASE_URL}/generate-email`, {
|
|
186
|
+
method: 'GET',
|
|
187
|
+
headers: {
|
|
188
|
+
...DEFAULT_HEADERS,
|
|
189
|
+
Cookie: `gm_sid=${gmSid}`,
|
|
190
|
+
'X-Inbox-Token': browserToken,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (response.status === 429) {
|
|
195
|
+
lastStatus = 429;
|
|
196
|
+
if (attempt < maxAttempts - 1) {
|
|
197
|
+
const wait = Math.min(3000 * 2 ** attempt, 60_000);
|
|
198
|
+
await sleepMs(wait);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
throw new Error('Failed to generate email: 429');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!response.ok) {
|
|
205
|
+
lastStatus = response.status;
|
|
206
|
+
throw new Error(`Failed to generate email: ${response.status}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const data = await response.json();
|
|
210
|
+
|
|
211
|
+
if (!data.success) {
|
|
212
|
+
throw new Error('Failed to generate email');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const email = data.data.email as string;
|
|
216
|
+
const tokenFromAuth = data.auth?.token as string | undefined;
|
|
217
|
+
const inboxJwt = tokenFromAuth || (await fetchInboxTokenWithRetry(email, gmSid));
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
channel: CHANNEL,
|
|
221
|
+
email,
|
|
222
|
+
token: JSON.stringify({ gmSid, inbox: inboxJwt }),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
throw new Error(`Failed to generate email: ${lastStatus || 'unknown'}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function getEmails(token: string, email: string): Promise<Email[]> {
|
|
230
|
+
if (!token) {
|
|
231
|
+
throw new Error('internal error: token missing for chatgpt-org-uk');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let { gmSid, inbox } = parseChatgptPackedToken(token);
|
|
235
|
+
if (!gmSid) {
|
|
236
|
+
gmSid = (await fetchHomeSessionWithRetry()).gmSid;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
return await fetchEmails(inbox, email, gmSid);
|
|
241
|
+
} catch (error: any) {
|
|
242
|
+
const message = String(error?.message || error || '').toLowerCase();
|
|
243
|
+
if (message.includes('401')) {
|
|
244
|
+
const sess = await fetchHomeSessionWithRetry();
|
|
245
|
+
const newInbox = await fetchInboxTokenWithRetry(email, sess.gmSid);
|
|
246
|
+
return await fetchEmails(newInbox, email, sess.gmSid);
|
|
247
|
+
}
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -1,8 +1,168 @@
|
|
|
1
1
|
import { InternalEmailInfo, Email, Channel } from '../types';
|
|
2
2
|
import { normalizeEmail } from '../normalize';
|
|
3
|
+
import { fetchWithTimeout } from '../retry';
|
|
4
|
+
import { getConfig } from '../config';
|
|
3
5
|
|
|
4
6
|
const CHANNEL: Channel = 'dropmail';
|
|
5
|
-
|
|
7
|
+
|
|
8
|
+
const TOKEN_GENERATE_URL = 'https://dropmail.me/api/token/generate';
|
|
9
|
+
const TOKEN_RENEW_URL = 'https://dropmail.me/api/token/renew';
|
|
10
|
+
/** 申请 1h 令牌,缓存略短于 1h,避免边界过期 */
|
|
11
|
+
const AUTO_TOKEN_CACHE_MS = 50 * 60 * 1000;
|
|
12
|
+
/** 距缓存过期前多久触发 renew(毫秒) */
|
|
13
|
+
const RENEW_BEFORE_EXPIRY_MS = 10 * 60 * 1000;
|
|
14
|
+
|
|
15
|
+
function cacheMsForLifetime(lifetime: string): number {
|
|
16
|
+
const s = lifetime.trim().toLowerCase();
|
|
17
|
+
if (s === '1h') return 50 * 60 * 1000;
|
|
18
|
+
if (s === '1d') return 23 * 60 * 60 * 1000;
|
|
19
|
+
if (s === '7d' || s === '30d' || s === '90d') {
|
|
20
|
+
const days = parseInt(s, 10);
|
|
21
|
+
return Math.max(0, days - 1) * 24 * 60 * 60 * 1000;
|
|
22
|
+
}
|
|
23
|
+
return AUTO_TOKEN_CACHE_MS;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function dropmailRenewLifetime(): string {
|
|
27
|
+
const c = getConfig().dropmailRenewLifetime?.trim();
|
|
28
|
+
if (c) return c;
|
|
29
|
+
const e =
|
|
30
|
+
typeof process !== 'undefined' && process.env?.DROPMAIL_RENEW_LIFETIME?.trim();
|
|
31
|
+
return e || '1d';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const TOKEN_HEADERS: Record<string, string> = {
|
|
35
|
+
Accept: 'application/json',
|
|
36
|
+
'Content-Type': 'application/json',
|
|
37
|
+
Origin: 'https://dropmail.me',
|
|
38
|
+
Referer: 'https://dropmail.me/api/',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let cachedAfToken: { value: string; expiresAt: number } | null = null;
|
|
42
|
+
let tokenFetchInFlight: Promise<string> | null = null;
|
|
43
|
+
|
|
44
|
+
function explicitDropmailAuthToken(): string | undefined {
|
|
45
|
+
const fromConfig = getConfig().dropmailAuthToken?.trim();
|
|
46
|
+
const fromEnv =
|
|
47
|
+
typeof process !== 'undefined' && process.env
|
|
48
|
+
? process.env.DROPMAIL_AUTH_TOKEN?.trim() || process.env.DROPMAIL_API_TOKEN?.trim()
|
|
49
|
+
: undefined;
|
|
50
|
+
return fromConfig || fromEnv;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function dropmailAutoTokenDisabled(): boolean {
|
|
54
|
+
if (getConfig().dropmailDisableAutoToken) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
const v = typeof process !== 'undefined' && process.env?.DROPMAIL_NO_AUTO_TOKEN?.trim().toLowerCase();
|
|
58
|
+
return v === '1' || v === 'true' || v === 'yes';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function fetchAfTokenFromApi(): Promise<string> {
|
|
62
|
+
const response = await fetchWithTimeout(TOKEN_GENERATE_URL, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: TOKEN_HEADERS,
|
|
65
|
+
body: JSON.stringify({ type: 'af', lifetime: '1h' }),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
throw new Error(`DropMail token/generate HTTP ${response.status}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const body = (await response.json()) as { token?: string; error?: string };
|
|
73
|
+
const token = typeof body.token === 'string' ? body.token.trim() : '';
|
|
74
|
+
if (!token || !token.startsWith('af_')) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
body.error || 'DropMail token/generate 未返回有效 af_ 令牌',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return token;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function renewAfTokenFromApi(currentToken: string, lifetime: string): Promise<string> {
|
|
83
|
+
const response = await fetchWithTimeout(TOKEN_RENEW_URL, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: TOKEN_HEADERS,
|
|
86
|
+
body: JSON.stringify({ token: currentToken, lifetime }),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw new Error(`DropMail token/renew HTTP ${response.status}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const body = (await response.json()) as { token?: string; error?: string };
|
|
94
|
+
const token = typeof body.token === 'string' ? body.token.trim() : '';
|
|
95
|
+
if (!token || !token.startsWith('af_')) {
|
|
96
|
+
throw new Error(body.error || 'DropMail token/renew 未返回有效 af_ 令牌');
|
|
97
|
+
}
|
|
98
|
+
return token;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 解析 GraphQL 用的 af_ 令牌:优先配置/环境变量,否则 generate + 缓存,将过期时 renew。
|
|
103
|
+
*/
|
|
104
|
+
async function resolveDropmailAuthToken(): Promise<string> {
|
|
105
|
+
const explicit = explicitDropmailAuthToken();
|
|
106
|
+
if (explicit) {
|
|
107
|
+
return explicit;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (dropmailAutoTokenDisabled()) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
'DropMail 已禁用自动令牌:请设置 DROPMAIL_AUTH_TOKEN,或 setConfig({ dropmailAuthToken: "af_..." }),见 https://dropmail.me/api/',
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
if (
|
|
118
|
+
cachedAfToken &&
|
|
119
|
+
now < cachedAfToken.expiresAt - RENEW_BEFORE_EXPIRY_MS
|
|
120
|
+
) {
|
|
121
|
+
return cachedAfToken.value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (tokenFetchInFlight) {
|
|
125
|
+
return tokenFetchInFlight;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const renewLifetime = dropmailRenewLifetime();
|
|
129
|
+
|
|
130
|
+
tokenFetchInFlight = (async () => {
|
|
131
|
+
try {
|
|
132
|
+
if (cachedAfToken?.value) {
|
|
133
|
+
try {
|
|
134
|
+
const renewed = await renewAfTokenFromApi(
|
|
135
|
+
cachedAfToken.value,
|
|
136
|
+
renewLifetime,
|
|
137
|
+
);
|
|
138
|
+
cachedAfToken = {
|
|
139
|
+
value: renewed,
|
|
140
|
+
expiresAt: Date.now() + cacheMsForLifetime(renewLifetime),
|
|
141
|
+
};
|
|
142
|
+
return renewed;
|
|
143
|
+
} catch {
|
|
144
|
+
/* 续期失败则重新申请 */
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const token = await fetchAfTokenFromApi();
|
|
149
|
+
cachedAfToken = {
|
|
150
|
+
value: token,
|
|
151
|
+
expiresAt: Date.now() + AUTO_TOKEN_CACHE_MS,
|
|
152
|
+
};
|
|
153
|
+
return token;
|
|
154
|
+
} finally {
|
|
155
|
+
tokenFetchInFlight = null;
|
|
156
|
+
}
|
|
157
|
+
})();
|
|
158
|
+
|
|
159
|
+
return tokenFetchInFlight;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function dropmailGraphqlUrl(): Promise<string> {
|
|
163
|
+
const token = await resolveDropmailAuthToken();
|
|
164
|
+
return `https://dropmail.me/api/graphql/${encodeURIComponent(token)}`;
|
|
165
|
+
}
|
|
6
166
|
|
|
7
167
|
const DEFAULT_HEADERS: Record<string, string> = {
|
|
8
168
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -29,7 +189,7 @@ async function graphqlRequest(query: string, variables?: Record<string, any>): P
|
|
|
29
189
|
params.set('variables', JSON.stringify(variables));
|
|
30
190
|
}
|
|
31
191
|
|
|
32
|
-
const response = await
|
|
192
|
+
const response = await fetchWithTimeout(await dropmailGraphqlUrl(), {
|
|
33
193
|
method: 'POST',
|
|
34
194
|
headers: DEFAULT_HEADERS,
|
|
35
195
|
body: params.toString(),
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { InternalEmailInfo, Email, Channel } from '../types';
|
|
12
12
|
import { normalizeEmail } from '../normalize';
|
|
13
|
+
import { fetchWithTimeout } from '../retry';
|
|
13
14
|
|
|
14
15
|
const CHANNEL: Channel = 'guerrillamail';
|
|
15
16
|
const BASE_URL = 'https://api.guerrillamail.com/ajax.php';
|
|
@@ -20,7 +21,7 @@ const BASE_URL = 'https://api.guerrillamail.com/ajax.php';
|
|
|
20
21
|
* 返回 email_addr + sid_token(用于后续获取邮件)
|
|
21
22
|
*/
|
|
22
23
|
export async function generateEmail(): Promise<InternalEmailInfo> {
|
|
23
|
-
const response = await
|
|
24
|
+
const response = await fetchWithTimeout(`${BASE_URL}?f=get_email_address&lang=en`, {
|
|
24
25
|
method: 'GET',
|
|
25
26
|
headers: {
|
|
26
27
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
@@ -51,7 +52,7 @@ export async function generateEmail(): Promise<InternalEmailInfo> {
|
|
|
51
52
|
* 返回 list 数组,每个元素包含 mail_id, mail_from, mail_subject, mail_body 等
|
|
52
53
|
*/
|
|
53
54
|
export async function getEmails(token: string, email: string): Promise<Email[]> {
|
|
54
|
-
const response = await
|
|
55
|
+
const response = await fetchWithTimeout(`${BASE_URL}?f=check_email&seq=0&sid_token=${encodeURIComponent(token)}`, {
|
|
55
56
|
method: 'GET',
|
|
56
57
|
headers: {
|
|
57
58
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { InternalEmailInfo, Email, Channel } from '../types';
|
|
2
2
|
import { normalizeEmail } from '../normalize';
|
|
3
|
+
import { fetchWithTimeout } from '../retry';
|
|
4
|
+
import { randomSyntheticLinshiKey } from './linshi-token';
|
|
3
5
|
|
|
4
6
|
const CHANNEL: Channel = 'linshi-email';
|
|
5
7
|
const BASE_URL = 'https://www.linshi-email.com/api/v1';
|
|
6
|
-
const API_KEY = '552562b8524879814776e52bc8de5c9f';
|
|
7
8
|
|
|
8
9
|
const DEFAULT_HEADERS = {
|
|
9
10
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
|
|
@@ -17,7 +18,8 @@ const DEFAULT_HEADERS = {
|
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
export async function generateEmail(): Promise<InternalEmailInfo> {
|
|
20
|
-
const
|
|
21
|
+
const { apiPathKey } = randomSyntheticLinshiKey();
|
|
22
|
+
const response = await fetchWithTimeout(`${BASE_URL}/email/${apiPathKey}`, {
|
|
21
23
|
method: 'POST',
|
|
22
24
|
headers: DEFAULT_HEADERS,
|
|
23
25
|
body: JSON.stringify({}),
|
|
@@ -28,22 +30,37 @@ export async function generateEmail(): Promise<InternalEmailInfo> {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
const data = await response.json();
|
|
31
|
-
|
|
33
|
+
|
|
32
34
|
if (data.status !== 'ok') {
|
|
33
35
|
throw new Error('Failed to generate email');
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
const d = data.data;
|
|
39
|
+
const raw =
|
|
40
|
+
(typeof d?.email === 'string' && d.email) ||
|
|
41
|
+
(typeof d?.mail === 'string' && d.mail) ||
|
|
42
|
+
(typeof d?.address === 'string' && d.address) ||
|
|
43
|
+
'';
|
|
44
|
+
const email = String(raw).trim();
|
|
45
|
+
|
|
46
|
+
if (!email || !email.includes('@')) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
'linshi-email: API 未返回有效邮箱地址(data 为空或缺 email 字段,常见于频率限制:每小时约 10 个 / 每天约 20 个)',
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
return {
|
|
37
53
|
channel: CHANNEL,
|
|
38
|
-
email
|
|
39
|
-
expiresAt:
|
|
54
|
+
email,
|
|
55
|
+
expiresAt: d?.expired,
|
|
56
|
+
token: apiPathKey,
|
|
40
57
|
};
|
|
41
58
|
}
|
|
42
59
|
|
|
43
|
-
export async function getEmails(email: string): Promise<Email[]> {
|
|
60
|
+
export async function getEmails(email: string, apiPathKey: string): Promise<Email[]> {
|
|
44
61
|
const encodedEmail = encodeURIComponent(email);
|
|
45
62
|
const timestamp = Date.now();
|
|
46
|
-
const response = await
|
|
63
|
+
const response = await fetchWithTimeout(`${BASE_URL}/refreshmessage/${apiPathKey}/${encodedEmail}?t=${timestamp}`, {
|
|
47
64
|
method: 'GET',
|
|
48
65
|
headers: DEFAULT_HEADERS,
|
|
49
66
|
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomInt } from 'crypto';
|
|
2
|
+
export function deriveLinshiApiPathKey(visitorId: string): string {
|
|
3
|
+
if (visitorId.length < 4) {
|
|
4
|
+
throw new Error('visitorId 过短,无法套用 u()');
|
|
5
|
+
}
|
|
6
|
+
let e = visitorId.substring(0, visitorId.length - 2);
|
|
7
|
+
let t = 0;
|
|
8
|
+
for (let i = 0; i < e.length; i++) {
|
|
9
|
+
t += e.charCodeAt(i) % 5;
|
|
10
|
+
}
|
|
11
|
+
if (t < 10) {
|
|
12
|
+
t = 10;
|
|
13
|
+
}
|
|
14
|
+
if (t >= 100) {
|
|
15
|
+
t = 99;
|
|
16
|
+
}
|
|
17
|
+
const ts = t.toString();
|
|
18
|
+
e = e.slice(0, 3) + ts[0] + e.slice(3);
|
|
19
|
+
e = e.slice(0, 12) + ts[1] + e.slice(12);
|
|
20
|
+
return e;
|
|
21
|
+
}
|
|
22
|
+
export interface SyntheticBrowserProfile {
|
|
23
|
+
userAgent: string;
|
|
24
|
+
platform: string;
|
|
25
|
+
language: string;
|
|
26
|
+
languages: string;
|
|
27
|
+
hardwareConcurrency: number;
|
|
28
|
+
deviceMemory: number;
|
|
29
|
+
timezone: string;
|
|
30
|
+
colorDepth: number;
|
|
31
|
+
pixelRatio: number;
|
|
32
|
+
screen: { width: number; height: number; availWidth: number; availHeight: number };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const UA_POOL = [
|
|
36
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
37
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0',
|
|
38
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
39
|
+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
|
|
40
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function pick<T>(arr: T[]): T {
|
|
44
|
+
return arr[randomInt(arr.length)];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function randomBrowserLikeProfile(): SyntheticBrowserProfile {
|
|
48
|
+
const w = pick([1920, 2560, 1680, 1536, 1366]) + randomInt(0, 80);
|
|
49
|
+
const h = pick([1080, 1440, 1050, 864, 768]) + randomInt(0, 40);
|
|
50
|
+
return {
|
|
51
|
+
userAgent: pick(UA_POOL),
|
|
52
|
+
platform: pick(['Win32', 'MacIntel', 'Linux x86_64']),
|
|
53
|
+
language: pick(['zh-CN', 'en-US', 'en-GB', 'ja-JP']),
|
|
54
|
+
languages: pick(['zh-CN,zh,en', 'en-US,en', 'ja-JP,ja,en']),
|
|
55
|
+
hardwareConcurrency: randomInt(4, 33),
|
|
56
|
+
deviceMemory: pick([4, 8, 16]),
|
|
57
|
+
timezone: pick(['Asia/Shanghai', 'America/New_York', 'Europe/London', 'UTC']),
|
|
58
|
+
colorDepth: pick([24, 30]),
|
|
59
|
+
pixelRatio: pick([1, 1.25, 1.5, 2]),
|
|
60
|
+
screen: {
|
|
61
|
+
width: w,
|
|
62
|
+
height: h,
|
|
63
|
+
availWidth: w,
|
|
64
|
+
availHeight: h - randomInt(24, 120),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function syntheticVisitorIdFromProfile(
|
|
70
|
+
profile: SyntheticBrowserProfile,
|
|
71
|
+
salt: Buffer = randomBytes(16),
|
|
72
|
+
): string {
|
|
73
|
+
const payload = JSON.stringify(profile, Object.keys(profile).sort());
|
|
74
|
+
return createHash('sha256').update(payload).update(salt).digest('hex').slice(0, 32);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function randomSyntheticLinshiKey(): {
|
|
78
|
+
profile: SyntheticBrowserProfile;
|
|
79
|
+
visitorId: string;
|
|
80
|
+
apiPathKey: string;
|
|
81
|
+
} {
|
|
82
|
+
const profile = randomBrowserLikeProfile();
|
|
83
|
+
const visitorId = syntheticVisitorIdFromProfile(profile);
|
|
84
|
+
const apiPathKey = deriveLinshiApiPathKey(visitorId);
|
|
85
|
+
return { profile, visitorId, apiPathKey };
|
|
86
|
+
}
|