tempmail-sdk 1.0.1 → 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/README.md +162 -72
- package/demo/poll-emails.ts +25 -40
- package/dist/index.d.ts +96 -2
- package/dist/index.js +186 -21
- package/dist/logger.d.ts +73 -0
- package/dist/logger.js +100 -0
- package/dist/normalize.d.ts +12 -0
- package/dist/normalize.js +132 -0
- package/dist/providers/awamail.d.ts +15 -0
- package/dist/providers/awamail.js +91 -0
- package/dist/providers/chatgpt-org-uk.js +4 -2
- package/dist/providers/dropmail.d.ts +13 -0
- package/dist/providers/dropmail.js +86 -0
- package/dist/providers/linshi-email.js +4 -2
- package/dist/providers/mail-tm.d.ts +11 -0
- package/dist/providers/mail-tm.js +172 -0
- package/dist/providers/temp-mail-io.d.ts +13 -0
- package/dist/providers/temp-mail-io.js +99 -0
- package/dist/providers/tempmail-la.d.ts +15 -0
- package/dist/providers/tempmail-la.js +89 -0
- package/dist/providers/tempmail-lol.d.ts +1 -1
- package/dist/providers/tempmail-lol.js +5 -3
- package/dist/providers/tempmail.js +4 -2
- package/dist/retry.d.ts +38 -0
- package/dist/retry.js +121 -0
- package/dist/types.d.ts +118 -28
- package/dist/types.js +1 -1
- package/package.json +1 -1
- package/src/index.ts +182 -25
- package/src/logger.ts +106 -0
- package/src/normalize.ts +125 -0
- package/src/providers/awamail.ts +101 -0
- package/src/providers/chatgpt-org-uk.ts +3 -1
- package/src/providers/dropmail.ts +98 -0
- package/src/providers/linshi-email.ts +3 -1
- package/src/providers/mail-tm.ts +193 -0
- package/src/providers/temp-mail-io.ts +108 -0
- package/src/providers/tempmail-la.ts +99 -0
- package/src/providers/tempmail-lol.ts +4 -2
- package/src/providers/tempmail.ts +3 -1
- package/src/retry.ts +146 -0
- package/src/types.ts +120 -28
- package/test/example.ts +16 -1
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
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Email, EmailAttachment } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 将各提供商返回的原始邮件数据标准化为统一的 Email 格式
|
|
5
|
+
*
|
|
6
|
+
* 不同渠道的 API 返回字段名各不相同,此函数通过多字段候选策略
|
|
7
|
+
* 将它们统一映射为标准的 Email 结构,保证 SDK 输出一致性。
|
|
8
|
+
*
|
|
9
|
+
* @param raw - 原始邮件数据(来自不同提供商的 API 响应)
|
|
10
|
+
* @param recipientEmail - 收件人邮箱地址,当原始数据中无收件人字段时用作回退值
|
|
11
|
+
* @returns 标准化的 Email 对象
|
|
12
|
+
*/
|
|
13
|
+
export function normalizeEmail(raw: any, recipientEmail: string = ''): Email {
|
|
14
|
+
return {
|
|
15
|
+
id: normalizeId(raw),
|
|
16
|
+
from: normalizeFrom(raw),
|
|
17
|
+
to: normalizeTo(raw, recipientEmail),
|
|
18
|
+
subject: normalizeSubject(raw),
|
|
19
|
+
text: normalizeText(raw),
|
|
20
|
+
html: normalizeHtml(raw),
|
|
21
|
+
date: normalizeDate(raw),
|
|
22
|
+
isRead: normalizeIsRead(raw),
|
|
23
|
+
attachments: normalizeAttachments(raw.attachments),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 提取邮件 ID
|
|
29
|
+
* 候选字段: id, eid, _id, mailboxId, messageId, mail_id
|
|
30
|
+
*/
|
|
31
|
+
function normalizeId(raw: any): string {
|
|
32
|
+
const id = raw.id ?? raw.eid ?? raw._id ?? raw.mailboxId ?? raw.messageId ?? raw.mail_id ?? '';
|
|
33
|
+
return String(id);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 提取发件人地址
|
|
38
|
+
* 候选字段: from_address, address_from, from, messageFrom, sender
|
|
39
|
+
*/
|
|
40
|
+
function normalizeFrom(raw: any): string {
|
|
41
|
+
return raw.from_address || raw.address_from || raw.from || raw.messageFrom || raw.sender || '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 提取收件人地址,无匹配字段时回退为 recipientEmail
|
|
46
|
+
* 候选字段: to, to_address, name_to, email_address, address
|
|
47
|
+
*/
|
|
48
|
+
function normalizeTo(raw: any, recipientEmail: string): string {
|
|
49
|
+
return raw.to || raw.to_address || raw.name_to || raw.email_address || raw.address || recipientEmail || '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 提取邮件主题
|
|
54
|
+
* 候选字段: subject, e_subject
|
|
55
|
+
*/
|
|
56
|
+
function normalizeSubject(raw: any): string {
|
|
57
|
+
return raw.subject || raw.e_subject || '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 提取纯文本内容
|
|
62
|
+
* 候选字段: text, body, content, body_text, text_content
|
|
63
|
+
*/
|
|
64
|
+
function normalizeText(raw: any): string {
|
|
65
|
+
return raw.text || raw.body || raw.content || raw.body_text || raw.text_content || '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 提取 HTML 内容
|
|
70
|
+
* 候选字段: html, html_content, body_html
|
|
71
|
+
*/
|
|
72
|
+
function normalizeHtml(raw: any): string {
|
|
73
|
+
return raw.html || raw.html_content || raw.body_html || '';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 提取并统一日期格式为 ISO 8601
|
|
78
|
+
* 候选字段: received_at, created_at, createdAt, date, timestamp, e_date
|
|
79
|
+
* 其中 timestamp 为 Unix 秒级时间戳,需乘以 1000 转为毫秒
|
|
80
|
+
*/
|
|
81
|
+
function normalizeDate(raw: any): string {
|
|
82
|
+
try {
|
|
83
|
+
if (raw.received_at) return new Date(raw.received_at).toISOString();
|
|
84
|
+
if (raw.created_at) return new Date(raw.created_at).toISOString();
|
|
85
|
+
if (raw.createdAt) return new Date(raw.createdAt).toISOString();
|
|
86
|
+
if (raw.date) {
|
|
87
|
+
if (typeof raw.date === 'number') return new Date(raw.date).toISOString();
|
|
88
|
+
return new Date(raw.date).toISOString();
|
|
89
|
+
}
|
|
90
|
+
if (raw.timestamp) return new Date(raw.timestamp * 1000).toISOString();
|
|
91
|
+
if (raw.e_date) return new Date(raw.e_date).toISOString();
|
|
92
|
+
} catch {
|
|
93
|
+
/* 日期解析失败,返回空字符串 */
|
|
94
|
+
}
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 提取已读状态
|
|
100
|
+
* 候选字段: seen, read, isRead, is_read
|
|
101
|
+
* 支持 boolean / number(0|1) / string('0'|'1') 多种类型
|
|
102
|
+
*/
|
|
103
|
+
function normalizeIsRead(raw: any): boolean {
|
|
104
|
+
if (typeof raw.seen === 'boolean') return raw.seen;
|
|
105
|
+
if (typeof raw.read === 'boolean') return raw.read;
|
|
106
|
+
if (typeof raw.isRead === 'boolean') return raw.isRead;
|
|
107
|
+
if (typeof raw.is_read === 'number') return raw.is_read === 1;
|
|
108
|
+
if (typeof raw.is_read === 'string') return raw.is_read === '1';
|
|
109
|
+
if (typeof raw.is_read === 'boolean') return raw.is_read;
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 提取并标准化附件列表
|
|
115
|
+
* 每个附件的字段也采用多候选策略映射
|
|
116
|
+
*/
|
|
117
|
+
function normalizeAttachments(attachments: any): EmailAttachment[] {
|
|
118
|
+
if (!attachments || !Array.isArray(attachments)) return [];
|
|
119
|
+
return attachments.map((a: any) => ({
|
|
120
|
+
filename: a.filename || a.name || '',
|
|
121
|
+
size: a.size || a.filesize || undefined,
|
|
122
|
+
contentType: a.contentType || a.content_type || a.mimeType || a.mime_type || undefined,
|
|
123
|
+
url: a.url || a.download_url || a.downloadUrl || undefined,
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { EmailInfo, Email, Channel } from '../types';
|
|
2
|
+
import { normalizeEmail } from '../normalize';
|
|
3
|
+
|
|
4
|
+
const CHANNEL: Channel = 'awamail';
|
|
5
|
+
const BASE_URL = 'https://awamail.com/welcome';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_HEADERS: Record<string, string> = {
|
|
8
|
+
'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',
|
|
9
|
+
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
|
10
|
+
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
|
|
11
|
+
'cache-control': 'no-cache',
|
|
12
|
+
'dnt': '1',
|
|
13
|
+
'origin': 'https://awamail.com',
|
|
14
|
+
'pragma': 'no-cache',
|
|
15
|
+
'referer': 'https://awamail.com/?lang=zh',
|
|
16
|
+
'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Microsoft Edge";v="144"',
|
|
17
|
+
'sec-ch-ua-mobile': '?0',
|
|
18
|
+
'sec-ch-ua-platform': '"Windows"',
|
|
19
|
+
'sec-fetch-dest': 'empty',
|
|
20
|
+
'sec-fetch-mode': 'cors',
|
|
21
|
+
'sec-fetch-site': 'same-origin',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 从 Set-Cookie 响应头中提取 awamail_session 值
|
|
26
|
+
*/
|
|
27
|
+
function extractSessionCookie(response: Response): string {
|
|
28
|
+
const setCookie = response.headers.get('set-cookie') || '';
|
|
29
|
+
const match = setCookie.match(/awamail_session=([^;]+)/);
|
|
30
|
+
return match ? `awamail_session=${match[1]}` : '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 创建临时邮箱
|
|
35
|
+
* API: POST /welcome/change_mailbox (空 body)
|
|
36
|
+
* 返回: { success, data: { email_address, expired_at, created_at, ... } }
|
|
37
|
+
* 需要保存响应中的 Set-Cookie (awamail_session) 用于后续获取邮件
|
|
38
|
+
*/
|
|
39
|
+
export async function generateEmail(): Promise<EmailInfo> {
|
|
40
|
+
const response = await fetch(`${BASE_URL}/change_mailbox`, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: {
|
|
43
|
+
...DEFAULT_HEADERS,
|
|
44
|
+
'Content-Length': '0',
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(`Failed to generate email: ${response.status}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 提取 session cookie
|
|
53
|
+
const sessionCookie = extractSessionCookie(response);
|
|
54
|
+
if (!sessionCookie) {
|
|
55
|
+
throw new Error('Failed to extract session cookie');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const data = await response.json();
|
|
59
|
+
|
|
60
|
+
if (!data.success || !data.data) {
|
|
61
|
+
throw new Error('Failed to generate email');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
channel: CHANNEL,
|
|
66
|
+
email: data.data.email_address,
|
|
67
|
+
token: sessionCookie,
|
|
68
|
+
expiresAt: data.data.expired_at,
|
|
69
|
+
createdAt: data.data.created_at,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 获取邮件列表
|
|
75
|
+
* API: GET /welcome/get_emails
|
|
76
|
+
* 需要传入 Cookie (awamail_session) 和 x-requested-with 头
|
|
77
|
+
* 返回: { success, data: { emails: [...], latest: {...} } }
|
|
78
|
+
*/
|
|
79
|
+
export async function getEmails(token: string, email: string): Promise<Email[]> {
|
|
80
|
+
const response = await fetch(`${BASE_URL}/get_emails`, {
|
|
81
|
+
method: 'GET',
|
|
82
|
+
headers: {
|
|
83
|
+
...DEFAULT_HEADERS,
|
|
84
|
+
'Cookie': token,
|
|
85
|
+
'x-requested-with': 'XMLHttpRequest',
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw new Error(`Failed to get emails: ${response.status}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const data = await response.json();
|
|
94
|
+
|
|
95
|
+
if (!data.success || !data.data) {
|
|
96
|
+
throw new Error('Failed to get emails');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const rawEmails = data.data.emails || [];
|
|
100
|
+
return rawEmails.map((raw: any) => normalizeEmail(raw, email));
|
|
101
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EmailInfo, Email, Channel } from '../types';
|
|
2
|
+
import { normalizeEmail } from '../normalize';
|
|
2
3
|
|
|
3
4
|
const CHANNEL: Channel = 'chatgpt-org-uk';
|
|
4
5
|
const BASE_URL = 'https://mail.chatgpt.org.uk/api';
|
|
@@ -53,5 +54,6 @@ export async function getEmails(email: string): Promise<Email[]> {
|
|
|
53
54
|
throw new Error('Failed to get emails');
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
const rawEmails = data.data?.emails || [];
|
|
58
|
+
return rawEmails.map((raw: any) => normalizeEmail(raw, email));
|
|
57
59
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { EmailInfo, Email, Channel } from '../types';
|
|
2
|
+
import { normalizeEmail } from '../normalize';
|
|
3
|
+
|
|
4
|
+
const CHANNEL: Channel = 'dropmail';
|
|
5
|
+
const BASE_URL = 'https://dropmail.me/api/graphql/MY_TOKEN';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_HEADERS: Record<string, string> = {
|
|
8
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const CREATE_SESSION_QUERY = 'mutation {introduceSession {id, expiresAt, addresses{id, address}}}';
|
|
12
|
+
|
|
13
|
+
const GET_MAILS_QUERY = `query ($id: ID!) {
|
|
14
|
+
session(id:$id) {
|
|
15
|
+
mails {
|
|
16
|
+
id, rawSize, fromAddr, toAddr, receivedAt,
|
|
17
|
+
text, headerFrom, headerSubject, html
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}`;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 执行 GraphQL 请求
|
|
24
|
+
*/
|
|
25
|
+
async function graphqlRequest(query: string, variables?: Record<string, any>): Promise<any> {
|
|
26
|
+
const params = new URLSearchParams();
|
|
27
|
+
params.set('query', query);
|
|
28
|
+
if (variables) {
|
|
29
|
+
params.set('variables', JSON.stringify(variables));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const response = await fetch(BASE_URL, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: DEFAULT_HEADERS,
|
|
35
|
+
body: params.toString(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(`GraphQL request failed: ${response.status}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
|
|
44
|
+
if (data.errors) {
|
|
45
|
+
throw new Error(`GraphQL error: ${data.errors[0]?.message || 'Unknown error'}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return data.data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 创建临时邮箱
|
|
53
|
+
* GraphQL mutation: introduceSession
|
|
54
|
+
* 返回 session ID (存入 token) 和邮箱地址
|
|
55
|
+
*/
|
|
56
|
+
export async function generateEmail(): Promise<EmailInfo> {
|
|
57
|
+
const data = await graphqlRequest(CREATE_SESSION_QUERY);
|
|
58
|
+
|
|
59
|
+
const session = data?.introduceSession;
|
|
60
|
+
if (!session || !session.addresses || session.addresses.length === 0) {
|
|
61
|
+
throw new Error('Failed to generate email');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
channel: CHANNEL,
|
|
66
|
+
email: session.addresses[0].address,
|
|
67
|
+
token: session.id,
|
|
68
|
+
expiresAt: session.expiresAt,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 将 dropmail 的邮件格式扁平化为 normalizeEmail 可处理的格式
|
|
74
|
+
*/
|
|
75
|
+
function flattenMessage(mail: any, recipientEmail: string): any {
|
|
76
|
+
return {
|
|
77
|
+
id: mail.id || '',
|
|
78
|
+
from: mail.fromAddr || '',
|
|
79
|
+
to: mail.toAddr || recipientEmail,
|
|
80
|
+
subject: mail.headerSubject || '',
|
|
81
|
+
text: mail.text || '',
|
|
82
|
+
html: mail.html || '',
|
|
83
|
+
received_at: mail.receivedAt || '',
|
|
84
|
+
attachments: [],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 获取邮件列表
|
|
90
|
+
* GraphQL query: session(id) { mails {...} }
|
|
91
|
+
* token 中存储的是 session ID
|
|
92
|
+
*/
|
|
93
|
+
export async function getEmails(token: string, email: string): Promise<Email[]> {
|
|
94
|
+
const data = await graphqlRequest(GET_MAILS_QUERY, { id: token });
|
|
95
|
+
|
|
96
|
+
const mails = data?.session?.mails || [];
|
|
97
|
+
return mails.map((mail: any) => normalizeEmail(flattenMessage(mail, email), email));
|
|
98
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EmailInfo, Email, Channel } from '../types';
|
|
2
|
+
import { normalizeEmail } from '../normalize';
|
|
2
3
|
|
|
3
4
|
const CHANNEL: Channel = 'linshi-email';
|
|
4
5
|
const BASE_URL = 'https://www.linshi-email.com/api/v1';
|
|
@@ -57,5 +58,6 @@ export async function getEmails(email: string): Promise<Email[]> {
|
|
|
57
58
|
throw new Error('Failed to get emails');
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
const rawEmails = data.list || [];
|
|
62
|
+
return rawEmails.map((raw: any) => normalizeEmail(raw, email));
|
|
61
63
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { EmailInfo, Email, Channel } from '../types';
|
|
2
|
+
import { normalizeEmail } from '../normalize';
|
|
3
|
+
|
|
4
|
+
const CHANNEL: Channel = 'mail-tm';
|
|
5
|
+
const BASE_URL = 'https://api.mail.tm';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_HEADERS: Record<string, string> = {
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
'Accept': 'application/json',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 生成随机字符串
|
|
14
|
+
*/
|
|
15
|
+
function randomString(length: number): string {
|
|
16
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
17
|
+
let result = '';
|
|
18
|
+
for (let i = 0; i < length; i++) {
|
|
19
|
+
result += chars[Math.floor(Math.random() * chars.length)];
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 获取可用域名列表
|
|
26
|
+
* API: GET /domains
|
|
27
|
+
*/
|
|
28
|
+
async function getDomains(): Promise<string[]> {
|
|
29
|
+
const response = await fetch(`${BASE_URL}/domains`, {
|
|
30
|
+
method: 'GET',
|
|
31
|
+
headers: DEFAULT_HEADERS,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
throw new Error(`Failed to get domains: ${response.status}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const data = await response.json();
|
|
39
|
+
/* 兼容两种响应格式:
|
|
40
|
+
* - Accept: application/ld+json → Hydra 格式 { "hydra:member": [...] }
|
|
41
|
+
* - Accept: application/json → 纯数组 [...]
|
|
42
|
+
*/
|
|
43
|
+
const members = Array.isArray(data) ? data : (data['hydra:member'] || []);
|
|
44
|
+
return members
|
|
45
|
+
.filter((d: any) => d.isActive)
|
|
46
|
+
.map((d: any) => d.domain);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 创建账号
|
|
51
|
+
* API: POST /accounts
|
|
52
|
+
*/
|
|
53
|
+
async function createAccount(address: string, password: string): Promise<any> {
|
|
54
|
+
const response = await fetch(`${BASE_URL}/accounts`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { ...DEFAULT_HEADERS, 'Content-Type': 'application/ld+json' },
|
|
57
|
+
body: JSON.stringify({ address, password }),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const text = await response.text();
|
|
62
|
+
throw new Error(`Failed to create account: ${response.status} ${text}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return response.json();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 获取 Bearer Token
|
|
70
|
+
* API: POST /token
|
|
71
|
+
*/
|
|
72
|
+
async function getToken(address: string, password: string): Promise<string> {
|
|
73
|
+
const response = await fetch(`${BASE_URL}/token`, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: DEFAULT_HEADERS,
|
|
76
|
+
body: JSON.stringify({ address, password }),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw new Error(`Failed to get token: ${response.status}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const data = await response.json();
|
|
84
|
+
return data.token;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 创建临时邮箱
|
|
89
|
+
* 流程: 获取域名 → 生成随机邮箱/密码 → 创建账号 → 获取 Token
|
|
90
|
+
*/
|
|
91
|
+
export async function generateEmail(): Promise<EmailInfo> {
|
|
92
|
+
// 1. 获取可用域名
|
|
93
|
+
const domains = await getDomains();
|
|
94
|
+
if (domains.length === 0) {
|
|
95
|
+
throw new Error('No available domains');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 2. 生成随机邮箱和密码
|
|
99
|
+
const domain = domains[Math.floor(Math.random() * domains.length)];
|
|
100
|
+
const username = randomString(12);
|
|
101
|
+
const address = `${username}@${domain}`;
|
|
102
|
+
const password = randomString(16);
|
|
103
|
+
|
|
104
|
+
// 3. 创建账号
|
|
105
|
+
const account = await createAccount(address, password);
|
|
106
|
+
|
|
107
|
+
// 4. 获取 Bearer Token
|
|
108
|
+
const token = await getToken(address, password);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
channel: CHANNEL,
|
|
112
|
+
email: address,
|
|
113
|
+
token: token,
|
|
114
|
+
createdAt: account.createdAt,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 将 mail.tm 的消息格式扁平化为 normalizeEmail 可处理的格式
|
|
120
|
+
* mail.tm 的 from 是 {name, address} 对象,to 是数组,html 是字符串数组
|
|
121
|
+
*/
|
|
122
|
+
function flattenMessage(msg: any, recipientEmail: string): any {
|
|
123
|
+
return {
|
|
124
|
+
id: msg.id || '',
|
|
125
|
+
from: msg.from?.address || '',
|
|
126
|
+
to: msg.to?.[0]?.address || recipientEmail,
|
|
127
|
+
subject: msg.subject || '',
|
|
128
|
+
text: msg.text || '',
|
|
129
|
+
html: Array.isArray(msg.html) ? msg.html.join('') : (msg.html || ''),
|
|
130
|
+
createdAt: msg.createdAt || '',
|
|
131
|
+
seen: msg.seen ?? false,
|
|
132
|
+
attachments: (msg.attachments || []).map((a: any) => ({
|
|
133
|
+
filename: a.filename || '',
|
|
134
|
+
size: a.size || undefined,
|
|
135
|
+
contentType: a.contentType || undefined,
|
|
136
|
+
downloadUrl: a.downloadUrl ? `${BASE_URL}${a.downloadUrl}` : undefined,
|
|
137
|
+
})),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 获取邮件列表
|
|
143
|
+
* 流程: GET /messages 获取列表 → 对每封邮件 GET /messages/{id} 获取详情(含 text/html)
|
|
144
|
+
*/
|
|
145
|
+
export async function getEmails(token: string, email: string): Promise<Email[]> {
|
|
146
|
+
// 1. 获取邮件列表
|
|
147
|
+
const listResponse = await fetch(`${BASE_URL}/messages`, {
|
|
148
|
+
method: 'GET',
|
|
149
|
+
headers: {
|
|
150
|
+
...DEFAULT_HEADERS,
|
|
151
|
+
'Authorization': `Bearer ${token}`,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!listResponse.ok) {
|
|
156
|
+
throw new Error(`Failed to get emails: ${listResponse.status}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const listData = await listResponse.json();
|
|
160
|
+
/* 兼容 Hydra 格式和纯数组格式 */
|
|
161
|
+
const messages = Array.isArray(listData) ? listData : (listData['hydra:member'] || []);
|
|
162
|
+
|
|
163
|
+
if (messages.length === 0) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 2. 并行获取每封邮件的详情(含 text/html/attachments)
|
|
168
|
+
const detailPromises = messages.map(async (msg: any) => {
|
|
169
|
+
try {
|
|
170
|
+
const detailResponse = await fetch(`${BASE_URL}/messages/${msg.id}`, {
|
|
171
|
+
method: 'GET',
|
|
172
|
+
headers: {
|
|
173
|
+
...DEFAULT_HEADERS,
|
|
174
|
+
'Authorization': `Bearer ${token}`,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (!detailResponse.ok) {
|
|
179
|
+
// 如果详情获取失败,退回使用列表数据
|
|
180
|
+
return flattenMessage(msg, email);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const detail = await detailResponse.json();
|
|
184
|
+
return flattenMessage(detail, email);
|
|
185
|
+
} catch {
|
|
186
|
+
// 出错时退回使用列表数据
|
|
187
|
+
return flattenMessage(msg, email);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const flatMessages = await Promise.all(detailPromises);
|
|
192
|
+
return flatMessages.map((raw: any) => normalizeEmail(raw, email));
|
|
193
|
+
}
|