tempmail-sdk 1.1.3 → 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.
@@ -5,11 +5,39 @@ import { fetchWithTimeout } from '../retry';
5
5
  const CHANNEL: Channel = 'mail-tm';
6
6
  const BASE_URL = 'https://api.mail.tm';
7
7
 
8
+ /**
9
+ * 与 Internxt 临时邮箱页(https://internxt.com/temporary-email)前端一致:
10
+ * 同源 cross-site 请求 api.mail.tm 时常带 Origin/Referer;domains 可为空 Bearer。
11
+ */
8
12
  const DEFAULT_HEADERS: Record<string, string> = {
9
- 'Content-Type': 'application/json',
10
13
  'Accept': 'application/json',
14
+ 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
15
+ 'Cache-Control': 'no-cache',
16
+ 'Pragma': 'no-cache',
17
+ 'Origin': 'https://internxt.com',
18
+ 'Referer': 'https://internxt.com/',
19
+ 'User-Agent':
20
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0',
11
21
  };
12
22
 
23
+ function jsonHeaders(extra?: Record<string, string>): Record<string, string> {
24
+ return { ...DEFAULT_HEADERS, 'Content-Type': 'application/json', ...extra };
25
+ }
26
+
27
+ function bearerHeaders(token: string, extra?: Record<string, string>): Record<string, string> {
28
+ return { ...jsonHeaders(extra), Authorization: `Bearer ${token}` };
29
+ }
30
+
31
+ function parseHydraMember(data: unknown): any[] {
32
+ if (Array.isArray(data)) return data;
33
+ if (data && typeof data === 'object') {
34
+ const o = data as Record<string, unknown>;
35
+ if (Array.isArray(o['hydra:member'])) return o['hydra:member'];
36
+ if (Array.isArray(o['data'])) return o['data'];
37
+ }
38
+ return [];
39
+ }
40
+
13
41
  /**
14
42
  * 生成随机字符串
15
43
  */
@@ -27,7 +55,7 @@ function randomString(length: number): string {
27
55
  * API: GET /domains
28
56
  */
29
57
  async function getDomains(): Promise<string[]> {
30
- const response = await fetchWithTimeout(`${BASE_URL}/domains`, {
58
+ const response = await fetchWithTimeout(`${BASE_URL}/domains?page=1`, {
31
59
  method: 'GET',
32
60
  headers: DEFAULT_HEADERS,
33
61
  });
@@ -37,14 +65,11 @@ async function getDomains(): Promise<string[]> {
37
65
  }
38
66
 
39
67
  const data = await response.json();
40
- /* 兼容两种响应格式:
41
- * - Accept: application/ld+json → Hydra 格式 { "hydra:member": [...] }
42
- * - Accept: application/json → 纯数组 [...]
43
- */
44
- const members = Array.isArray(data) ? data : (data['hydra:member'] || []);
68
+ const members = parseHydraMember(data);
45
69
  return members
46
- .filter((d: any) => d.isActive)
47
- .map((d: any) => d.domain);
70
+ .filter((d: any) => d && d.isActive !== false)
71
+ .map((d: any) => d.domain)
72
+ .filter(Boolean);
48
73
  }
49
74
 
50
75
  /**
@@ -73,7 +98,7 @@ async function createAccount(address: string, password: string): Promise<any> {
73
98
  async function getToken(address: string, password: string): Promise<string> {
74
99
  const response = await fetchWithTimeout(`${BASE_URL}/token`, {
75
100
  method: 'POST',
76
- headers: DEFAULT_HEADERS,
101
+ headers: jsonHeaders(),
77
102
  body: JSON.stringify({ address, password }),
78
103
  });
79
104
 
@@ -145,12 +170,9 @@ function flattenMessage(msg: any, recipientEmail: string): any {
145
170
  */
146
171
  export async function getEmails(token: string, email: string): Promise<Email[]> {
147
172
  // 1. 获取邮件列表
148
- const listResponse = await fetchWithTimeout(`${BASE_URL}/messages`, {
173
+ const listResponse = await fetchWithTimeout(`${BASE_URL}/messages?page=1`, {
149
174
  method: 'GET',
150
- headers: {
151
- ...DEFAULT_HEADERS,
152
- 'Authorization': `Bearer ${token}`,
153
- },
175
+ headers: bearerHeaders(token),
154
176
  });
155
177
 
156
178
  if (!listResponse.ok) {
@@ -158,8 +180,7 @@ export async function getEmails(token: string, email: string): Promise<Email[]>
158
180
  }
159
181
 
160
182
  const listData = await listResponse.json();
161
- /* 兼容 Hydra 格式和纯数组格式 */
162
- const messages = Array.isArray(listData) ? listData : (listData['hydra:member'] || []);
183
+ const messages = parseHydraMember(listData);
163
184
 
164
185
  if (messages.length === 0) {
165
186
  return [];
@@ -170,10 +191,7 @@ export async function getEmails(token: string, email: string): Promise<Email[]>
170
191
  try {
171
192
  const detailResponse = await fetchWithTimeout(`${BASE_URL}/messages/${msg.id}`, {
172
193
  method: 'GET',
173
- headers: {
174
- ...DEFAULT_HEADERS,
175
- 'Authorization': `Bearer ${token}`,
176
- },
194
+ headers: bearerHeaders(token),
177
195
  });
178
196
 
179
197
  if (!detailResponse.ok) {
@@ -0,0 +1,382 @@
1
+ import { InternalEmailInfo, Email, Channel } from '../types';
2
+ import { normalizeEmail } from '../normalize';
3
+ import { fetchWithTimeout } from '../retry';
4
+
5
+ const CHANNEL: Channel = 'smail-pw';
6
+ const ROOT_DATA_URL = 'https://smail.pw/_root.data';
7
+
8
+ const DEFAULT_HEADERS: Record<string, string> = {
9
+ Accept: '*/*',
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://smail.pw',
14
+ pragma: 'no-cache',
15
+ referer: 'https://smail.pw/',
16
+ 'sec-ch-ua':
17
+ '"Chromium";v="146", "Not-A.Brand";v="24", "Microsoft Edge";v="146"',
18
+ 'sec-ch-ua-mobile': '?0',
19
+ 'sec-ch-ua-platform': '"Windows"',
20
+ 'sec-fetch-dest': 'empty',
21
+ 'sec-fetch-mode': 'cors',
22
+ 'sec-fetch-site': 'same-origin',
23
+ 'User-Agent':
24
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0',
25
+ };
26
+
27
+ /** Flight/RSC 根为数组时,按出现顺序展开嵌套数组,保留标量与对象(不展开对象键),用于解析交错键值 */
28
+ function flightLinearLeaves(node: unknown): unknown[] {
29
+ if (!Array.isArray(node)) {
30
+ return [];
31
+ }
32
+ const out: unknown[] = [];
33
+ for (const el of node) {
34
+ if (Array.isArray(el)) {
35
+ out.push(...flightLinearLeaves(el));
36
+ } else {
37
+ out.push(el);
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+
43
+ const MAIL_KV_KEYS = new Set(['id', 'to_address', 'from_name', 'from_address']);
44
+
45
+ /**
46
+ * 在线性序列中查找 ... "subject", <str>, "time", <num>,再向前成对读取已知字段。
47
+ * 比纯正则更耐受 emails 与 id 之间的 {"_23":4,...} 引用块。
48
+ */
49
+ function parseSmailEmailsFromLinear(
50
+ leaves: unknown[],
51
+ recipientEmail: string,
52
+ ): any[] {
53
+ const mails: any[] = [];
54
+ for (let i = 0; i + 3 < leaves.length; i++) {
55
+ if (leaves[i] !== 'subject') {
56
+ continue;
57
+ }
58
+ const subj = leaves[i + 1];
59
+ if (typeof subj !== 'string') {
60
+ continue;
61
+ }
62
+ if (leaves[i + 2] !== 'time') {
63
+ continue;
64
+ }
65
+ const timeRaw = leaves[i + 3];
66
+ const timeMs =
67
+ typeof timeRaw === 'number'
68
+ ? timeRaw
69
+ : typeof timeRaw === 'string' && /^\d+$/.test(timeRaw)
70
+ ? parseInt(timeRaw, 10)
71
+ : NaN;
72
+ if (!Number.isFinite(timeMs)) {
73
+ continue;
74
+ }
75
+
76
+ const fields: Record<string, string> = {};
77
+ for (let k = i - 2; k >= 1; k -= 2) {
78
+ const key = leaves[k];
79
+ const val = leaves[k + 1];
80
+ if (typeof key !== 'string' || typeof val !== 'string') {
81
+ break;
82
+ }
83
+ if (MAIL_KV_KEYS.has(key)) {
84
+ fields[key] = val;
85
+ }
86
+ /* 未知键值对跳过(RSC 可能在 subject 前插入其它字符串字段) */
87
+ }
88
+
89
+ mails.push({
90
+ id: fields.id || '',
91
+ to_address: fields.to_address || recipientEmail,
92
+ from_name: fields.from_name || '',
93
+ from_address: fields.from_address || '',
94
+ subject: subj,
95
+ date: timeMs,
96
+ text: '',
97
+ html: '',
98
+ attachments: [],
99
+ });
100
+ }
101
+ return mails;
102
+ }
103
+
104
+ function extractSessionCookie(response: Response): string {
105
+ const h = response.headers as Headers & { getSetCookie?: () => string[] };
106
+ if (typeof h.getSetCookie === 'function') {
107
+ for (const line of h.getSetCookie()) {
108
+ const m = line.match(/^__session=([^;]+)/);
109
+ if (m) return `__session=${m[1]}`;
110
+ }
111
+ }
112
+ const single = response.headers.get('set-cookie');
113
+ if (single) {
114
+ const m = single.match(/__session=([^;]+)/);
115
+ if (m) return `__session=${m[1]}`;
116
+ }
117
+ return '';
118
+ }
119
+
120
+ /** RSC/Flight 风格 JSON 文本中提取 @smail.pw 收件地址 */
121
+ function extractInboxEmail(text: string): string | null {
122
+ const quoted = text.match(/"([a-z0-9][a-z0-9.-]*@smail\.pw)"/i);
123
+ if (quoted) return quoted[1];
124
+ const plain = text.match(/\b([a-z0-9][a-z0-9.-]*@smail\.pw)\b/i);
125
+ return plain ? plain[1] : null;
126
+ }
127
+
128
+ /** 从响应体解析邮件条目(正则路径,与早期 curl 样本一致) */
129
+ function parseSmailEmailsRegex(text: string, recipientEmail: string): any[] {
130
+ const out: any[] = [];
131
+ const re =
132
+ /"id","([^"]+)","to_address","([^"]*)","from_name","([^"]*)","from_address","([^"]*)","subject","([^"]*)","time",(\d+)/g;
133
+ let m: RegExpExecArray | null;
134
+ while ((m = re.exec(text)) !== null) {
135
+ out.push({
136
+ id: m[1],
137
+ to_address: m[2] || recipientEmail,
138
+ from_name: m[3],
139
+ from_address: m[4],
140
+ subject: m[5],
141
+ date: parseInt(m[6], 10),
142
+ text: '',
143
+ html: '',
144
+ attachments: [],
145
+ });
146
+ }
147
+ if (out.length > 0) {
148
+ return out;
149
+ }
150
+
151
+ const re2 =
152
+ /"id","([^"]+)","from_name","([^"]*)","from_address","([^"]*)","subject","([^"]*)","time",(\d+)/g;
153
+ while ((m = re2.exec(text)) !== null) {
154
+ out.push({
155
+ id: m[1],
156
+ to_address: recipientEmail,
157
+ from_name: m[2],
158
+ from_address: m[3],
159
+ subject: m[4],
160
+ date: parseInt(m[5], 10),
161
+ text: '',
162
+ html: '',
163
+ attachments: [],
164
+ });
165
+ }
166
+ return out;
167
+ }
168
+
169
+ function mergeMailsById(lists: any[][]): any[] {
170
+ const map = new Map<string, any>();
171
+ let anon = 0;
172
+ for (const list of lists) {
173
+ for (const mail of list) {
174
+ let id = String(mail?.id || '');
175
+ if (!id) {
176
+ id = `__smail_${anon++}_${mail?.date ?? ''}_${String(mail?.subject ?? '').slice(0, 48)}`;
177
+ mail.id = id;
178
+ }
179
+ if (!map.has(id)) {
180
+ map.set(id, mail);
181
+ }
182
+ }
183
+ }
184
+ return [...map.values()];
185
+ }
186
+
187
+ /**
188
+ * 官方 loader 返回 D1 行:{ id, to_address, from_name, from_address, subject, time }(见 akazwz/smail app/types/email.ts)。
189
+ * Flight 序列化后多为普通 JSON 对象,而非 "id","…" 交错字符串元组,须在整棵树递归收集。
190
+ */
191
+ function collectPlainRowEmails(root: unknown, recipientEmail: string): any[] {
192
+ const mails: any[] = [];
193
+ const seen = new WeakSet<object>();
194
+
195
+ function walk(node: unknown): void {
196
+ if (node === null || node === undefined) {
197
+ return;
198
+ }
199
+ if (typeof node !== 'object') {
200
+ return;
201
+ }
202
+ if (seen.has(node as object)) {
203
+ return;
204
+ }
205
+ seen.add(node as object);
206
+
207
+ if (Array.isArray(node)) {
208
+ for (const el of node) {
209
+ walk(el);
210
+ }
211
+ return;
212
+ }
213
+
214
+ const o = node as Record<string, unknown>;
215
+ if (typeof o.subject === 'string') {
216
+ const tr = o.time;
217
+ const timeMs =
218
+ typeof tr === 'number' && Number.isFinite(tr)
219
+ ? tr
220
+ : typeof tr === 'string' && /^\d+$/.test(tr)
221
+ ? parseInt(tr, 10)
222
+ : NaN;
223
+ if (Number.isFinite(timeMs)) {
224
+ mails.push({
225
+ id: String(o.id ?? ''),
226
+ to_address: String(o.to_address ?? recipientEmail),
227
+ from_name: String(o.from_name ?? ''),
228
+ from_address: String(o.from_address ?? ''),
229
+ subject: o.subject,
230
+ date: timeMs,
231
+ text: '',
232
+ html: '',
233
+ attachments: [],
234
+ });
235
+ }
236
+ }
237
+
238
+ for (const v of Object.values(o)) {
239
+ if (v !== null && typeof v === 'object') {
240
+ walk(v);
241
+ }
242
+ }
243
+ }
244
+
245
+ walk(root);
246
+ return mails;
247
+ }
248
+
249
+ /**
250
+ * Flight 风格 payload:根为数组,"addresses",[i] 表示 root[i] 为邮箱字符串;
251
+ * "emails",[a,b,...] 中每项为下标,指向 root[idx] 的邮件行(常为嵌套数组,内含 id/to_address/subject/time 键值序列)。
252
+ */
253
+ function resolveFlightSlot(root: unknown[], idx: number, visited: Set<number>): unknown {
254
+ if (idx < 0 || idx >= root.length || visited.has(idx)) {
255
+ return undefined;
256
+ }
257
+ visited.add(idx);
258
+ const val = root[idx];
259
+ if (typeof val === 'number' && Number.isInteger(val) && val >= 0 && val < root.length) {
260
+ return resolveFlightSlot(root, val, visited);
261
+ }
262
+ return val;
263
+ }
264
+
265
+ function flattenFlightIndices(refs: unknown[]): number[] {
266
+ const out: number[] = [];
267
+ for (const r of refs) {
268
+ if (typeof r === 'number' && Number.isInteger(r)) {
269
+ out.push(r);
270
+ } else if (typeof r === 'string' && /^\d+$/.test(r)) {
271
+ out.push(parseInt(r, 10));
272
+ } else if (Array.isArray(r)) {
273
+ for (const x of r) {
274
+ if (typeof x === 'number' && Number.isInteger(x)) {
275
+ out.push(x);
276
+ } else if (typeof x === 'string' && /^\d+$/.test(x)) {
277
+ out.push(parseInt(x, 10));
278
+ }
279
+ }
280
+ }
281
+ }
282
+ return out;
283
+ }
284
+
285
+ function parseSmailEmailsFromFlightRoot(root: unknown[], recipientEmail: string): any[] {
286
+ const mails: any[] = [];
287
+ for (let i = 0; i < root.length - 1; i++) {
288
+ if (root[i] !== 'emails') {
289
+ continue;
290
+ }
291
+ const refs = root[i + 1];
292
+ if (!Array.isArray(refs)) {
293
+ break;
294
+ }
295
+ for (const r of flattenFlightIndices(refs)) {
296
+ const node = resolveFlightSlot(root, r, new Set());
297
+ if (Array.isArray(node)) {
298
+ const leaves = flightLinearLeaves(node);
299
+ mails.push(...parseSmailEmailsFromLinear(leaves, recipientEmail));
300
+ } else if (node !== null && typeof node === 'object') {
301
+ mails.push(...collectPlainRowEmails(node, recipientEmail));
302
+ }
303
+ }
304
+ break;
305
+ }
306
+ return mails;
307
+ }
308
+
309
+ function parseSmailEmailsFromPayload(text: string, recipientEmail: string): any[] {
310
+ const regexMails = parseSmailEmailsRegex(text, recipientEmail);
311
+ let linearMails: any[] = [];
312
+ let flightMails: any[] = [];
313
+ let plainMails: any[] = [];
314
+ try {
315
+ const root = JSON.parse(text) as unknown;
316
+ plainMails = collectPlainRowEmails(root, recipientEmail);
317
+ if (Array.isArray(root)) {
318
+ flightMails = parseSmailEmailsFromFlightRoot(root, recipientEmail);
319
+ const leaves = flightLinearLeaves(root);
320
+ linearMails = parseSmailEmailsFromLinear(leaves, recipientEmail);
321
+ }
322
+ } catch {
323
+ /* 非 JSON 或结构异常时仅用正则 */
324
+ }
325
+ return mergeMailsById([regexMails, linearMails, flightMails, plainMails]);
326
+ }
327
+
328
+ /**
329
+ * POST intent=generate,保存 Set-Cookie __session,从 RSC 响应中解析邮箱
330
+ */
331
+ export async function generateEmail(): Promise<InternalEmailInfo> {
332
+ const response = await fetchWithTimeout(ROOT_DATA_URL, {
333
+ method: 'POST',
334
+ headers: {
335
+ ...DEFAULT_HEADERS,
336
+ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
337
+ },
338
+ body: 'intent=generate',
339
+ });
340
+
341
+ if (!response.ok) {
342
+ throw new Error(`smail.pw generate failed: ${response.status}`);
343
+ }
344
+
345
+ const cookie = extractSessionCookie(response);
346
+ if (!cookie) {
347
+ throw new Error('Failed to extract __session cookie');
348
+ }
349
+
350
+ const text = await response.text();
351
+ const email = extractInboxEmail(text);
352
+ if (!email) {
353
+ throw new Error('Failed to parse inbox address from smail.pw response');
354
+ }
355
+
356
+ return {
357
+ channel: CHANNEL,
358
+ email,
359
+ token: cookie,
360
+ };
361
+ }
362
+
363
+ /**
364
+ * GET _root.data,携带 __session Cookie,解析邮件列表
365
+ */
366
+ export async function getEmails(token: string, email: string): Promise<Email[]> {
367
+ const response = await fetchWithTimeout(ROOT_DATA_URL, {
368
+ method: 'GET',
369
+ headers: {
370
+ ...DEFAULT_HEADERS,
371
+ Cookie: token,
372
+ },
373
+ });
374
+
375
+ if (!response.ok) {
376
+ throw new Error(`smail.pw get emails failed: ${response.status}`);
377
+ }
378
+
379
+ const text = await response.text();
380
+ const rawList = parseSmailEmailsFromPayload(text, email);
381
+ return rawList.map((raw) => normalizeEmail(raw, email));
382
+ }
package/src/types.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * 支持的临时邮箱渠道标识
3
3
  * 每个渠道对应一个第三方临时邮箱服务商
4
4
  */
5
- export type Channel = 'tempmail' | 'linshi-email' | 'tempmail-lol' | 'chatgpt-org-uk' | 'tempmail-la' | 'temp-mail-io' | 'awamail' | 'mail-tm' | 'dropmail' | 'guerrillamail' | 'maildrop';
5
+ export type Channel = 'tempmail' | 'linshi-email' | 'tempmail-lol' | 'chatgpt-org-uk' | 'temp-mail-io' | 'awamail' | 'mail-tm' | 'dropmail' | 'guerrillamail' | 'maildrop' | 'smail-pw';
6
6
 
7
7
  /**
8
8
  * 创建临时邮箱后返回的邮箱信息
@@ -123,6 +123,11 @@ export interface RetryConfig {
123
123
  export interface GenerateEmailOptions {
124
124
  /** 指定渠道,不传则随机选择 */
125
125
  channel?: Channel;
126
+ /**
127
+ * 为 false 时仅尝试 `channel` 指定的渠道,失败即返回 null,不 Fallback 到其他渠道。
128
+ * 用于按渠道探测可用性。默认 true(保持原有「优先指定渠道、失败后试其他」行为)。
129
+ */
130
+ channelFallback?: boolean;
126
131
  /** 邮箱有效时长 */
127
132
  duration?: number;
128
133
  /** 指定邮箱域名 */
package/test/example.ts CHANGED
@@ -1,11 +1,177 @@
1
1
  import { generateEmail, getEmails, TempEmailClient, Channel } from '../src';
2
+ import nodemailer from 'nodemailer';
3
+
4
+ type SmtpConfig = {
5
+ host: string;
6
+ port: number;
7
+ user?: string;
8
+ pass?: string;
9
+ from: string;
10
+ secure: boolean;
11
+ timeoutMs: number;
12
+ subject: string;
13
+ body: string;
14
+ };
15
+
16
+ type PollConfig = {
17
+ intervalMs: number;
18
+ max: number;
19
+ };
20
+
21
+ function parseBoolean(value?: string): boolean {
22
+ if (!value) return false;
23
+ return value === '1' || value.toLowerCase() === 'true';
24
+ }
25
+
26
+ function parsePositiveNumber(value: string | undefined, fallback: number): number {
27
+ if (!value) return fallback;
28
+ const parsed = Number(value);
29
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
30
+ return parsed;
31
+ }
32
+
33
+ function getSmtpConfig(): SmtpConfig | null {
34
+ const enabled = parseBoolean(process.env.TEMPMAIL_SMTP_ENABLED);
35
+ if (!enabled) return null;
36
+
37
+ const host = process.env.TEMPMAIL_SMTP_HOST?.trim();
38
+ const portRaw = process.env.TEMPMAIL_SMTP_PORT?.trim();
39
+ const port = portRaw ? Number(portRaw) : 0;
40
+
41
+ if (!host || !port || !Number.isFinite(port)) {
42
+ console.warn('SMTP 已启用,但 TEMPMAIL_SMTP_HOST/TEMPMAIL_SMTP_PORT 未正确配置,跳过发送。');
43
+ return null;
44
+ }
45
+
46
+ const user = process.env.TEMPMAIL_SMTP_USER?.trim();
47
+ const pass = process.env.TEMPMAIL_SMTP_PASS?.trim();
48
+ const from = (process.env.TEMPMAIL_SMTP_FROM?.trim() || user || '').trim();
49
+
50
+ if (!from) {
51
+ console.warn('SMTP 已启用,但 TEMPMAIL_SMTP_FROM 未配置且 USER 为空,跳过发送。');
52
+ return null;
53
+ }
54
+
55
+ const secureEnv = process.env.TEMPMAIL_SMTP_SECURE?.trim();
56
+ const secure = secureEnv ? parseBoolean(secureEnv) : port === 465;
57
+
58
+ const timeoutMs = parsePositiveNumber(process.env.TEMPMAIL_SMTP_TIMEOUT, 10000);
59
+ const subject = process.env.TEMPMAIL_SMTP_SUBJECT?.trim() || 'TempMail SDK SMTP Test';
60
+ const body = process.env.TEMPMAIL_SMTP_BODY?.trim() || 'This is a tempmail SDK SMTP test email.';
61
+
62
+ return {
63
+ host,
64
+ port,
65
+ user,
66
+ pass,
67
+ from,
68
+ secure,
69
+ timeoutMs,
70
+ subject,
71
+ body,
72
+ };
73
+ }
74
+
75
+ function getPollConfig(): PollConfig {
76
+ return {
77
+ intervalMs: parsePositiveNumber(process.env.TEMPMAIL_POLL_INTERVAL_MS, 3000),
78
+ max: parsePositiveNumber(process.env.TEMPMAIL_POLL_MAX, 10),
79
+ };
80
+ }
81
+
82
+ function createTraceId(): string {
83
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
84
+ }
85
+
86
+ function sleep(ms: number) {
87
+ return new Promise<void>((resolve) => setTimeout(resolve, ms));
88
+ }
89
+
90
+ async function sendSmtpTestEmail(targetEmail: string): Promise<{ marker: string } | null> {
91
+ const smtp = getSmtpConfig();
92
+ if (!smtp) return null;
93
+
94
+ const marker = `tempmail-sdk:${createTraceId()}`;
95
+ const subject = `${smtp.subject} [${marker}]`;
96
+ const text = `${smtp.body}\n\n[${marker}]`;
97
+ const html = `${smtp.body}<br/><br/><strong>[${marker}]</strong>`;
98
+
99
+ const transport = nodemailer.createTransport({
100
+ host: smtp.host,
101
+ port: smtp.port,
102
+ secure: smtp.secure,
103
+ auth: smtp.user && smtp.pass ? { user: smtp.user, pass: smtp.pass } : undefined,
104
+ connectionTimeout: smtp.timeoutMs,
105
+ });
106
+
107
+ try {
108
+ await transport.sendMail({
109
+ from: smtp.from,
110
+ to: targetEmail,
111
+ subject,
112
+ text,
113
+ html,
114
+ });
115
+ console.log(`SMTP 测试邮件已发送到 ${targetEmail}`);
116
+ return { marker };
117
+ } catch (error) {
118
+ console.warn(`SMTP 发送失败:${error}`);
119
+ return null;
120
+ }
121
+ }
122
+
123
+ function emailHasMarker(content: string | undefined, marker: string): boolean {
124
+ return !!content && content.includes(marker);
125
+ }
126
+
127
+ async function pollForEmail(client: TempEmailClient, marker: string): Promise<boolean> {
128
+ const { intervalMs, max } = getPollConfig();
129
+
130
+ for (let attempt = 1; attempt <= max; attempt++) {
131
+ try {
132
+ const result = await client.getEmails();
133
+ const found = result.emails.some((email) => {
134
+ return (
135
+ emailHasMarker(email.subject, marker) ||
136
+ emailHasMarker(email.text, marker) ||
137
+ emailHasMarker(email.html, marker)
138
+ );
139
+ });
140
+ if (found) {
141
+ console.log(`轮询成功:已收到包含标识 ${marker} 的邮件。`);
142
+ return true;
143
+ }
144
+ } catch (error) {
145
+ console.warn(`轮询第 ${attempt} 次失败:${error}`);
146
+ }
147
+
148
+ if (attempt < max) {
149
+ await sleep(intervalMs);
150
+ }
151
+ }
152
+
153
+ console.warn(`轮询超时:未收到包含标识 ${marker} 的邮件。`);
154
+ return false;
155
+ }
2
156
 
3
157
  async function testGenerateEmail() {
4
158
  console.log('=== Test Generate Email ===\n');
5
159
 
6
- // Test each channel
7
- const channels: Channel[] = ['tempmail', 'linshi-email', 'tempmail-lol', 'chatgpt-org-uk', 'tempmail-la', 'temp-mail-io', 'awamail', 'mail-tm', 'dropmail'];
8
-
160
+ // Test each channel(与 src/index.ts allChannels 一致)
161
+ const channels: Channel[] = [
162
+ 'tempmail',
163
+ 'linshi-email',
164
+ 'tempmail-lol',
165
+ 'chatgpt-org-uk',
166
+ 'temp-mail-io',
167
+ 'awamail',
168
+ 'mail-tm',
169
+ 'dropmail',
170
+ 'guerrillamail',
171
+ 'maildrop',
172
+ 'smail-pw',
173
+ ];
174
+
9
175
  for (const channel of channels) {
10
176
  try {
11
177
  console.log(`Testing channel: ${channel}`);
@@ -58,6 +224,11 @@ async function testGetEmails() {
58
224
  console.log();
59
225
  }
60
226
  console.log();
227
+
228
+ const smtpResult = await sendSmtpTestEmail(emailInfo.email);
229
+ if (smtpResult) {
230
+ await pollForEmail(client, smtpResult.marker);
231
+ }
61
232
  } catch (error) {
62
233
  console.error(`Error: ${error}`);
63
234
  }