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.
Files changed (47) hide show
  1. package/README.md +43 -38
  2. package/demo/poll-emails.ts +290 -28
  3. package/dist/config.d.ts +16 -0
  4. package/dist/config.js +4 -1
  5. package/dist/index.d.ts +4 -1
  6. package/dist/index.js +33 -16
  7. package/dist/providers/awamail.js +4 -3
  8. package/dist/providers/chatgpt-org-uk.d.ts +1 -1
  9. package/dist/providers/chatgpt-org-uk.js +188 -20
  10. package/dist/providers/dropmail.js +135 -3
  11. package/dist/providers/guerrillamail.js +4 -3
  12. package/dist/providers/linshi-email.d.ts +1 -1
  13. package/dist/providers/linshi-email.js +19 -7
  14. package/dist/providers/linshi-token.d.ts +25 -0
  15. package/dist/providers/linshi-token.js +69 -0
  16. package/dist/providers/mail-tm.js +43 -25
  17. package/dist/providers/maildrop.js +3 -2
  18. package/dist/providers/smail-pw.d.ts +9 -0
  19. package/dist/providers/smail-pw.js +356 -0
  20. package/dist/providers/temp-mail-io.js +5 -4
  21. package/dist/providers/tempmail-lol.js +4 -3
  22. package/dist/providers/tempmail.js +4 -3
  23. package/dist/retry.d.ts +2 -10
  24. package/dist/retry.js +41 -10
  25. package/dist/types.d.ts +6 -1
  26. package/dist/types.js +1 -1
  27. package/package.json +1 -1
  28. package/src/config.ts +16 -0
  29. package/src/index.ts +31 -14
  30. package/src/providers/awamail.ts +3 -2
  31. package/src/providers/chatgpt-org-uk.ts +213 -22
  32. package/src/providers/dropmail.ts +162 -2
  33. package/src/providers/guerrillamail.ts +3 -2
  34. package/src/providers/linshi-email.ts +24 -7
  35. package/src/providers/linshi-token.ts +86 -0
  36. package/src/providers/mail-tm.ts +43 -24
  37. package/src/providers/maildrop.ts +2 -1
  38. package/src/providers/smail-pw.ts +382 -0
  39. package/src/providers/temp-mail-io.ts +4 -3
  40. package/src/providers/tempmail-lol.ts +3 -2
  41. package/src/providers/tempmail.ts +3 -2
  42. package/src/retry.ts +42 -9
  43. package/src/types.ts +6 -1
  44. package/test/example.ts +183 -4
  45. package/dist/providers/tempmail-la.d.ts +0 -15
  46. package/dist/providers/tempmail-la.js +0 -89
  47. 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': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
9
- 'Content-Type': 'application/json',
10
- 'Accept': '*/*',
11
- 'Referer': 'https://mail.chatgpt.org.uk/',
12
- 'sec-ch-ua': '"Microsoft Edge";v="143", "Chromium";v="143", "Not A(Brand";v="24"',
13
- 'sec-ch-ua-mobile': '?0',
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
- export async function generateEmail(): Promise<InternalEmailInfo> {
19
- const response = await fetch(`${BASE_URL}/generate-email`, {
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 generate email: ${response.status}`);
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 (!data.success) {
31
- throw new Error('Failed to generate email');
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
- channel: CHANNEL,
36
- email: data.data.email,
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
- export async function getEmails(email: string): Promise<Email[]> {
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 fetch(`${BASE_URL}/emails?email=${encodedEmail}`, {
152
+ const response = await fetchWithTimeout(`${BASE_URL}/emails?email=${encodedEmail}`, {
43
153
  method: 'GET',
44
- headers: DEFAULT_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
- const BASE_URL = 'https://dropmail.me/api/graphql/MY_TOKEN';
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 fetch(BASE_URL, {
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 fetch(`${BASE_URL}?f=get_email_address&lang=en`, {
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 fetch(`${BASE_URL}?f=check_email&seq=0&sid_token=${encodeURIComponent(token)}`, {
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 response = await fetch(`${BASE_URL}/email/${API_KEY}`, {
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: data.data.email,
39
- expiresAt: data.data.expired,
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 fetch(`${BASE_URL}/refreshmessage/${API_KEY}/${encodedEmail}?t=${timestamp}`, {
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
+ }