tempmail-sdk 1.0.2 → 1.0.3
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/dist/index.d.ts +94 -1
- package/dist/index.js +150 -36
- 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/mail-tm.js +8 -3
- 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 +87 -0
- package/dist/types.js +1 -1
- package/package.json +1 -1
- package/src/index.ts +148 -41
- package/src/logger.ts +106 -0
- package/src/normalize.ts +46 -1
- package/src/providers/mail-tm.ts +7 -2
- package/src/providers/temp-mail-io.ts +55 -23
- package/src/retry.ts +146 -0
- package/src/types.ts +88 -0
package/dist/types.d.ts
CHANGED
|
@@ -1,18 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 支持的临时邮箱渠道标识
|
|
3
|
+
* 每个渠道对应一个第三方临时邮箱服务商
|
|
4
|
+
*/
|
|
1
5
|
export type Channel = 'tempmail' | 'linshi-email' | 'tempmail-lol' | 'chatgpt-org-uk' | 'tempmail-la' | 'temp-mail-io' | 'awamail' | 'mail-tm' | 'dropmail';
|
|
6
|
+
/**
|
|
7
|
+
* 创建临时邮箱后返回的邮箱信息
|
|
8
|
+
* 包含邮箱地址、认证令牌和生命周期信息
|
|
9
|
+
*/
|
|
2
10
|
export interface EmailInfo {
|
|
11
|
+
/** 创建该邮箱所使用的渠道 */
|
|
3
12
|
channel: Channel;
|
|
13
|
+
/** 临时邮箱地址 */
|
|
4
14
|
email: string;
|
|
15
|
+
/** 认证令牌,部分渠道在获取邮件时需要此令牌 */
|
|
5
16
|
token?: string;
|
|
17
|
+
/** 邮箱过期时间(ISO 8601 字符串或 Unix 时间戳) */
|
|
6
18
|
expiresAt?: string | number;
|
|
19
|
+
/** 邮箱创建时间(ISO 8601 字符串) */
|
|
7
20
|
createdAt?: string;
|
|
8
21
|
}
|
|
9
22
|
/**
|
|
10
23
|
* 标准化邮件附件
|
|
24
|
+
* 不同渠道的附件字段名不同,SDK 统一归一化为此结构
|
|
11
25
|
*/
|
|
12
26
|
export interface EmailAttachment {
|
|
27
|
+
/** 附件文件名 */
|
|
13
28
|
filename: string;
|
|
29
|
+
/** 附件大小(字节) */
|
|
14
30
|
size?: number;
|
|
31
|
+
/** MIME 类型,如 application/pdf */
|
|
15
32
|
contentType?: string;
|
|
33
|
+
/** 附件下载地址 */
|
|
16
34
|
url?: string;
|
|
17
35
|
}
|
|
18
36
|
/**
|
|
@@ -38,19 +56,88 @@ export interface Email {
|
|
|
38
56
|
/** 附件列表 */
|
|
39
57
|
attachments: EmailAttachment[];
|
|
40
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* 获取邮件列表的返回结果
|
|
61
|
+
* success 为 false 时表示请求失败(重试耗尽),emails 为空数组
|
|
62
|
+
*/
|
|
41
63
|
export interface GetEmailsResult {
|
|
64
|
+
/** 所使用的渠道 */
|
|
42
65
|
channel: Channel;
|
|
66
|
+
/** 查询的邮箱地址 */
|
|
43
67
|
email: string;
|
|
68
|
+
/** 邮件列表,失败时为空数组 */
|
|
44
69
|
emails: Email[];
|
|
70
|
+
/** 请求是否成功,false 表示重试耗尽后仍失败 */
|
|
45
71
|
success: boolean;
|
|
46
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* 重试配置
|
|
75
|
+
* SDK 内部对网络错误、超时、5xx 服务端错误自动重试
|
|
76
|
+
* 4xx 客户端错误(如参数错误)不会重试
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* // 自定义重试策略:最多重试 3 次,首次延迟 2 秒
|
|
81
|
+
* const email = await generateEmail({
|
|
82
|
+
* channel: 'mail-tm',
|
|
83
|
+
* retry: { maxRetries: 3, initialDelay: 2000 },
|
|
84
|
+
* });
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export interface RetryConfig {
|
|
88
|
+
/** 最大重试次数(不含首次请求),默认 2 */
|
|
89
|
+
maxRetries?: number;
|
|
90
|
+
/** 初始重试延迟(毫秒),采用指数退避策略,默认 1000 */
|
|
91
|
+
initialDelay?: number;
|
|
92
|
+
/** 最大重试延迟上限(毫秒),默认 5000 */
|
|
93
|
+
maxDelay?: number;
|
|
94
|
+
/** 单次请求超时时间(毫秒),默认 15000 */
|
|
95
|
+
timeout?: number;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 创建临时邮箱的选项
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* // 使用指定渠道创建邮箱
|
|
103
|
+
* const email = await generateEmail({ channel: 'mail-tm' });
|
|
104
|
+
*
|
|
105
|
+
* // 随机选择渠道
|
|
106
|
+
* const email = await generateEmail();
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
47
109
|
export interface GenerateEmailOptions {
|
|
110
|
+
/** 指定渠道,不传则随机选择 */
|
|
48
111
|
channel?: Channel;
|
|
112
|
+
/** 邮箱有效时长 */
|
|
49
113
|
duration?: number;
|
|
114
|
+
/** 指定邮箱域名 */
|
|
50
115
|
domain?: string | null;
|
|
116
|
+
/** 重试配置,不传则使用默认值(最多重试 2 次) */
|
|
117
|
+
retry?: RetryConfig;
|
|
51
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* 获取邮件列表的选项
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts
|
|
124
|
+
* const result = await getEmails({
|
|
125
|
+
* channel: emailInfo.channel,
|
|
126
|
+
* email: emailInfo.email,
|
|
127
|
+
* token: emailInfo.token,
|
|
128
|
+
* });
|
|
129
|
+
* if (result.success && result.emails.length > 0) {
|
|
130
|
+
* console.log('收到邮件:', result.emails);
|
|
131
|
+
* }
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
52
134
|
export interface GetEmailsOptions {
|
|
135
|
+
/** 渠道标识,必填 */
|
|
53
136
|
channel: Channel;
|
|
137
|
+
/** 邮箱地址,必填 */
|
|
54
138
|
email: string;
|
|
139
|
+
/** 认证令牌 */
|
|
55
140
|
token?: string;
|
|
141
|
+
/** 重试配置,不传则使用默认值(最多重试 2 次) */
|
|
142
|
+
retry?: RetryConfig;
|
|
56
143
|
}
|
package/dist/types.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
3
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvdHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICog5pSv5oyB55qE5Li05pe26YKu566x5rig6YGT5qCH6K+GXG4gKiDmr4/kuKrmuKDpgZPlr7nlupTkuIDkuKrnrKzkuInmlrnkuLTml7bpgq7nrrHmnI3liqHllYZcbiAqL1xuZXhwb3J0IHR5cGUgQ2hhbm5lbCA9ICd0ZW1wbWFpbCcgfCAnbGluc2hpLWVtYWlsJyB8ICd0ZW1wbWFpbC1sb2wnIHwgJ2NoYXRncHQtb3JnLXVrJyB8ICd0ZW1wbWFpbC1sYScgfCAndGVtcC1tYWlsLWlvJyB8ICdhd2FtYWlsJyB8ICdtYWlsLXRtJyB8ICdkcm9wbWFpbCc7XG5cbi8qKlxuICog5Yib5bu65Li05pe26YKu566x5ZCO6L+U5Zue55qE6YKu566x5L+h5oGvXG4gKiDljIXlkKvpgq7nrrHlnLDlnYDjgIHorqTor4Hku6TniYzlkoznlJ/lkb3lkajmnJ/kv6Hmga9cbiAqL1xuZXhwb3J0IGludGVyZmFjZSBFbWFpbEluZm8ge1xuICAvKiog5Yib5bu66K+l6YKu566x5omA5L2/55So55qE5rig6YGTICovXG4gIGNoYW5uZWw6IENoYW5uZWw7XG4gIC8qKiDkuLTml7bpgq7nrrHlnLDlnYAgKi9cbiAgZW1haWw6IHN0cmluZztcbiAgLyoqIOiupOivgeS7pOeJjO+8jOmDqOWIhua4oOmBk+WcqOiOt+WPlumCruS7tuaXtumcgOimgeatpOS7pOeJjCAqL1xuICB0b2tlbj86IHN0cmluZztcbiAgLyoqIOmCrueusei/h+acn+aXtumXtO+8iElTTyA4NjAxIOWtl+espuS4suaIliBVbml4IOaXtumXtOaIs++8iSAqL1xuICBleHBpcmVzQXQ/OiBzdHJpbmcgfCBudW1iZXI7XG4gIC8qKiDpgq7nrrHliJvlu7rml7bpl7TvvIhJU08gODYwMSDlrZfnrKbkuLLvvIkgKi9cbiAgY3JlYXRlZEF0Pzogc3RyaW5nO1xufVxuXG4vKipcbiAqIOagh+WHhuWMlumCruS7tumZhOS7tlxuICog5LiN5ZCM5rig6YGT55qE6ZmE5Lu25a2X5q615ZCN5LiN5ZCM77yMU0RLIOe7n+S4gOW9kuS4gOWMluS4uuatpOe7k+aehFxuICovXG5leHBvcnQgaW50ZXJmYWNlIEVtYWlsQXR0YWNobWVudCB7XG4gIC8qKiDpmYTku7bmlofku7blkI0gKi9cbiAgZmlsZW5hbWU6IHN0cmluZztcbiAgLyoqIOmZhOS7tuWkp+Wwj++8iOWtl+iKgu+8iSAqL1xuICBzaXplPzogbnVtYmVyO1xuICAvKiogTUlNRSDnsbvlnovvvIzlpoIgYXBwbGljYXRpb24vcGRmICovXG4gIGNvbnRlbnRUeXBlPzogc3RyaW5nO1xuICAvKiog6ZmE5Lu25LiL6L295Zyw5Z2AICovXG4gIHVybD86IHN0cmluZztcbn1cblxuLyoqXG4gKiDmoIflh4bljJbpgq7ku7bmoLzlvI8gLSDmiYDmnInmj5DkvpvllYbov5Tlm57nu5/kuIDnu5PmnoRcbiAqL1xuZXhwb3J0IGludGVyZmFjZSBFbWFpbCB7XG4gIC8qKiDpgq7ku7bllK/kuIDmoIfor4YgKi9cbiAgaWQ6IHN0cmluZztcbiAgLyoqIOWPkeS7tuS6uumCrueuseWcsOWdgCAqL1xuICBmcm9tOiBzdHJpbmc7XG4gIC8qKiDmlLbku7bkurrpgq7nrrHlnLDlnYAgKi9cbiAgdG86IHN0cmluZztcbiAgLyoqIOmCruS7tuS4u+mimCAqL1xuICBzdWJqZWN0OiBzdHJpbmc7XG4gIC8qKiDnuq/mlofmnKzlhoXlrrkgKi9cbiAgdGV4dDogc3RyaW5nO1xuICAvKiogSFRNTCDlhoXlrrkgKi9cbiAgaHRtbDogc3RyaW5nO1xuICAvKiogSVNPIDg2MDEg5qC85byP55qE5pel5pyf5a2X56ym5LiyICovXG4gIGRhdGU6IHN0cmluZztcbiAgLyoqIOaYr+WQpuW3suivuyAqL1xuICBpc1JlYWQ6IGJvb2xlYW47XG4gIC8qKiDpmYTku7bliJfooaggKi9cbiAgYXR0YWNobWVudHM6IEVtYWlsQXR0YWNobWVudFtdO1xufVxuXG4vKipcbiAqIOiOt+WPlumCruS7tuWIl+ihqOeahOi/lOWbnue7k+aenFxuICogc3VjY2VzcyDkuLogZmFsc2Ug5pe26KGo56S66K+35rGC5aSx6LSl77yI6YeN6K+V6ICX5bC977yJ77yMZW1haWxzIOS4uuepuuaVsOe7hFxuICovXG5leHBvcnQgaW50ZXJmYWNlIEdldEVtYWlsc1Jlc3VsdCB7XG4gIC8qKiDmiYDkvb/nlKjnmoTmuKDpgZMgKi9cbiAgY2hhbm5lbDogQ2hhbm5lbDtcbiAgLyoqIOafpeivoueahOmCrueuseWcsOWdgCAqL1xuICBlbWFpbDogc3RyaW5nO1xuICAvKiog6YKu5Lu25YiX6KGo77yM5aSx6LSl5pe25Li656m65pWw57uEICovXG4gIGVtYWlsczogRW1haWxbXTtcbiAgLyoqIOivt+axguaYr+WQpuaIkOWKn++8jGZhbHNlIOihqOekuumHjeivleiAl+WwveWQjuS7jeWksei0pSAqL1xuICBzdWNjZXNzOiBib29sZWFuO1xufVxuXG4vKipcbiAqIOmHjeivlemFjee9rlxuICogU0RLIOWGhemDqOWvuee9kee7nOmUmeivr+OAgei2heaXtuOAgTV4eCDmnI3liqHnq6/plJnor6/oh6rliqjph43or5VcbiAqIDR4eCDlrqLmiLfnq6/plJnor6/vvIjlpoLlj4LmlbDplJnor6/vvInkuI3kvJrph43or5VcbiAqXG4gKiBAZXhhbXBsZVxuICogYGBgdHNcbiAqIC8vIOiHquWumuS5iemHjeivleetlueVpe+8muacgOWkmumHjeivlSAzIOasoe+8jOmmluasoeW7tui/nyAyIOenklxuICogY29uc3QgZW1haWwgPSBhd2FpdCBnZW5lcmF0ZUVtYWlsKHtcbiAqICAgY2hhbm5lbDogJ21haWwtdG0nLFxuICogICByZXRyeTogeyBtYXhSZXRyaWVzOiAzLCBpbml0aWFsRGVsYXk6IDIwMDAgfSxcbiAqIH0pO1xuICogYGBgXG4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgUmV0cnlDb25maWcge1xuICAvKiog5pyA5aSn6YeN6K+V5qyh5pWw77yI5LiN5ZCr6aaW5qyh6K+35rGC77yJ77yM6buY6K6kIDIgKi9cbiAgbWF4UmV0cmllcz86IG51bWJlcjtcbiAgLyoqIOWIneWni+mHjeivleW7tui/n++8iOavq+enku+8ie+8jOmHh+eUqOaMh+aVsOmAgOmBv+etlueVpe+8jOm7mOiupCAxMDAwICovXG4gIGluaXRpYWxEZWxheT86IG51bWJlcjtcbiAgLyoqIOacgOWkp+mHjeivleW7tui/n+S4iumZkO+8iOavq+enku+8ie+8jOm7mOiupCA1MDAwICovXG4gIG1heERlbGF5PzogbnVtYmVyO1xuICAvKiog5Y2V5qyh6K+35rGC6LaF5pe25pe26Ze077yI5q+r56eS77yJ77yM6buY6K6kIDE1MDAwICovXG4gIHRpbWVvdXQ/OiBudW1iZXI7XG59XG5cbi8qKlxuICog5Yib5bu65Li05pe26YKu566x55qE6YCJ6aG5XG4gKlxuICogQGV4YW1wbGVcbiAqIGBgYHRzXG4gKiAvLyDkvb/nlKjmjIflrprmuKDpgZPliJvlu7rpgq7nrrFcbiAqIGNvbnN0IGVtYWlsID0gYXdhaXQgZ2VuZXJhdGVFbWFpbCh7IGNoYW5uZWw6ICdtYWlsLXRtJyB9KTtcbiAqXG4gKiAvLyDpmo/mnLrpgInmi6nmuKDpgZNcbiAqIGNvbnN0IGVtYWlsID0gYXdhaXQgZ2VuZXJhdGVFbWFpbCgpO1xuICogYGBgXG4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgR2VuZXJhdGVFbWFpbE9wdGlvbnMge1xuICAvKiog5oyH5a6a5rig6YGT77yM5LiN5Lyg5YiZ6ZqP5py66YCJ5oupICovXG4gIGNoYW5uZWw/OiBDaGFubmVsO1xuICAvKiog6YKu566x5pyJ5pWI5pe26ZW/ICovXG4gIGR1cmF0aW9uPzogbnVtYmVyO1xuICAvKiog5oyH5a6a6YKu566x5Z+f5ZCNICovXG4gIGRvbWFpbj86IHN0cmluZyB8IG51bGw7XG4gIC8qKiDph43or5XphY3nva7vvIzkuI3kvKDliJnkvb/nlKjpu5jorqTlgLzvvIjmnIDlpJrph43or5UgMiDmrKHvvIkgKi9cbiAgcmV0cnk/OiBSZXRyeUNvbmZpZztcbn1cblxuLyoqXG4gKiDojrflj5bpgq7ku7bliJfooajnmoTpgInpoblcbiAqXG4gKiBAZXhhbXBsZVxuICogYGBgdHNcbiAqIGNvbnN0IHJlc3VsdCA9IGF3YWl0IGdldEVtYWlscyh7XG4gKiAgIGNoYW5uZWw6IGVtYWlsSW5mby5jaGFubmVsLFxuICogICBlbWFpbDogZW1haWxJbmZvLmVtYWlsLFxuICogICB0b2tlbjogZW1haWxJbmZvLnRva2VuLFxuICogfSk7XG4gKiBpZiAocmVzdWx0LnN1Y2Nlc3MgJiYgcmVzdWx0LmVtYWlscy5sZW5ndGggPiAwKSB7XG4gKiAgIGNvbnNvbGUubG9nKCfmlLbliLDpgq7ku7Y6JywgcmVzdWx0LmVtYWlscyk7XG4gKiB9XG4gKiBgYGBcbiAqL1xuZXhwb3J0IGludGVyZmFjZSBHZXRFbWFpbHNPcHRpb25zIHtcbiAgLyoqIOa4oOmBk+agh+ivhu+8jOW/heWhqyAqL1xuICBjaGFubmVsOiBDaGFubmVsO1xuICAvKiog6YKu566x5Zyw5Z2A77yM5b+F5aGrICovXG4gIGVtYWlsOiBzdHJpbmc7XG4gIC8qKiDorqTor4Hku6TniYwgKi9cbiAgdG9rZW4/OiBzdHJpbmc7XG4gIC8qKiDph43or5XphY3nva7vvIzkuI3kvKDliJnkvb/nlKjpu5jorqTlgLzvvIjmnIDlpJrph43or5UgMiDmrKHvvIkgKi9cbiAgcmV0cnk/OiBSZXRyeUNvbmZpZztcbn1cbiJdfQ==
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -8,10 +8,15 @@ import * as awamail from './providers/awamail';
|
|
|
8
8
|
import * as mailTm from './providers/mail-tm';
|
|
9
9
|
import * as dropmail from './providers/dropmail';
|
|
10
10
|
import { Channel, EmailInfo, Email, EmailAttachment, GetEmailsResult, GenerateEmailOptions, GetEmailsOptions } from './types';
|
|
11
|
+
import { withRetry, RetryOptions } from './retry';
|
|
12
|
+
import { logger } from './logger';
|
|
11
13
|
|
|
12
14
|
export { Channel, EmailInfo, Email, EmailAttachment, GetEmailsResult, GenerateEmailOptions, GetEmailsOptions } from './types';
|
|
13
15
|
export { normalizeEmail } from './normalize';
|
|
16
|
+
export { withRetry, fetchWithTimeout, RetryOptions } from './retry';
|
|
17
|
+
export { LogLevel, LogHandler, setLogLevel, getLogLevel, setLogger, logger } from './logger';
|
|
14
18
|
|
|
19
|
+
/** 渠道名称到 provider 实现的映射表 */
|
|
15
20
|
const providers = {
|
|
16
21
|
'tempmail': tempmail,
|
|
17
22
|
'linshi-email': linshiEmail,
|
|
@@ -24,14 +29,22 @@ const providers = {
|
|
|
24
29
|
'dropmail': dropmail,
|
|
25
30
|
};
|
|
26
31
|
|
|
32
|
+
/** 所有支持的渠道列表,用于随机选择和遍历 */
|
|
27
33
|
const allChannels: Channel[] = ['tempmail', 'linshi-email', 'tempmail-lol', 'chatgpt-org-uk', 'tempmail-la', 'temp-mail-io', 'awamail', 'mail-tm', 'dropmail'];
|
|
28
34
|
|
|
35
|
+
/**
|
|
36
|
+
* 渠道信息,包含渠道标识、显示名称和对应网站
|
|
37
|
+
*/
|
|
29
38
|
export interface ChannelInfo {
|
|
39
|
+
/** 渠道标识 */
|
|
30
40
|
channel: Channel;
|
|
41
|
+
/** 渠道显示名称 */
|
|
31
42
|
name: string;
|
|
43
|
+
/** 对应的临时邮箱服务网站 */
|
|
32
44
|
website: string;
|
|
33
45
|
}
|
|
34
46
|
|
|
47
|
+
/** 渠道信息映射表 */
|
|
35
48
|
const channelInfoMap: Record<Channel, ChannelInfo> = {
|
|
36
49
|
'tempmail': { channel: 'tempmail', name: 'TempMail', website: 'tempmail.ing' },
|
|
37
50
|
'linshi-email': { channel: 'linshi-email', name: '临时邮箱', website: 'linshi-email.com' },
|
|
@@ -46,21 +59,60 @@ const channelInfoMap: Record<Channel, ChannelInfo> = {
|
|
|
46
59
|
|
|
47
60
|
/**
|
|
48
61
|
* 获取所有支持的渠道列表
|
|
62
|
+
*
|
|
63
|
+
* @returns 所有渠道的信息数组
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* const channels = listChannels();
|
|
68
|
+
* channels.forEach(ch => console.log(`${ch.name} (${ch.website})`));
|
|
69
|
+
* ```
|
|
49
70
|
*/
|
|
50
71
|
export function listChannels(): ChannelInfo[] {
|
|
51
72
|
return allChannels.map(ch => channelInfoMap[ch]);
|
|
52
73
|
}
|
|
53
74
|
|
|
54
75
|
/**
|
|
55
|
-
*
|
|
76
|
+
* 获取指定渠道的详细信息
|
|
77
|
+
*
|
|
78
|
+
* @param channel - 渠道标识
|
|
79
|
+
* @returns 渠道信息,不存在时返回 undefined
|
|
56
80
|
*/
|
|
57
81
|
export function getChannelInfo(channel: Channel): ChannelInfo | undefined {
|
|
58
82
|
return channelInfoMap[channel];
|
|
59
83
|
}
|
|
60
84
|
|
|
85
|
+
/**
|
|
86
|
+
* 创建临时邮箱
|
|
87
|
+
*
|
|
88
|
+
* 错误处理策略:
|
|
89
|
+
* - 网络错误、超时、服务端 5xx 错误 → 自动重试(默认 2 次,指数退避)
|
|
90
|
+
* - 4xx 客户端错误、参数错误 → 直接抛出异常
|
|
91
|
+
*
|
|
92
|
+
* @param options - 创建选项,可指定渠道、有效时长、域名等
|
|
93
|
+
* @returns 邮箱信息,包含地址、令牌等
|
|
94
|
+
* @throws 重试耗尽后仍失败时抛出异常
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* const emailInfo = await generateEmail({ channel: 'temp-mail-io' });
|
|
99
|
+
* console.log(emailInfo.email); // 临时邮箱地址
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
61
102
|
export async function generateEmail(options: GenerateEmailOptions = {}): Promise<EmailInfo> {
|
|
62
103
|
const channel = options.channel || allChannels[Math.floor(Math.random() * allChannels.length)];
|
|
63
|
-
|
|
104
|
+
|
|
105
|
+
logger.info(`创建临时邮箱, 渠道: ${channel}`);
|
|
106
|
+
const result = await withRetry(() => generateEmailOnce(channel, options), options.retry);
|
|
107
|
+
logger.info(`邮箱创建成功: ${result.email}`);
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 单次创建邮箱(不含重试逻辑)
|
|
113
|
+
* 根据渠道类型分发到对应的 provider 实现
|
|
114
|
+
*/
|
|
115
|
+
async function generateEmailOnce(channel: Channel, options: GenerateEmailOptions): Promise<EmailInfo> {
|
|
64
116
|
switch (channel) {
|
|
65
117
|
case 'tempmail':
|
|
66
118
|
return tempmail.generateEmail(options.duration || 30);
|
|
@@ -85,9 +137,35 @@ export async function generateEmail(options: GenerateEmailOptions = {}): Promise
|
|
|
85
137
|
}
|
|
86
138
|
}
|
|
87
139
|
|
|
140
|
+
/**
|
|
141
|
+
* 获取邮件列表
|
|
142
|
+
*
|
|
143
|
+
* 错误处理策略:
|
|
144
|
+
* - 网络错误、超时、服务端 5xx 错误 → 自动重试(默认 2 次)
|
|
145
|
+
* - 重试耗尽后返回 { success: false, emails: [] },不抛异常
|
|
146
|
+
* - 参数校验错误(缺少 channel / token)直接抛出
|
|
147
|
+
*
|
|
148
|
+
* 这种设计让调用方在轮询场景下不会因网络波动而中断整个流程,
|
|
149
|
+
* 只需检查 success 字段即可判断本次请求是否成功。
|
|
150
|
+
*
|
|
151
|
+
* @param options - 获取选项,包含渠道、邮箱地址、令牌
|
|
152
|
+
* @returns 邮件结果,包含 success 标记和邮件列表
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```ts
|
|
156
|
+
* const result = await getEmails({
|
|
157
|
+
* channel: emailInfo.channel,
|
|
158
|
+
* email: emailInfo.email,
|
|
159
|
+
* token: emailInfo.token,
|
|
160
|
+
* });
|
|
161
|
+
* if (result.success && result.emails.length > 0) {
|
|
162
|
+
* console.log('收到邮件:', result.emails[0].subject);
|
|
163
|
+
* }
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
88
166
|
export async function getEmails(options: GetEmailsOptions): Promise<GetEmailsResult> {
|
|
89
167
|
const { channel, email, token } = options;
|
|
90
|
-
|
|
168
|
+
|
|
91
169
|
if (!channel) {
|
|
92
170
|
throw new Error('Channel is required');
|
|
93
171
|
}
|
|
@@ -95,68 +173,93 @@ export async function getEmails(options: GetEmailsOptions): Promise<GetEmailsRes
|
|
|
95
173
|
throw new Error('Email is required');
|
|
96
174
|
}
|
|
97
175
|
|
|
98
|
-
|
|
176
|
+
logger.debug(`获取邮件, 渠道: ${channel}, 邮箱: ${email}`);
|
|
177
|
+
try {
|
|
178
|
+
const emails = await withRetry(() => getEmailsOnce(channel, email, token), options.retry);
|
|
179
|
+
if (emails.length > 0) {
|
|
180
|
+
logger.info(`获取到 ${emails.length} 封邮件, 渠道: ${channel}`);
|
|
181
|
+
} else {
|
|
182
|
+
logger.debug(`暂无邮件, 渠道: ${channel}`);
|
|
183
|
+
}
|
|
184
|
+
return { channel, email, emails, success: true };
|
|
185
|
+
} catch (err: any) {
|
|
186
|
+
/*
|
|
187
|
+
* 重试耗尽后仍然失败 → 返回空结果而非抛异常
|
|
188
|
+
* 这样调用方在轮询场景下不会因为一次网络波动而中断整个流程
|
|
189
|
+
*/
|
|
190
|
+
logger.error(`获取邮件失败, 渠道: ${channel}, 错误: ${err.message || err}`);
|
|
191
|
+
return { channel, email, emails: [], success: false };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
99
194
|
|
|
195
|
+
/**
|
|
196
|
+
* 单次获取邮件(不含重试逻辑)
|
|
197
|
+
* 根据渠道类型分发到对应的 provider 实现,并校验必需的 token 参数
|
|
198
|
+
*/
|
|
199
|
+
async function getEmailsOnce(channel: Channel, email: string, token?: string): Promise<Email[]> {
|
|
100
200
|
switch (channel) {
|
|
101
201
|
case 'tempmail':
|
|
102
|
-
|
|
103
|
-
break;
|
|
202
|
+
return tempmail.getEmails(email);
|
|
104
203
|
case 'linshi-email':
|
|
105
|
-
|
|
106
|
-
break;
|
|
204
|
+
return linshiEmail.getEmails(email);
|
|
107
205
|
case 'tempmail-lol':
|
|
108
|
-
if (!token)
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
emails = await tempmailLol.getEmails(token, email);
|
|
112
|
-
break;
|
|
206
|
+
if (!token) throw new Error('Token is required for tempmail-lol channel');
|
|
207
|
+
return tempmailLol.getEmails(token, email);
|
|
113
208
|
case 'chatgpt-org-uk':
|
|
114
|
-
|
|
115
|
-
break;
|
|
209
|
+
return chatgptOrgUk.getEmails(email);
|
|
116
210
|
case 'tempmail-la':
|
|
117
|
-
|
|
118
|
-
break;
|
|
211
|
+
return tempmailLa.getEmails(email);
|
|
119
212
|
case 'temp-mail-io':
|
|
120
|
-
|
|
121
|
-
break;
|
|
213
|
+
return tempMailIO.getEmails(email);
|
|
122
214
|
case 'awamail':
|
|
123
|
-
if (!token)
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
emails = await awamail.getEmails(token, email);
|
|
127
|
-
break;
|
|
215
|
+
if (!token) throw new Error('Token is required for awamail channel');
|
|
216
|
+
return awamail.getEmails(token, email);
|
|
128
217
|
case 'mail-tm':
|
|
129
|
-
if (!token)
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
emails = await mailTm.getEmails(token, email);
|
|
133
|
-
break;
|
|
218
|
+
if (!token) throw new Error('Token is required for mail-tm channel');
|
|
219
|
+
return mailTm.getEmails(token, email);
|
|
134
220
|
case 'dropmail':
|
|
135
|
-
if (!token)
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
emails = await dropmail.getEmails(token, email);
|
|
139
|
-
break;
|
|
221
|
+
if (!token) throw new Error('Token is required for dropmail channel');
|
|
222
|
+
return dropmail.getEmails(token, email);
|
|
140
223
|
default:
|
|
141
224
|
throw new Error(`Unknown channel: ${channel}`);
|
|
142
225
|
}
|
|
143
|
-
|
|
144
|
-
return {
|
|
145
|
-
channel,
|
|
146
|
-
email,
|
|
147
|
-
emails,
|
|
148
|
-
success: true,
|
|
149
|
-
};
|
|
150
226
|
}
|
|
151
227
|
|
|
228
|
+
/**
|
|
229
|
+
* 临时邮箱客户端
|
|
230
|
+
* 封装了邮箱创建和邮件获取的完整流程,自动管理邮箱信息和认证令牌
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```ts
|
|
234
|
+
* const client = new TempEmailClient();
|
|
235
|
+
* const emailInfo = await client.generate({ channel: 'mail-tm' });
|
|
236
|
+
* console.log('邮箱:', emailInfo.email);
|
|
237
|
+
*
|
|
238
|
+
* // 轮询获取邮件
|
|
239
|
+
* const result = await client.getEmails();
|
|
240
|
+
* if (result.success) {
|
|
241
|
+
* console.log('邮件数:', result.emails.length);
|
|
242
|
+
* }
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
152
245
|
export class TempEmailClient {
|
|
153
246
|
private emailInfo: EmailInfo | null = null;
|
|
154
247
|
|
|
248
|
+
/**
|
|
249
|
+
* 创建临时邮箱并缓存邮箱信息
|
|
250
|
+
* 后续调用 getEmails() 时自动使用此邮箱的渠道、地址和令牌
|
|
251
|
+
*/
|
|
155
252
|
async generate(options: GenerateEmailOptions = {}): Promise<EmailInfo> {
|
|
156
253
|
this.emailInfo = await generateEmail(options);
|
|
157
254
|
return this.emailInfo;
|
|
158
255
|
}
|
|
159
256
|
|
|
257
|
+
/**
|
|
258
|
+
* 获取当前邮箱的邮件列表
|
|
259
|
+
* 必须先调用 generate() 创建邮箱
|
|
260
|
+
*
|
|
261
|
+
* @throws 未调用 generate() 时抛出异常
|
|
262
|
+
*/
|
|
160
263
|
async getEmails(): Promise<GetEmailsResult> {
|
|
161
264
|
if (!this.emailInfo) {
|
|
162
265
|
throw new Error('No email generated. Call generate() first.');
|
|
@@ -169,6 +272,10 @@ export class TempEmailClient {
|
|
|
169
272
|
});
|
|
170
273
|
}
|
|
171
274
|
|
|
275
|
+
/**
|
|
276
|
+
* 获取当前缓存的邮箱信息
|
|
277
|
+
* 未调用 generate() 时返回 null
|
|
278
|
+
*/
|
|
172
279
|
getEmailInfo(): EmailInfo | null {
|
|
173
280
|
return this.emailInfo;
|
|
174
281
|
}
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK 日志模块
|
|
3
|
+
* 提供分级日志能力,支持自定义日志处理器
|
|
4
|
+
* 默认静默不输出,用户可通过 setLogLevel / setLogger 启用
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 日志级别枚举
|
|
9
|
+
* 数值越小级别越高,设置某级别后只输出该级别及以上的日志
|
|
10
|
+
*/
|
|
11
|
+
export enum LogLevel {
|
|
12
|
+
/** 关闭所有日志 */
|
|
13
|
+
SILENT = 0,
|
|
14
|
+
/** 错误日志:请求失败、重试耗尽等 */
|
|
15
|
+
ERROR = 1,
|
|
16
|
+
/** 警告日志:重试中、降级处理等 */
|
|
17
|
+
WARN = 2,
|
|
18
|
+
/** 信息日志:请求开始、完成等关键流程 */
|
|
19
|
+
INFO = 3,
|
|
20
|
+
/** 调试日志:请求详情、响应内容等 */
|
|
21
|
+
DEBUG = 4,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 日志处理器接口
|
|
26
|
+
* 用户可实现此接口来自定义日志输出方式(如写文件、发送到远程等)
|
|
27
|
+
*/
|
|
28
|
+
export interface LogHandler {
|
|
29
|
+
error(message: string, ...args: any[]): void;
|
|
30
|
+
warn(message: string, ...args: any[]): void;
|
|
31
|
+
info(message: string, ...args: any[]): void;
|
|
32
|
+
debug(message: string, ...args: any[]): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 默认日志处理器,直接输出到 console
|
|
37
|
+
*/
|
|
38
|
+
const defaultHandler: LogHandler = {
|
|
39
|
+
error: (msg, ...args) => console.error(msg, ...args),
|
|
40
|
+
warn: (msg, ...args) => console.warn(msg, ...args),
|
|
41
|
+
info: (msg, ...args) => console.info(msg, ...args),
|
|
42
|
+
debug: (msg, ...args) => console.debug(msg, ...args),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
let currentLevel: LogLevel = LogLevel.SILENT;
|
|
46
|
+
let currentHandler: LogHandler = defaultHandler;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 设置日志级别
|
|
50
|
+
* 默认 SILENT(不输出任何日志)
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* import { setLogLevel, LogLevel } from 'tempmail-sdk';
|
|
55
|
+
* setLogLevel(LogLevel.DEBUG); // 开启所有日志
|
|
56
|
+
* setLogLevel(LogLevel.INFO); // 只输出 INFO 及以上
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function setLogLevel(level: LogLevel): void {
|
|
60
|
+
currentLevel = level;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 获取当前日志级别
|
|
65
|
+
*/
|
|
66
|
+
export function getLogLevel(): LogLevel {
|
|
67
|
+
return currentLevel;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 设置自定义日志处理器
|
|
72
|
+
* 替换默认的 console 输出
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* import { setLogger } from 'tempmail-sdk';
|
|
77
|
+
* setLogger({
|
|
78
|
+
* error: (msg, ...args) => myLogger.error(msg, ...args),
|
|
79
|
+
* warn: (msg, ...args) => myLogger.warn(msg, ...args),
|
|
80
|
+
* info: (msg, ...args) => myLogger.info(msg, ...args),
|
|
81
|
+
* debug: (msg, ...args) => myLogger.debug(msg, ...args),
|
|
82
|
+
* });
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function setLogger(handler: LogHandler): void {
|
|
86
|
+
currentHandler = handler;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* SDK 内部日志工具
|
|
91
|
+
* 根据当前日志级别过滤输出
|
|
92
|
+
*/
|
|
93
|
+
export const logger = {
|
|
94
|
+
error(msg: string, ...args: any[]): void {
|
|
95
|
+
if (currentLevel >= LogLevel.ERROR) currentHandler.error(msg, ...args);
|
|
96
|
+
},
|
|
97
|
+
warn(msg: string, ...args: any[]): void {
|
|
98
|
+
if (currentLevel >= LogLevel.WARN) currentHandler.warn(msg, ...args);
|
|
99
|
+
},
|
|
100
|
+
info(msg: string, ...args: any[]): void {
|
|
101
|
+
if (currentLevel >= LogLevel.INFO) currentHandler.info(msg, ...args);
|
|
102
|
+
},
|
|
103
|
+
debug(msg: string, ...args: any[]): void {
|
|
104
|
+
if (currentLevel >= LogLevel.DEBUG) currentHandler.debug(msg, ...args);
|
|
105
|
+
},
|
|
106
|
+
};
|
package/src/normalize.ts
CHANGED
|
@@ -2,6 +2,13 @@ import { Email, EmailAttachment } from './types';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* 将各提供商返回的原始邮件数据标准化为统一的 Email 格式
|
|
5
|
+
*
|
|
6
|
+
* 不同渠道的 API 返回字段名各不相同,此函数通过多字段候选策略
|
|
7
|
+
* 将它们统一映射为标准的 Email 结构,保证 SDK 输出一致性。
|
|
8
|
+
*
|
|
9
|
+
* @param raw - 原始邮件数据(来自不同提供商的 API 响应)
|
|
10
|
+
* @param recipientEmail - 收件人邮箱地址,当原始数据中无收件人字段时用作回退值
|
|
11
|
+
* @returns 标准化的 Email 对象
|
|
5
12
|
*/
|
|
6
13
|
export function normalizeEmail(raw: any, recipientEmail: string = ''): Email {
|
|
7
14
|
return {
|
|
@@ -17,31 +24,60 @@ export function normalizeEmail(raw: any, recipientEmail: string = ''): Email {
|
|
|
17
24
|
};
|
|
18
25
|
}
|
|
19
26
|
|
|
27
|
+
/**
|
|
28
|
+
* 提取邮件 ID
|
|
29
|
+
* 候选字段: id, eid, _id, mailboxId, messageId, mail_id
|
|
30
|
+
*/
|
|
20
31
|
function normalizeId(raw: any): string {
|
|
21
32
|
const id = raw.id ?? raw.eid ?? raw._id ?? raw.mailboxId ?? raw.messageId ?? raw.mail_id ?? '';
|
|
22
33
|
return String(id);
|
|
23
34
|
}
|
|
24
35
|
|
|
36
|
+
/**
|
|
37
|
+
* 提取发件人地址
|
|
38
|
+
* 候选字段: from_address, address_from, from, messageFrom, sender
|
|
39
|
+
*/
|
|
25
40
|
function normalizeFrom(raw: any): string {
|
|
26
41
|
return raw.from_address || raw.address_from || raw.from || raw.messageFrom || raw.sender || '';
|
|
27
42
|
}
|
|
28
43
|
|
|
44
|
+
/**
|
|
45
|
+
* 提取收件人地址,无匹配字段时回退为 recipientEmail
|
|
46
|
+
* 候选字段: to, to_address, name_to, email_address, address
|
|
47
|
+
*/
|
|
29
48
|
function normalizeTo(raw: any, recipientEmail: string): string {
|
|
30
49
|
return raw.to || raw.to_address || raw.name_to || raw.email_address || raw.address || recipientEmail || '';
|
|
31
50
|
}
|
|
32
51
|
|
|
52
|
+
/**
|
|
53
|
+
* 提取邮件主题
|
|
54
|
+
* 候选字段: subject, e_subject
|
|
55
|
+
*/
|
|
33
56
|
function normalizeSubject(raw: any): string {
|
|
34
57
|
return raw.subject || raw.e_subject || '';
|
|
35
58
|
}
|
|
36
59
|
|
|
60
|
+
/**
|
|
61
|
+
* 提取纯文本内容
|
|
62
|
+
* 候选字段: text, body, content, body_text, text_content
|
|
63
|
+
*/
|
|
37
64
|
function normalizeText(raw: any): string {
|
|
38
65
|
return raw.text || raw.body || raw.content || raw.body_text || raw.text_content || '';
|
|
39
66
|
}
|
|
40
67
|
|
|
68
|
+
/**
|
|
69
|
+
* 提取 HTML 内容
|
|
70
|
+
* 候选字段: html, html_content, body_html
|
|
71
|
+
*/
|
|
41
72
|
function normalizeHtml(raw: any): string {
|
|
42
73
|
return raw.html || raw.html_content || raw.body_html || '';
|
|
43
74
|
}
|
|
44
75
|
|
|
76
|
+
/**
|
|
77
|
+
* 提取并统一日期格式为 ISO 8601
|
|
78
|
+
* 候选字段: received_at, created_at, createdAt, date, timestamp, e_date
|
|
79
|
+
* 其中 timestamp 为 Unix 秒级时间戳,需乘以 1000 转为毫秒
|
|
80
|
+
*/
|
|
45
81
|
function normalizeDate(raw: any): string {
|
|
46
82
|
try {
|
|
47
83
|
if (raw.received_at) return new Date(raw.received_at).toISOString();
|
|
@@ -54,11 +90,16 @@ function normalizeDate(raw: any): string {
|
|
|
54
90
|
if (raw.timestamp) return new Date(raw.timestamp * 1000).toISOString();
|
|
55
91
|
if (raw.e_date) return new Date(raw.e_date).toISOString();
|
|
56
92
|
} catch {
|
|
57
|
-
|
|
93
|
+
/* 日期解析失败,返回空字符串 */
|
|
58
94
|
}
|
|
59
95
|
return '';
|
|
60
96
|
}
|
|
61
97
|
|
|
98
|
+
/**
|
|
99
|
+
* 提取已读状态
|
|
100
|
+
* 候选字段: seen, read, isRead, is_read
|
|
101
|
+
* 支持 boolean / number(0|1) / string('0'|'1') 多种类型
|
|
102
|
+
*/
|
|
62
103
|
function normalizeIsRead(raw: any): boolean {
|
|
63
104
|
if (typeof raw.seen === 'boolean') return raw.seen;
|
|
64
105
|
if (typeof raw.read === 'boolean') return raw.read;
|
|
@@ -69,6 +110,10 @@ function normalizeIsRead(raw: any): boolean {
|
|
|
69
110
|
return false;
|
|
70
111
|
}
|
|
71
112
|
|
|
113
|
+
/**
|
|
114
|
+
* 提取并标准化附件列表
|
|
115
|
+
* 每个附件的字段也采用多候选策略映射
|
|
116
|
+
*/
|
|
72
117
|
function normalizeAttachments(attachments: any): EmailAttachment[] {
|
|
73
118
|
if (!attachments || !Array.isArray(attachments)) return [];
|
|
74
119
|
return attachments.map((a: any) => ({
|
package/src/providers/mail-tm.ts
CHANGED
|
@@ -36,7 +36,11 @@ async function getDomains(): Promise<string[]> {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const data = await response.json();
|
|
39
|
-
|
|
39
|
+
/* 兼容两种响应格式:
|
|
40
|
+
* - Accept: application/ld+json → Hydra 格式 { "hydra:member": [...] }
|
|
41
|
+
* - Accept: application/json → 纯数组 [...]
|
|
42
|
+
*/
|
|
43
|
+
const members = Array.isArray(data) ? data : (data['hydra:member'] || []);
|
|
40
44
|
return members
|
|
41
45
|
.filter((d: any) => d.isActive)
|
|
42
46
|
.map((d: any) => d.domain);
|
|
@@ -153,7 +157,8 @@ export async function getEmails(token: string, email: string): Promise<Email[]>
|
|
|
153
157
|
}
|
|
154
158
|
|
|
155
159
|
const listData = await listResponse.json();
|
|
156
|
-
|
|
160
|
+
/* 兼容 Hydra 格式和纯数组格式 */
|
|
161
|
+
const messages = Array.isArray(listData) ? listData : (listData['hydra:member'] || []);
|
|
157
162
|
|
|
158
163
|
if (messages.length === 0) {
|
|
159
164
|
return [];
|