tempmail-sdk 1.0.2 → 1.1.0
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 +7 -3
- package/dist/index.d.ts +94 -1
- package/dist/index.js +169 -37
- package/dist/logger.d.ts +73 -0
- package/dist/logger.js +100 -0
- package/dist/normalize.d.ts +7 -0
- package/dist/normalize.js +47 -2
- package/dist/providers/guerrillamail.d.ts +22 -0
- package/dist/providers/guerrillamail.js +71 -0
- package/dist/providers/mail-tm.js +8 -3
- package/dist/providers/maildrop.d.ts +21 -0
- package/dist/providers/maildrop.js +180 -0
- package/dist/providers/temp-mail-io.js +54 -24
- package/dist/retry.d.ts +38 -0
- package/dist/retry.js +121 -0
- package/dist/types.d.ts +88 -1
- package/dist/types.js +1 -1
- package/package.json +1 -1
- package/src/index.ts +165 -42
- package/src/logger.ts +106 -0
- package/src/normalize.ts +46 -1
- package/src/providers/guerrillamail.ts +78 -0
- package/src/providers/mail-tm.ts +7 -2
- package/src/providers/maildrop.ts +210 -0
- package/src/providers/temp-mail-io.ts +55 -23
- package/src/retry.ts +146 -0
- package/src/types.ts +89 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maildrop 渠道实现
|
|
3
|
+
* API: GraphQL endpoint https://api.maildrop.cc/graphql
|
|
4
|
+
*
|
|
5
|
+
* 特点:
|
|
6
|
+
* - 无需认证,公开 GraphQL API
|
|
7
|
+
* - 自带反垃圾过滤
|
|
8
|
+
* - 邮箱名即用户名(任意字符串@maildrop.cc)
|
|
9
|
+
* - 无过期时间限制
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { EmailInfo, Email, Channel } from '../types';
|
|
13
|
+
|
|
14
|
+
const CHANNEL: Channel = 'maildrop';
|
|
15
|
+
const GRAPHQL_URL = 'https://api.maildrop.cc/graphql';
|
|
16
|
+
const DOMAIN = 'maildrop.cc';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 解码 RFC 2047 编码的邮件头(如发件人、主题)
|
|
20
|
+
* 支持 Base64 (B) 和 Quoted-Printable (Q) 编码
|
|
21
|
+
*/
|
|
22
|
+
function decodeRfc2047(str: string): string {
|
|
23
|
+
if (!str) return '';
|
|
24
|
+
return str.replace(/=\?([^?]+)\?(B|Q)\?([^?]*)\?=/gi, (_, charset, encoding, encoded) => {
|
|
25
|
+
try {
|
|
26
|
+
if (encoding.toUpperCase() === 'B') {
|
|
27
|
+
return Buffer.from(encoded, 'base64').toString('utf-8');
|
|
28
|
+
}
|
|
29
|
+
/* Quoted-Printable: _=空格,=XX=十六进制字节 */
|
|
30
|
+
const decoded = encoded
|
|
31
|
+
.replace(/_/g, ' ')
|
|
32
|
+
.replace(/=([0-9A-Fa-f]{2})/g, (_m: string, hex: string) => String.fromCharCode(parseInt(hex, 16)));
|
|
33
|
+
return decoded;
|
|
34
|
+
} catch {
|
|
35
|
+
return encoded;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 从原始 MIME 邮件源码中提取纯文本正文
|
|
42
|
+
* maildrop 的 data 字段返回完整 MIME 源码,需要解析出 text/plain 部分
|
|
43
|
+
*/
|
|
44
|
+
function extractTextFromMime(raw: string): string {
|
|
45
|
+
if (!raw) return '';
|
|
46
|
+
|
|
47
|
+
/* 分离邮件头和正文(双换行分隔) */
|
|
48
|
+
const headerBodySplit = raw.indexOf('\r\n\r\n');
|
|
49
|
+
if (headerBodySplit === -1) return raw;
|
|
50
|
+
|
|
51
|
+
const headers = raw.substring(0, headerBodySplit);
|
|
52
|
+
const body = raw.substring(headerBodySplit + 4);
|
|
53
|
+
|
|
54
|
+
/* 检查是否为 multipart 邮件 */
|
|
55
|
+
const boundaryMatch = headers.match(/boundary="?([^";\r\n]+)"?/i);
|
|
56
|
+
if (boundaryMatch) {
|
|
57
|
+
const boundary = boundaryMatch[1];
|
|
58
|
+
const parts = body.split('--' + boundary);
|
|
59
|
+
|
|
60
|
+
for (const part of parts) {
|
|
61
|
+
/* 查找 text/plain 部分 */
|
|
62
|
+
if (part.match(/Content-Type:\s*text\/plain/i)) {
|
|
63
|
+
const partHeaderEnd = part.indexOf('\r\n\r\n');
|
|
64
|
+
if (partHeaderEnd === -1) continue;
|
|
65
|
+
|
|
66
|
+
const partHeaders = part.substring(0, partHeaderEnd);
|
|
67
|
+
let content = part.substring(partHeaderEnd + 4).replace(/\r\n$/, '').replace(/--$/, '').trim();
|
|
68
|
+
|
|
69
|
+
/* 处理 Content-Transfer-Encoding */
|
|
70
|
+
if (partHeaders.match(/Content-Transfer-Encoding:\s*base64/i)) {
|
|
71
|
+
try {
|
|
72
|
+
content = Buffer.from(content.replace(/\s/g, ''), 'base64').toString('utf-8');
|
|
73
|
+
} catch { /* 解码失败则保留原文 */ }
|
|
74
|
+
} else if (partHeaders.match(/Content-Transfer-Encoding:\s*quoted-printable/i)) {
|
|
75
|
+
content = content
|
|
76
|
+
.replace(/=\r?\n/g, '')
|
|
77
|
+
.replace(/=([0-9A-Fa-f]{2})/g, (_, hex: string) => String.fromCharCode(parseInt(hex, 16)));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return content.trim();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* 非 multipart:检查整体编码 */
|
|
86
|
+
if (headers.match(/Content-Transfer-Encoding:\s*base64/i)) {
|
|
87
|
+
try {
|
|
88
|
+
return Buffer.from(body.replace(/\s/g, ''), 'base64').toString('utf-8').trim();
|
|
89
|
+
} catch { /* 解码失败 */ }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return body.trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 生成随机用户名
|
|
97
|
+
*/
|
|
98
|
+
function randomUsername(length: number = 10): string {
|
|
99
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
100
|
+
let result = '';
|
|
101
|
+
for (let i = 0; i < length; i++) {
|
|
102
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 发送 GraphQL 请求
|
|
109
|
+
* 使用 operationName + variables 的标准 GraphQL 格式
|
|
110
|
+
*/
|
|
111
|
+
async function graphqlRequest(
|
|
112
|
+
operationName: string,
|
|
113
|
+
query: string,
|
|
114
|
+
variables: Record<string, string> = {},
|
|
115
|
+
): Promise<any> {
|
|
116
|
+
const response = await fetch(GRAPHQL_URL, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: {
|
|
119
|
+
'Content-Type': 'application/json',
|
|
120
|
+
'Origin': 'https://maildrop.cc',
|
|
121
|
+
'Referer': 'https://maildrop.cc/',
|
|
122
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
123
|
+
},
|
|
124
|
+
body: JSON.stringify({ operationName, variables, query }),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
throw new Error(`Maildrop GraphQL request failed: ${response.status}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const data = await response.json();
|
|
132
|
+
if (data.errors && data.errors.length > 0) {
|
|
133
|
+
throw new Error(`Maildrop GraphQL error: ${data.errors[0].message}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return data.data;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 创建临时邮箱
|
|
141
|
+
* Maildrop 无需注册,任意用户名即可接收邮件
|
|
142
|
+
*/
|
|
143
|
+
export async function generateEmail(): Promise<EmailInfo> {
|
|
144
|
+
const username = randomUsername();
|
|
145
|
+
const email = `${username}@${DOMAIN}`;
|
|
146
|
+
|
|
147
|
+
/* 验证邮箱可用:查询一次 inbox 确认 API 正常 */
|
|
148
|
+
await graphqlRequest(
|
|
149
|
+
'GetInbox',
|
|
150
|
+
'query GetInbox($mailbox: String!) { inbox(mailbox: $mailbox) { id } }',
|
|
151
|
+
{ mailbox: username },
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
channel: CHANNEL,
|
|
156
|
+
email,
|
|
157
|
+
token: username,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 获取邮件列表
|
|
163
|
+
* 先查 inbox 获取邮件 ID 列表,再逐封获取完整内容
|
|
164
|
+
*/
|
|
165
|
+
export async function getEmails(token: string, email: string): Promise<Email[]> {
|
|
166
|
+
const mailbox = token || email.split('@')[0];
|
|
167
|
+
|
|
168
|
+
/* 查询收件箱列表 */
|
|
169
|
+
const inboxData = await graphqlRequest(
|
|
170
|
+
'GetInbox',
|
|
171
|
+
'query GetInbox($mailbox: String!) { inbox(mailbox: $mailbox) { id headerfrom subject date } }',
|
|
172
|
+
{ mailbox },
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const inbox = inboxData?.inbox;
|
|
176
|
+
if (!Array.isArray(inbox) || inbox.length === 0) {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* 逐封获取完整邮件内容 */
|
|
181
|
+
const emails: Email[] = [];
|
|
182
|
+
for (const item of inbox) {
|
|
183
|
+
try {
|
|
184
|
+
const msgData = await graphqlRequest(
|
|
185
|
+
'GetMessage',
|
|
186
|
+
'query GetMessage($mailbox: String!, $id: String!) { message(mailbox: $mailbox, id: $id) { id headerfrom subject date data html } }',
|
|
187
|
+
{ mailbox, id: item.id },
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const msg = msgData?.message;
|
|
191
|
+
if (msg) {
|
|
192
|
+
emails.push({
|
|
193
|
+
id: msg.id || item.id,
|
|
194
|
+
from: decodeRfc2047(msg.headerfrom || item.headerfrom || ''),
|
|
195
|
+
to: email,
|
|
196
|
+
subject: decodeRfc2047(msg.subject || item.subject || ''),
|
|
197
|
+
text: extractTextFromMime(msg.data || ''),
|
|
198
|
+
html: msg.html || '',
|
|
199
|
+
date: msg.date || item.date || '',
|
|
200
|
+
isRead: false,
|
|
201
|
+
attachments: [],
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
/* 单封邮件获取失败不影响整体 */
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return emails;
|
|
210
|
+
}
|
|
@@ -3,25 +3,56 @@ import { normalizeEmail } from '../normalize';
|
|
|
3
3
|
|
|
4
4
|
const CHANNEL: Channel = 'temp-mail-io';
|
|
5
5
|
const BASE_URL = 'https://api.internal.temp-mail.io/api/v3';
|
|
6
|
+
const PAGE_URL = 'https://temp-mail.io/en';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
};
|
|
8
|
+
/**
|
|
9
|
+
* 缓存从页面动态获取的 mobileTestingHeader 值(用于 X-CORS-Header)
|
|
10
|
+
*/
|
|
11
|
+
let cachedCorsHeader: string | null = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 从 temp-mail.io 页面的 __NUXT__ 运行时配置中提取 mobileTestingHeader
|
|
15
|
+
* 该值用于 API 请求的 X-CORS-Header 头,缺少此头会导致 400 错误
|
|
16
|
+
*/
|
|
17
|
+
async function fetchCorsHeader(): Promise<string> {
|
|
18
|
+
if (cachedCorsHeader) return cachedCorsHeader;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(PAGE_URL, {
|
|
22
|
+
headers: {
|
|
23
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
const html = await response.text();
|
|
27
|
+
const match = html.match(/mobileTestingHeader\s*:\s*"([^"]+)"/);
|
|
28
|
+
if (match) {
|
|
29
|
+
cachedCorsHeader = match[1];
|
|
30
|
+
return cachedCorsHeader;
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
/* 提取失败时使用默认值 */
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
cachedCorsHeader = '1';
|
|
37
|
+
return cachedCorsHeader;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 构建 API 请求头
|
|
42
|
+
* 关键头: Content-Type, Application-Name, Application-Version, X-CORS-Header
|
|
43
|
+
*/
|
|
44
|
+
async function getApiHeaders(): Promise<Record<string, string>> {
|
|
45
|
+
const corsHeader = await fetchCorsHeader();
|
|
46
|
+
return {
|
|
47
|
+
'Content-Type': 'application/json',
|
|
48
|
+
'Application-Name': 'web',
|
|
49
|
+
'Application-Version': '4.0.0',
|
|
50
|
+
'X-CORS-Header': corsHeader,
|
|
51
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0',
|
|
52
|
+
'origin': 'https://temp-mail.io',
|
|
53
|
+
'referer': 'https://temp-mail.io/',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
25
56
|
|
|
26
57
|
/**
|
|
27
58
|
* 创建临时邮箱
|
|
@@ -29,10 +60,11 @@ const DEFAULT_HEADERS: Record<string, string> = {
|
|
|
29
60
|
* 返回: { email, token }
|
|
30
61
|
*/
|
|
31
62
|
export async function generateEmail(): Promise<EmailInfo> {
|
|
63
|
+
const headers = await getApiHeaders();
|
|
32
64
|
const response = await fetch(`${BASE_URL}/email/new`, {
|
|
33
65
|
method: 'POST',
|
|
34
|
-
headers
|
|
35
|
-
body: JSON.stringify({ min_name_length:
|
|
66
|
+
headers,
|
|
67
|
+
body: JSON.stringify({ min_name_length: 10, max_name_length: 10 }),
|
|
36
68
|
});
|
|
37
69
|
|
|
38
70
|
if (!response.ok) {
|
|
@@ -58,10 +90,10 @@ export async function generateEmail(): Promise<EmailInfo> {
|
|
|
58
90
|
* 返回: [ { id, from, to, cc, subject, body_text, body_html, created_at, attachments } ]
|
|
59
91
|
*/
|
|
60
92
|
export async function getEmails(email: string): Promise<Email[]> {
|
|
61
|
-
const
|
|
62
|
-
const response = await fetch(`${BASE_URL}/email/${
|
|
93
|
+
const headers = await getApiHeaders();
|
|
94
|
+
const response = await fetch(`${BASE_URL}/email/${email}/messages`, {
|
|
63
95
|
method: 'GET',
|
|
64
|
-
headers
|
|
96
|
+
headers,
|
|
65
97
|
});
|
|
66
98
|
|
|
67
99
|
if (!response.ok) {
|
package/src/retry.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通用重试工具
|
|
3
|
+
* 提供请求重试、超时控制、指数退避等错误恢复机制
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { logger } from './logger';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 重试配置选项
|
|
10
|
+
*/
|
|
11
|
+
export interface RetryOptions {
|
|
12
|
+
/** 最大重试次数(不含首次请求),默认 2 */
|
|
13
|
+
maxRetries?: number;
|
|
14
|
+
/** 初始重试延迟(毫秒),默认 1000 */
|
|
15
|
+
initialDelay?: number;
|
|
16
|
+
/** 最大重试延迟(毫秒),默认 5000 */
|
|
17
|
+
maxDelay?: number;
|
|
18
|
+
/** 请求超时时间(毫秒),默认 15000 */
|
|
19
|
+
timeout?: number;
|
|
20
|
+
/** 是否对该错误进行重试的判断函数 */
|
|
21
|
+
shouldRetry?: (error: any) => boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DEFAULT_RETRY_OPTIONS: Required<RetryOptions> = {
|
|
25
|
+
maxRetries: 2,
|
|
26
|
+
initialDelay: 1000,
|
|
27
|
+
maxDelay: 5000,
|
|
28
|
+
timeout: 15000,
|
|
29
|
+
shouldRetry: defaultShouldRetry,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 默认重试判断
|
|
34
|
+
* 网络错误、超时、HTTP 4xx/5xx 错误均可重试
|
|
35
|
+
* 仅参数校验类错误(由 SDK 内部抛出)不重试
|
|
36
|
+
*/
|
|
37
|
+
function defaultShouldRetry(error: any): boolean {
|
|
38
|
+
if (!error) return false;
|
|
39
|
+
|
|
40
|
+
const message = String(error.message || error || '').toLowerCase();
|
|
41
|
+
|
|
42
|
+
/* 网络级别错误 → 重试 */
|
|
43
|
+
if (message.includes('fetch failed') ||
|
|
44
|
+
message.includes('network') ||
|
|
45
|
+
message.includes('econnrefused') ||
|
|
46
|
+
message.includes('econnreset') ||
|
|
47
|
+
message.includes('etimedout') ||
|
|
48
|
+
message.includes('timeout') ||
|
|
49
|
+
message.includes('socket hang up') ||
|
|
50
|
+
message.includes('dns') ||
|
|
51
|
+
message.includes('abort')) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* HTTP 4xx/5xx 错误 → 重试 */
|
|
56
|
+
const statusMatch = message.match(/:\s*(\d{3})/);
|
|
57
|
+
if (statusMatch) {
|
|
58
|
+
const status = parseInt(statusMatch[1], 10);
|
|
59
|
+
return status >= 400;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 休眠指定毫秒
|
|
67
|
+
*/
|
|
68
|
+
function sleep(ms: number): Promise<void> {
|
|
69
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 带重试的异步操作执行器
|
|
74
|
+
* - 自动重试可恢复的错误(网络错误、超时、5xx)
|
|
75
|
+
* - 指数退避避免过度请求
|
|
76
|
+
* - 不可恢复的错误(4xx 参数错误等)直接抛出不重试
|
|
77
|
+
*
|
|
78
|
+
* @param fn 要执行的异步操作
|
|
79
|
+
* @param options 重试配置
|
|
80
|
+
*/
|
|
81
|
+
export async function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T> {
|
|
82
|
+
const opts = { ...DEFAULT_RETRY_OPTIONS, ...options };
|
|
83
|
+
let lastError: any;
|
|
84
|
+
|
|
85
|
+
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
|
|
86
|
+
try {
|
|
87
|
+
const result = await fn();
|
|
88
|
+
if (attempt > 0) {
|
|
89
|
+
logger.info(`第 ${attempt + 1} 次尝试成功`);
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
} catch (error: any) {
|
|
93
|
+
lastError = error;
|
|
94
|
+
const errorMsg = error.message || String(error);
|
|
95
|
+
|
|
96
|
+
/* 最后一次尝试失败或不可重试的错误 → 直接抛出 */
|
|
97
|
+
if (attempt >= opts.maxRetries || !opts.shouldRetry(error)) {
|
|
98
|
+
if (attempt >= opts.maxRetries && opts.maxRetries > 0) {
|
|
99
|
+
logger.error(`重试 ${opts.maxRetries} 次后仍失败: ${errorMsg}`);
|
|
100
|
+
} else if (!opts.shouldRetry(error)) {
|
|
101
|
+
logger.debug(`不可重试的错误: ${errorMsg}`);
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* 指数退避等待 */
|
|
107
|
+
const delay = Math.min(opts.initialDelay * Math.pow(2, attempt), opts.maxDelay);
|
|
108
|
+
logger.warn(`请求失败 (${errorMsg}),${delay}ms 后第 ${attempt + 2} 次重试...`);
|
|
109
|
+
await sleep(delay);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
throw lastError;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 带超时控制的 fetch 包装
|
|
118
|
+
* 在原生 fetch 基础上添加超时中断能力
|
|
119
|
+
*
|
|
120
|
+
* @param url 请求 URL
|
|
121
|
+
* @param init fetch 选项
|
|
122
|
+
* @param timeoutMs 超时时间(毫秒)
|
|
123
|
+
*/
|
|
124
|
+
export async function fetchWithTimeout(
|
|
125
|
+
url: string,
|
|
126
|
+
init?: RequestInit,
|
|
127
|
+
timeoutMs: number = DEFAULT_RETRY_OPTIONS.timeout,
|
|
128
|
+
): Promise<Response> {
|
|
129
|
+
const controller = new AbortController();
|
|
130
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const response = await fetch(url, {
|
|
134
|
+
...init,
|
|
135
|
+
signal: controller.signal,
|
|
136
|
+
});
|
|
137
|
+
return response;
|
|
138
|
+
} catch (error: any) {
|
|
139
|
+
if (error.name === 'AbortError') {
|
|
140
|
+
throw new Error(`Request timeout after ${timeoutMs}ms: ${url}`);
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
} finally {
|
|
144
|
+
clearTimeout(timeoutId);
|
|
145
|
+
}
|
|
146
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,20 +1,38 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 支持的临时邮箱渠道标识
|
|
3
|
+
* 每个渠道对应一个第三方临时邮箱服务商
|
|
4
|
+
*/
|
|
5
|
+
export type Channel = 'tempmail' | 'linshi-email' | 'tempmail-lol' | 'chatgpt-org-uk' | 'tempmail-la' | 'temp-mail-io' | 'awamail' | 'mail-tm' | 'dropmail' | 'guerrillamail' | 'maildrop';
|
|
2
6
|
|
|
7
|
+
/**
|
|
8
|
+
* 创建临时邮箱后返回的邮箱信息
|
|
9
|
+
* 包含邮箱地址、认证令牌和生命周期信息
|
|
10
|
+
*/
|
|
3
11
|
export interface EmailInfo {
|
|
12
|
+
/** 创建该邮箱所使用的渠道 */
|
|
4
13
|
channel: Channel;
|
|
14
|
+
/** 临时邮箱地址 */
|
|
5
15
|
email: string;
|
|
16
|
+
/** 认证令牌,部分渠道在获取邮件时需要此令牌 */
|
|
6
17
|
token?: string;
|
|
18
|
+
/** 邮箱过期时间(ISO 8601 字符串或 Unix 时间戳) */
|
|
7
19
|
expiresAt?: string | number;
|
|
20
|
+
/** 邮箱创建时间(ISO 8601 字符串) */
|
|
8
21
|
createdAt?: string;
|
|
9
22
|
}
|
|
10
23
|
|
|
11
24
|
/**
|
|
12
25
|
* 标准化邮件附件
|
|
26
|
+
* 不同渠道的附件字段名不同,SDK 统一归一化为此结构
|
|
13
27
|
*/
|
|
14
28
|
export interface EmailAttachment {
|
|
29
|
+
/** 附件文件名 */
|
|
15
30
|
filename: string;
|
|
31
|
+
/** 附件大小(字节) */
|
|
16
32
|
size?: number;
|
|
33
|
+
/** MIME 类型,如 application/pdf */
|
|
17
34
|
contentType?: string;
|
|
35
|
+
/** 附件下载地址 */
|
|
18
36
|
url?: string;
|
|
19
37
|
}
|
|
20
38
|
|
|
@@ -42,21 +60,91 @@ export interface Email {
|
|
|
42
60
|
attachments: EmailAttachment[];
|
|
43
61
|
}
|
|
44
62
|
|
|
63
|
+
/**
|
|
64
|
+
* 获取邮件列表的返回结果
|
|
65
|
+
* success 为 false 时表示请求失败(重试耗尽),emails 为空数组
|
|
66
|
+
*/
|
|
45
67
|
export interface GetEmailsResult {
|
|
68
|
+
/** 所使用的渠道 */
|
|
46
69
|
channel: Channel;
|
|
70
|
+
/** 查询的邮箱地址 */
|
|
47
71
|
email: string;
|
|
72
|
+
/** 邮件列表,失败时为空数组 */
|
|
48
73
|
emails: Email[];
|
|
74
|
+
/** 请求是否成功,false 表示重试耗尽后仍失败 */
|
|
49
75
|
success: boolean;
|
|
50
76
|
}
|
|
51
77
|
|
|
78
|
+
/**
|
|
79
|
+
* 重试配置
|
|
80
|
+
* SDK 内部对网络错误、超时、5xx 服务端错误自动重试
|
|
81
|
+
* 4xx 客户端错误(如参数错误)不会重试
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* // 自定义重试策略:最多重试 3 次,首次延迟 2 秒
|
|
86
|
+
* const email = await generateEmail({
|
|
87
|
+
* channel: 'mail-tm',
|
|
88
|
+
* retry: { maxRetries: 3, initialDelay: 2000 },
|
|
89
|
+
* });
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export interface RetryConfig {
|
|
93
|
+
/** 最大重试次数(不含首次请求),默认 2 */
|
|
94
|
+
maxRetries?: number;
|
|
95
|
+
/** 初始重试延迟(毫秒),采用指数退避策略,默认 1000 */
|
|
96
|
+
initialDelay?: number;
|
|
97
|
+
/** 最大重试延迟上限(毫秒),默认 5000 */
|
|
98
|
+
maxDelay?: number;
|
|
99
|
+
/** 单次请求超时时间(毫秒),默认 15000 */
|
|
100
|
+
timeout?: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 创建临时邮箱的选项
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```ts
|
|
108
|
+
* // 使用指定渠道创建邮箱
|
|
109
|
+
* const email = await generateEmail({ channel: 'mail-tm' });
|
|
110
|
+
*
|
|
111
|
+
* // 随机选择渠道
|
|
112
|
+
* const email = await generateEmail();
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
52
115
|
export interface GenerateEmailOptions {
|
|
116
|
+
/** 指定渠道,不传则随机选择 */
|
|
53
117
|
channel?: Channel;
|
|
118
|
+
/** 邮箱有效时长 */
|
|
54
119
|
duration?: number;
|
|
120
|
+
/** 指定邮箱域名 */
|
|
55
121
|
domain?: string | null;
|
|
122
|
+
/** 重试配置,不传则使用默认值(最多重试 2 次) */
|
|
123
|
+
retry?: RetryConfig;
|
|
56
124
|
}
|
|
57
125
|
|
|
126
|
+
/**
|
|
127
|
+
* 获取邮件列表的选项
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* const result = await getEmails({
|
|
132
|
+
* channel: emailInfo.channel,
|
|
133
|
+
* email: emailInfo.email,
|
|
134
|
+
* token: emailInfo.token,
|
|
135
|
+
* });
|
|
136
|
+
* if (result.success && result.emails.length > 0) {
|
|
137
|
+
* console.log('收到邮件:', result.emails);
|
|
138
|
+
* }
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
58
141
|
export interface GetEmailsOptions {
|
|
142
|
+
/** 渠道标识,必填 */
|
|
59
143
|
channel: Channel;
|
|
144
|
+
/** 邮箱地址,必填 */
|
|
60
145
|
email: string;
|
|
146
|
+
/** 认证令牌 */
|
|
61
147
|
token?: string;
|
|
148
|
+
/** 重试配置,不传则使用默认值(最多重试 2 次) */
|
|
149
|
+
retry?: RetryConfig;
|
|
62
150
|
}
|