react-native-email-imap-smtp 0.3.26 → 0.3.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +225 -225
  2. package/src/web.ts +544 -544
package/src/web.ts CHANGED
@@ -1,544 +1,544 @@
1
- // ============================================================
2
- // Web implementation — HTTP Proxy Client
3
- // 浏览器通过 HTTP API 与代理服务器通信,代理使用 imapflow + nodemailer
4
- // ============================================================
5
- //
6
- // 需要启动代理服务器:
7
- // npx email-imap-proxy # 启动代理
8
- // npx email-imap-proxy & vite # 与 Vite 并行启动
9
- //
10
- // 或在 package.json 的 dev 脚本中加入:
11
- // "dev": "email-imap-proxy & next dev"
12
- //
13
- // 自定义代理地址:
14
- // import { setProxyUrl } from 'react-native-email-imap-smtp';
15
- // setProxyUrl('http://localhost:3987');
16
- // ============================================================
17
-
18
- import type {
19
- IMAPConfig,
20
- SMTPConfig,
21
- IMAPConnectionResult,
22
- SMTPConnectionResult,
23
- Mailbox,
24
- MailboxStatus,
25
- Folder,
26
- Email,
27
- Attachment,
28
- FetchEmailsOptions,
29
- SearchCriteria,
30
- BatchResult,
31
- IMAPNotification,
32
- QuotaInfo,
33
- AppendResult,
34
- SendMailOptions,
35
- SendMailResult,
36
- } from './types';
37
-
38
- // ─── 代理服务器配置 ─────────────────────────────────────────
39
-
40
- const DEFAULT_PROXY_URL = 'http://localhost:3987';
41
- // 60s:附件下载可能较慢/含多个 part,15s 容易误超时
42
- const API_TIMEOUT = 60000;
43
-
44
- let PROXY_URL = DEFAULT_PROXY_URL;
45
- let _ws: WebSocket | null = null;
46
-
47
- export function setProxyUrl(url: string): void {
48
- PROXY_URL = url.replace(/\/+$/, '');
49
- }
50
-
51
- export function getProxyUrl(): string {
52
- return PROXY_URL;
53
- }
54
-
55
- // ─── 内部辅助 ───────────────────────────────────────────────
56
-
57
- export class ImapNotConnectedError extends Error {
58
- constructor() {
59
- super('IMAP not connected. Call imapConnect first.');
60
- this.name = 'ImapNotConnectedError';
61
- }
62
- }
63
-
64
- export class SmtpNotConnectedError extends Error {
65
- constructor() {
66
- super('SMTP not connected. Call smtpConnect first.');
67
- this.name = 'SmtpNotConnectedError';
68
- }
69
- }
70
-
71
- interface ConnectResult {
72
- success: boolean;
73
- sessionId?: string;
74
- }
75
-
76
- let _imapSessionId: string | null = null;
77
- let _smtpSessionId: string | null = null;
78
- let _proxyChecked = false;
79
-
80
- /** 首次调用时检测代理是否可达,不可达则抛明确指引 */
81
- async function checkProxyOrThrow(): Promise<void> {
82
- if (_proxyChecked) return;
83
- _proxyChecked = true;
84
-
85
- try {
86
- const res = await fetch(`${PROXY_URL}/api/health`, {
87
- signal: new AbortController().signal as any,
88
- });
89
- if (res.ok) return;
90
- } catch {
91
- // 代理不可达
92
- }
93
-
94
- _proxyChecked = false; // 允许后续重试
95
- throw new Error(
96
- `[EmailImapSmtp] 代理服务器未启动!
97
-
98
- IMAP/SMTP 在浏览器中需要借助一个本地代理服务来连接邮件服务器。
99
-
100
- 请在项目配置中启动代理:
101
-
102
- ${'A. Vite 项目 — 在 vite.config.ts 中添加:'}
103
- import { startEmailProxy } from 'react-native-email-imap-smtp/server/proxy';
104
- startEmailProxy();
105
-
106
- ${'B. Next.js / Webpack 等 — 在 dev 脚本中添加:'}
107
- "dev": "email-imap-proxy & next dev"
108
-
109
- ${'C. 或手动启动(会自动安装依赖):'}
110
- npx email-imap-proxy
111
-
112
- ${'D. 如果已启动但端口不同,请设置:'}
113
- import { setProxyUrl } from 'react-native-email-imap-smtp';
114
- setProxyUrl('http://localhost:你的端口号');
115
- `
116
- );
117
- }
118
-
119
- function getImapSessionId(): string {
120
- if (!_imapSessionId) throw new ImapNotConnectedError();
121
- return _imapSessionId;
122
- }
123
-
124
- function getSmtpSessionId(): string {
125
- if (!_smtpSessionId) throw new SmtpNotConnectedError();
126
- return _smtpSessionId;
127
- }
128
-
129
- function extractSession<T extends ConnectResult>(result: T): T {
130
- if (result.success && result.sessionId) {
131
- _imapSessionId = result.sessionId;
132
- }
133
- return result;
134
- }
135
-
136
- function extractSmtpSession<T extends ConnectResult>(result: T): T {
137
- if (result.success && result.sessionId) {
138
- _smtpSessionId = result.sessionId;
139
- }
140
- return result;
141
- }
142
-
143
- /** ArrayBuffer → base64 */
144
- function arrayBufferToBase64(buf: ArrayBuffer): string {
145
- const bytes = new Uint8Array(buf);
146
- let binary = '';
147
- for (let i = 0; i < bytes.length; i++) {
148
- binary += String.fromCharCode(bytes[i]!);
149
- }
150
- return btoa(binary);
151
- }
152
-
153
- /** base64 → ArrayBuffer(服务器经 JSON 传输附件时为 base64 字符串,需解码回二进制) */
154
- function base64ToArrayBuffer(base64: string): ArrayBuffer {
155
- const binary = atob(base64);
156
- const bytes = new Uint8Array(binary.length);
157
- for (let i = 0; i < binary.length; i++) {
158
- bytes[i] = binary.charCodeAt(i);
159
- }
160
- return bytes.buffer;
161
- }
162
-
163
- /** 归一化服务器返回的附件:若 data 是 base64 字符串则解码为 ArrayBuffer */
164
- function normalizeAttachment(att: Attachment): Attachment {
165
- const data = att.data as unknown;
166
- if (typeof data === 'string') {
167
- return { ...att, data: base64ToArrayBuffer(data) };
168
- }
169
- return att;
170
- }
171
-
172
- async function apiPost<T>(
173
- path: string,
174
- body: any,
175
- errorMessage?: string
176
- ): Promise<T> {
177
- await checkProxyOrThrow();
178
-
179
- const controller = new AbortController();
180
- const timer = setTimeout(() => controller.abort(), API_TIMEOUT);
181
-
182
- try {
183
- const url = `${PROXY_URL}${path}`;
184
- const res = await fetch(url, {
185
- method: 'POST',
186
- headers: { 'Content-Type': 'application/json' },
187
- body: JSON.stringify(body),
188
- signal: controller.signal as any,
189
- });
190
-
191
- if (!res.ok) {
192
- const text = await res.text().catch(() => '');
193
- throw new Error(
194
- errorMessage ?? `HTTP ${res.status}: ${text || res.statusText}`
195
- );
196
- }
197
-
198
- return res.json() as Promise<T>;
199
- } finally {
200
- clearTimeout(timer);
201
- }
202
- }
203
-
204
- // ─── IMAP ────────────────────────────────────────────────────
205
-
206
- export async function imapConnect(
207
- config: IMAPConfig
208
- ): Promise<IMAPConnectionResult> {
209
- _imapSessionId = null;
210
- const result = await apiPost<IMAPConnectionResult & { sessionId?: string }>(
211
- '/api/imap/connect',
212
- config,
213
- 'IMAP connection failed'
214
- );
215
- return extractSession(result);
216
- }
217
-
218
- export async function imapDisconnect(): Promise<void> {
219
- const sid = _imapSessionId;
220
- _imapSessionId = null;
221
- if (sid) {
222
- await apiPost('/api/imap/disconnect', { sessionId: sid }).catch(() => {});
223
- }
224
- }
225
-
226
- export async function imapListMailboxes(): Promise<Mailbox[]> {
227
- return apiPost<Mailbox[]>('/api/imap/listMailboxes', {
228
- sessionId: getImapSessionId(),
229
- });
230
- }
231
-
232
- export async function imapSelectMailbox(
233
- mailbox: string,
234
- readOnly?: boolean
235
- ): Promise<MailboxStatus> {
236
- return apiPost<MailboxStatus>('/api/imap/selectMailbox', {
237
- sessionId: getImapSessionId(),
238
- mailbox,
239
- readOnly: readOnly ?? false,
240
- });
241
- }
242
-
243
- export async function imapRefreshMailbox(): Promise<MailboxStatus> {
244
- return apiPost<MailboxStatus>('/api/imap/refreshMailbox', {
245
- sessionId: getImapSessionId(),
246
- });
247
- }
248
-
249
- export async function imapFetchFolders(): Promise<Folder[]> {
250
- return apiPost<Folder[]>('/api/imap/fetchFolders', {
251
- sessionId: getImapSessionId(),
252
- });
253
- }
254
-
255
- export async function imapCreateFolder(folderName: string): Promise<void> {
256
- await apiPost('/api/imap/createFolder', {
257
- sessionId: getImapSessionId(),
258
- folderName,
259
- });
260
- }
261
-
262
- export async function imapDeleteFolder(folderName: string): Promise<void> {
263
- await apiPost('/api/imap/deleteFolder', {
264
- sessionId: getImapSessionId(),
265
- folderName,
266
- });
267
- }
268
-
269
- export async function imapRenameFolder(
270
- oldName: string,
271
- newName: string
272
- ): Promise<void> {
273
- await apiPost('/api/imap/renameFolder', {
274
- sessionId: getImapSessionId(),
275
- oldName,
276
- newName,
277
- });
278
- }
279
-
280
- export async function imapSubscribeFolder(folderName: string): Promise<void> {
281
- await apiPost('/api/imap/subscribeFolder', {
282
- sessionId: getImapSessionId(),
283
- folderName,
284
- });
285
- }
286
-
287
- export async function imapUnsubscribeFolder(folderName: string): Promise<void> {
288
- await apiPost('/api/imap/unsubscribeFolder', {
289
- sessionId: getImapSessionId(),
290
- folderName,
291
- });
292
- }
293
-
294
- export async function imapFetchEmails(
295
- options: FetchEmailsOptions
296
- ): Promise<Email[]> {
297
- return apiPost<Email[]>('/api/imap/fetchEmails', {
298
- sessionId: getImapSessionId(),
299
- options,
300
- });
301
- }
302
-
303
- export async function imapFetchEmailByUID(uid: number): Promise<Email | null> {
304
- return apiPost<Email | null>('/api/imap/fetchEmailByUID', {
305
- sessionId: getImapSessionId(),
306
- uid,
307
- });
308
- }
309
-
310
- export async function imapSearchEmails(
311
- criteria: SearchCriteria
312
- ): Promise<Email[]> {
313
- return apiPost<Email[]>('/api/imap/searchEmails', {
314
- sessionId: getImapSessionId(),
315
- criteria,
316
- });
317
- }
318
-
319
- export async function imapMarkAsRead(
320
- uids: number[],
321
- silent?: boolean
322
- ): Promise<void> {
323
- await apiPost('/api/imap/markAsRead', {
324
- sessionId: getImapSessionId(),
325
- uids,
326
- silent: silent ?? true,
327
- });
328
- }
329
-
330
- export async function imapMarkAsFlagged(
331
- uids: number[],
332
- flagged: boolean
333
- ): Promise<void> {
334
- await apiPost('/api/imap/markAsFlagged', {
335
- sessionId: getImapSessionId(),
336
- uids,
337
- flagged,
338
- });
339
- }
340
-
341
- export async function imapMoveEmails(
342
- uids: number[],
343
- destinationMailbox: string
344
- ): Promise<void> {
345
- await apiPost('/api/imap/moveEmails', {
346
- sessionId: getImapSessionId(),
347
- uids,
348
- destinationMailbox,
349
- });
350
- }
351
-
352
- export async function imapCopyEmails(
353
- uids: number[],
354
- destinationMailbox: string
355
- ): Promise<void> {
356
- await apiPost('/api/imap/copyEmails', {
357
- sessionId: getImapSessionId(),
358
- uids,
359
- destinationMailbox,
360
- });
361
- }
362
-
363
- export async function imapDeleteEmails(uids: number[]): Promise<BatchResult> {
364
- return apiPost<BatchResult>('/api/imap/deleteEmails', {
365
- sessionId: getImapSessionId(),
366
- uids,
367
- });
368
- }
369
-
370
- export async function imapExpunge(): Promise<void> {
371
- await apiPost('/api/imap/expunge', {
372
- sessionId: getImapSessionId(),
373
- });
374
- }
375
-
376
- export async function imapFetchAttachments(
377
- uid: number,
378
- partId: string
379
- ): Promise<Attachment | null> {
380
- const result = await apiPost<Attachment | null>('/api/imap/fetchAttachment', {
381
- sessionId: getImapSessionId(),
382
- uid,
383
- partId,
384
- });
385
- return result ? normalizeAttachment(result) : null;
386
- }
387
-
388
- export async function imapFetchAllAttachments(
389
- uid: number
390
- ): Promise<Attachment[]> {
391
- const result = await apiPost<Attachment[]>('/api/imap/fetchAllAttachments', {
392
- sessionId: getImapSessionId(),
393
- uid,
394
- });
395
- return result.map(normalizeAttachment);
396
- }
397
-
398
- export async function imapIdle(
399
- callback: (notification: IMAPNotification) => void
400
- ): Promise<void> {
401
- const sid = getImapSessionId();
402
-
403
- // 启动空闲监听
404
- await apiPost('/api/imap/idle', { sessionId: sid });
405
-
406
- // 建立或复用 WebSocket 连接
407
- if (!_ws || _ws.readyState !== WebSocket.OPEN) {
408
- // 关闭旧连接(如有)
409
- if (_ws) {
410
- _ws.onclose = null;
411
- _ws.onerror = null;
412
- _ws.close();
413
- }
414
-
415
- const wsUrl = PROXY_URL.replace(/^http/, 'ws');
416
- const ws = new WebSocket(
417
- `${wsUrl}/ws?sessionId=${sid}`
418
- ) as unknown as WebSocket;
419
-
420
- ws.onclose = () => {
421
- _ws = null;
422
- };
423
-
424
- ws.onerror = () => {
425
- _ws = null;
426
- };
427
-
428
- _ws = ws;
429
- }
430
-
431
- // 始终绑定当前回调,无论是否复用连接
432
- _ws.onmessage = (event) => {
433
- try {
434
- const data = JSON.parse(event.data as string);
435
- if (data.type === 'connected') return;
436
- callback(data as IMAPNotification);
437
- } catch {
438
- // ignore parse errors
439
- }
440
- };
441
- }
442
-
443
- export async function imapStopIdle(): Promise<void> {
444
- const sid = _imapSessionId;
445
- if (sid) {
446
- await apiPost('/api/imap/stopIdle', { sessionId: sid }).catch(() => {});
447
- }
448
- if (_ws) {
449
- _ws.close();
450
- _ws = null;
451
- }
452
- }
453
-
454
- export async function imapGetQuota(root?: string): Promise<QuotaInfo> {
455
- return apiPost<QuotaInfo>('/api/imap/getQuota', {
456
- sessionId: getImapSessionId(),
457
- root: root ?? null,
458
- });
459
- }
460
-
461
- export async function imapAppendMessage(
462
- mailbox: string,
463
- rawMimeData: ArrayBuffer,
464
- flags?: string[]
465
- ): Promise<AppendResult> {
466
- return apiPost<AppendResult>('/api/imap/appendMessage', {
467
- sessionId: getImapSessionId(),
468
- mailbox,
469
- rawMimeData: arrayBufferToBase64(rawMimeData),
470
- flags: flags ?? null,
471
- });
472
- }
473
-
474
- // ─── SMTP ────────────────────────────────────────────────────
475
-
476
- export async function smtpConnect(
477
- config: SMTPConfig
478
- ): Promise<SMTPConnectionResult> {
479
- _smtpSessionId = null;
480
- const result = await apiPost<SMTPConnectionResult & { sessionId?: string }>(
481
- '/api/smtp/connect',
482
- config,
483
- 'SMTP connection failed'
484
- );
485
- return extractSmtpSession(result);
486
- }
487
-
488
- export async function smtpDisconnect(): Promise<void> {
489
- const sid = _smtpSessionId;
490
- _smtpSessionId = null;
491
- if (sid) {
492
- await apiPost('/api/smtp/disconnect', { sessionId: sid }).catch(() => {});
493
- }
494
- }
495
-
496
- export async function smtpSendMail(
497
- options: SendMailOptions
498
- ): Promise<SendMailResult> {
499
- // 先处理附件中的 ArrayBuffer → base64,再序列化
500
- const processedOptions: any = {
501
- from: options.from,
502
- to: options.to,
503
- cc: options.cc,
504
- bcc: options.bcc,
505
- replyTo: options.replyTo,
506
- subject: options.subject,
507
- textBody: options.textBody,
508
- htmlBody: options.htmlBody,
509
- priority: options.priority,
510
- headers: options.headers,
511
- inReplyTo: options.inReplyTo,
512
- references: options.references,
513
- readReceiptTo: options.readReceiptTo,
514
- charset: options.charset,
515
- encoding: options.encoding,
516
- };
517
-
518
- if (options.attachments && options.attachments.length > 0) {
519
- processedOptions.attachments = options.attachments.map((att) => ({
520
- filename: att.filename,
521
- mimeType: att.mimeType,
522
- isInline: att.isInline,
523
- contentId: att.contentId,
524
- disposition: (att as any).disposition,
525
- data:
526
- att.data instanceof ArrayBuffer
527
- ? arrayBufferToBase64(att.data)
528
- : att.data,
529
- }));
530
- }
531
-
532
- return apiPost<SendMailResult>('/api/smtp/sendMail', {
533
- sessionId: getSmtpSessionId(),
534
- options: processedOptions,
535
- });
536
- }
537
-
538
- export async function smtpVerifyConnection(): Promise<boolean> {
539
- const result = await apiPost<{ success: boolean }>(
540
- '/api/smtp/verifyConnection',
541
- { sessionId: getSmtpSessionId() }
542
- );
543
- return result.success;
544
- }
1
+ // ============================================================
2
+ // Web implementation — HTTP Proxy Client
3
+ // 浏览器通过 HTTP API 与代理服务器通信,代理使用 imapflow + nodemailer
4
+ // ============================================================
5
+ //
6
+ // 需要启动代理服务器:
7
+ // npx email-imap-proxy # 启动代理
8
+ // npx email-imap-proxy & vite # 与 Vite 并行启动
9
+ //
10
+ // 或在 package.json 的 dev 脚本中加入:
11
+ // "dev": "email-imap-proxy & next dev"
12
+ //
13
+ // 自定义代理地址:
14
+ // import { setProxyUrl } from 'react-native-email-imap-smtp';
15
+ // setProxyUrl('http://localhost:3987');
16
+ // ============================================================
17
+
18
+ import type {
19
+ IMAPConfig,
20
+ SMTPConfig,
21
+ IMAPConnectionResult,
22
+ SMTPConnectionResult,
23
+ Mailbox,
24
+ MailboxStatus,
25
+ Folder,
26
+ Email,
27
+ Attachment,
28
+ FetchEmailsOptions,
29
+ SearchCriteria,
30
+ BatchResult,
31
+ IMAPNotification,
32
+ QuotaInfo,
33
+ AppendResult,
34
+ SendMailOptions,
35
+ SendMailResult,
36
+ } from './types';
37
+
38
+ // ─── 代理服务器配置 ─────────────────────────────────────────
39
+
40
+ const DEFAULT_PROXY_URL = 'http://localhost:3987';
41
+ // 60s:附件下载可能较慢/含多个 part,15s 容易误超时
42
+ const API_TIMEOUT = 60000;
43
+
44
+ let PROXY_URL = DEFAULT_PROXY_URL;
45
+ let _ws: WebSocket | null = null;
46
+
47
+ export function setProxyUrl(url: string): void {
48
+ PROXY_URL = url.replace(/\/+$/, '');
49
+ }
50
+
51
+ export function getProxyUrl(): string {
52
+ return PROXY_URL;
53
+ }
54
+
55
+ // ─── 内部辅助 ───────────────────────────────────────────────
56
+
57
+ export class ImapNotConnectedError extends Error {
58
+ constructor() {
59
+ super('IMAP not connected. Call imapConnect first.');
60
+ this.name = 'ImapNotConnectedError';
61
+ }
62
+ }
63
+
64
+ export class SmtpNotConnectedError extends Error {
65
+ constructor() {
66
+ super('SMTP not connected. Call smtpConnect first.');
67
+ this.name = 'SmtpNotConnectedError';
68
+ }
69
+ }
70
+
71
+ interface ConnectResult {
72
+ success: boolean;
73
+ sessionId?: string;
74
+ }
75
+
76
+ let _imapSessionId: string | null = null;
77
+ let _smtpSessionId: string | null = null;
78
+ let _proxyChecked = false;
79
+
80
+ /** 首次调用时检测代理是否可达,不可达则抛明确指引 */
81
+ async function checkProxyOrThrow(): Promise<void> {
82
+ if (_proxyChecked) return;
83
+ _proxyChecked = true;
84
+
85
+ try {
86
+ const res = await fetch(`${PROXY_URL}/api/health`, {
87
+ signal: new AbortController().signal as any,
88
+ });
89
+ if (res.ok) return;
90
+ } catch {
91
+ // 代理不可达
92
+ }
93
+
94
+ _proxyChecked = false; // 允许后续重试
95
+ throw new Error(
96
+ `[EmailImapSmtp] 代理服务器未启动!
97
+
98
+ IMAP/SMTP 在浏览器中需要借助一个本地代理服务来连接邮件服务器。
99
+
100
+ 请在项目配置中启动代理:
101
+
102
+ ${'A. Vite 项目 — 在 vite.config.ts 中添加:'}
103
+ import { startEmailProxy } from 'react-native-email-imap-smtp/server/proxy';
104
+ startEmailProxy();
105
+
106
+ ${'B. Next.js / Webpack 等 — 在 dev 脚本中添加:'}
107
+ "dev": "email-imap-proxy & next dev"
108
+
109
+ ${'C. 或手动启动(会自动安装依赖):'}
110
+ npx email-imap-proxy
111
+
112
+ ${'D. 如果已启动但端口不同,请设置:'}
113
+ import { setProxyUrl } from 'react-native-email-imap-smtp';
114
+ setProxyUrl('http://localhost:你的端口号');
115
+ `
116
+ );
117
+ }
118
+
119
+ function getImapSessionId(): string {
120
+ if (!_imapSessionId) throw new ImapNotConnectedError();
121
+ return _imapSessionId;
122
+ }
123
+
124
+ function getSmtpSessionId(): string {
125
+ if (!_smtpSessionId) throw new SmtpNotConnectedError();
126
+ return _smtpSessionId;
127
+ }
128
+
129
+ function extractSession<T extends ConnectResult>(result: T): T {
130
+ if (result.success && result.sessionId) {
131
+ _imapSessionId = result.sessionId;
132
+ }
133
+ return result;
134
+ }
135
+
136
+ function extractSmtpSession<T extends ConnectResult>(result: T): T {
137
+ if (result.success && result.sessionId) {
138
+ _smtpSessionId = result.sessionId;
139
+ }
140
+ return result;
141
+ }
142
+
143
+ /** ArrayBuffer → base64 */
144
+ function arrayBufferToBase64(buf: ArrayBuffer): string {
145
+ const bytes = new Uint8Array(buf);
146
+ let binary = '';
147
+ for (let i = 0; i < bytes.length; i++) {
148
+ binary += String.fromCharCode(bytes[i]!);
149
+ }
150
+ return btoa(binary);
151
+ }
152
+
153
+ /** base64 → ArrayBuffer(服务器经 JSON 传输附件时为 base64 字符串,需解码回二进制) */
154
+ function base64ToArrayBuffer(base64: string): ArrayBuffer {
155
+ const binary = atob(base64);
156
+ const bytes = new Uint8Array(binary.length);
157
+ for (let i = 0; i < binary.length; i++) {
158
+ bytes[i] = binary.charCodeAt(i);
159
+ }
160
+ return bytes.buffer;
161
+ }
162
+
163
+ /** 归一化服务器返回的附件:若 data 是 base64 字符串则解码为 ArrayBuffer */
164
+ function normalizeAttachment(att: Attachment): Attachment {
165
+ const data = att.data as unknown;
166
+ if (typeof data === 'string') {
167
+ return { ...att, data: base64ToArrayBuffer(data) };
168
+ }
169
+ return att;
170
+ }
171
+
172
+ async function apiPost<T>(
173
+ path: string,
174
+ body: any,
175
+ errorMessage?: string
176
+ ): Promise<T> {
177
+ await checkProxyOrThrow();
178
+
179
+ const controller = new AbortController();
180
+ const timer = setTimeout(() => controller.abort(), API_TIMEOUT);
181
+
182
+ try {
183
+ const url = `${PROXY_URL}${path}`;
184
+ const res = await fetch(url, {
185
+ method: 'POST',
186
+ headers: { 'Content-Type': 'application/json' },
187
+ body: JSON.stringify(body),
188
+ signal: controller.signal as any,
189
+ });
190
+
191
+ if (!res.ok) {
192
+ const text = await res.text().catch(() => '');
193
+ throw new Error(
194
+ errorMessage ?? `HTTP ${res.status}: ${text || res.statusText}`
195
+ );
196
+ }
197
+
198
+ return res.json() as Promise<T>;
199
+ } finally {
200
+ clearTimeout(timer);
201
+ }
202
+ }
203
+
204
+ // ─── IMAP ────────────────────────────────────────────────────
205
+
206
+ export async function imapConnect(
207
+ config: IMAPConfig
208
+ ): Promise<IMAPConnectionResult> {
209
+ _imapSessionId = null;
210
+ const result = await apiPost<IMAPConnectionResult & { sessionId?: string }>(
211
+ '/api/imap/connect',
212
+ config,
213
+ 'IMAP connection failed'
214
+ );
215
+ return extractSession(result);
216
+ }
217
+
218
+ export async function imapDisconnect(): Promise<void> {
219
+ const sid = _imapSessionId;
220
+ _imapSessionId = null;
221
+ if (sid) {
222
+ await apiPost('/api/imap/disconnect', { sessionId: sid }).catch(() => {});
223
+ }
224
+ }
225
+
226
+ export async function imapListMailboxes(): Promise<Mailbox[]> {
227
+ return apiPost<Mailbox[]>('/api/imap/listMailboxes', {
228
+ sessionId: getImapSessionId(),
229
+ });
230
+ }
231
+
232
+ export async function imapSelectMailbox(
233
+ mailbox: string,
234
+ readOnly?: boolean
235
+ ): Promise<MailboxStatus> {
236
+ return apiPost<MailboxStatus>('/api/imap/selectMailbox', {
237
+ sessionId: getImapSessionId(),
238
+ mailbox,
239
+ readOnly: readOnly ?? false,
240
+ });
241
+ }
242
+
243
+ export async function imapRefreshMailbox(): Promise<MailboxStatus> {
244
+ return apiPost<MailboxStatus>('/api/imap/refreshMailbox', {
245
+ sessionId: getImapSessionId(),
246
+ });
247
+ }
248
+
249
+ export async function imapFetchFolders(): Promise<Folder[]> {
250
+ return apiPost<Folder[]>('/api/imap/fetchFolders', {
251
+ sessionId: getImapSessionId(),
252
+ });
253
+ }
254
+
255
+ export async function imapCreateFolder(folderName: string): Promise<void> {
256
+ await apiPost('/api/imap/createFolder', {
257
+ sessionId: getImapSessionId(),
258
+ folderName,
259
+ });
260
+ }
261
+
262
+ export async function imapDeleteFolder(folderName: string): Promise<void> {
263
+ await apiPost('/api/imap/deleteFolder', {
264
+ sessionId: getImapSessionId(),
265
+ folderName,
266
+ });
267
+ }
268
+
269
+ export async function imapRenameFolder(
270
+ oldName: string,
271
+ newName: string
272
+ ): Promise<void> {
273
+ await apiPost('/api/imap/renameFolder', {
274
+ sessionId: getImapSessionId(),
275
+ oldName,
276
+ newName,
277
+ });
278
+ }
279
+
280
+ export async function imapSubscribeFolder(folderName: string): Promise<void> {
281
+ await apiPost('/api/imap/subscribeFolder', {
282
+ sessionId: getImapSessionId(),
283
+ folderName,
284
+ });
285
+ }
286
+
287
+ export async function imapUnsubscribeFolder(folderName: string): Promise<void> {
288
+ await apiPost('/api/imap/unsubscribeFolder', {
289
+ sessionId: getImapSessionId(),
290
+ folderName,
291
+ });
292
+ }
293
+
294
+ export async function imapFetchEmails(
295
+ options: FetchEmailsOptions
296
+ ): Promise<Email[]> {
297
+ return apiPost<Email[]>('/api/imap/fetchEmails', {
298
+ sessionId: getImapSessionId(),
299
+ options,
300
+ });
301
+ }
302
+
303
+ export async function imapFetchEmailByUID(uid: number): Promise<Email | null> {
304
+ return apiPost<Email | null>('/api/imap/fetchEmailByUID', {
305
+ sessionId: getImapSessionId(),
306
+ uid,
307
+ });
308
+ }
309
+
310
+ export async function imapSearchEmails(
311
+ criteria: SearchCriteria
312
+ ): Promise<Email[]> {
313
+ return apiPost<Email[]>('/api/imap/searchEmails', {
314
+ sessionId: getImapSessionId(),
315
+ criteria,
316
+ });
317
+ }
318
+
319
+ export async function imapMarkAsRead(
320
+ uids: number[],
321
+ silent?: boolean
322
+ ): Promise<void> {
323
+ await apiPost('/api/imap/markAsRead', {
324
+ sessionId: getImapSessionId(),
325
+ uids,
326
+ silent: silent ?? true,
327
+ });
328
+ }
329
+
330
+ export async function imapMarkAsFlagged(
331
+ uids: number[],
332
+ flagged: boolean
333
+ ): Promise<void> {
334
+ await apiPost('/api/imap/markAsFlagged', {
335
+ sessionId: getImapSessionId(),
336
+ uids,
337
+ flagged,
338
+ });
339
+ }
340
+
341
+ export async function imapMoveEmails(
342
+ uids: number[],
343
+ destinationMailbox: string
344
+ ): Promise<void> {
345
+ await apiPost('/api/imap/moveEmails', {
346
+ sessionId: getImapSessionId(),
347
+ uids,
348
+ destinationMailbox,
349
+ });
350
+ }
351
+
352
+ export async function imapCopyEmails(
353
+ uids: number[],
354
+ destinationMailbox: string
355
+ ): Promise<void> {
356
+ await apiPost('/api/imap/copyEmails', {
357
+ sessionId: getImapSessionId(),
358
+ uids,
359
+ destinationMailbox,
360
+ });
361
+ }
362
+
363
+ export async function imapDeleteEmails(uids: number[]): Promise<BatchResult> {
364
+ return apiPost<BatchResult>('/api/imap/deleteEmails', {
365
+ sessionId: getImapSessionId(),
366
+ uids,
367
+ });
368
+ }
369
+
370
+ export async function imapExpunge(): Promise<void> {
371
+ await apiPost('/api/imap/expunge', {
372
+ sessionId: getImapSessionId(),
373
+ });
374
+ }
375
+
376
+ export async function imapFetchAttachments(
377
+ uid: number,
378
+ partId: string
379
+ ): Promise<Attachment | null> {
380
+ const result = await apiPost<Attachment | null>('/api/imap/fetchAttachment', {
381
+ sessionId: getImapSessionId(),
382
+ uid,
383
+ partId,
384
+ });
385
+ return result ? normalizeAttachment(result) : null;
386
+ }
387
+
388
+ export async function imapFetchAllAttachments(
389
+ uid: number
390
+ ): Promise<Attachment[]> {
391
+ const result = await apiPost<Attachment[]>('/api/imap/fetchAllAttachments', {
392
+ sessionId: getImapSessionId(),
393
+ uid,
394
+ });
395
+ return result.map(normalizeAttachment);
396
+ }
397
+
398
+ export async function imapIdle(
399
+ callback: (notification: IMAPNotification) => void
400
+ ): Promise<void> {
401
+ const sid = getImapSessionId();
402
+
403
+ // 启动空闲监听
404
+ await apiPost('/api/imap/idle', { sessionId: sid });
405
+
406
+ // 建立或复用 WebSocket 连接
407
+ if (!_ws || _ws.readyState !== WebSocket.OPEN) {
408
+ // 关闭旧连接(如有)
409
+ if (_ws) {
410
+ _ws.onclose = null;
411
+ _ws.onerror = null;
412
+ _ws.close();
413
+ }
414
+
415
+ const wsUrl = PROXY_URL.replace(/^http/, 'ws');
416
+ const ws = new WebSocket(
417
+ `${wsUrl}/ws?sessionId=${sid}`
418
+ ) as unknown as WebSocket;
419
+
420
+ ws.onclose = () => {
421
+ _ws = null;
422
+ };
423
+
424
+ ws.onerror = () => {
425
+ _ws = null;
426
+ };
427
+
428
+ _ws = ws;
429
+ }
430
+
431
+ // 始终绑定当前回调,无论是否复用连接
432
+ _ws.onmessage = (event) => {
433
+ try {
434
+ const data = JSON.parse(event.data as string);
435
+ if (data.type === 'connected') return;
436
+ callback(data as IMAPNotification);
437
+ } catch {
438
+ // ignore parse errors
439
+ }
440
+ };
441
+ }
442
+
443
+ export async function imapStopIdle(): Promise<void> {
444
+ const sid = _imapSessionId;
445
+ if (sid) {
446
+ await apiPost('/api/imap/stopIdle', { sessionId: sid }).catch(() => {});
447
+ }
448
+ if (_ws) {
449
+ _ws.close();
450
+ _ws = null;
451
+ }
452
+ }
453
+
454
+ export async function imapGetQuota(root?: string): Promise<QuotaInfo> {
455
+ return apiPost<QuotaInfo>('/api/imap/getQuota', {
456
+ sessionId: getImapSessionId(),
457
+ root: root ?? null,
458
+ });
459
+ }
460
+
461
+ export async function imapAppendMessage(
462
+ mailbox: string,
463
+ rawMimeData: ArrayBuffer,
464
+ flags?: string[]
465
+ ): Promise<AppendResult> {
466
+ return apiPost<AppendResult>('/api/imap/appendMessage', {
467
+ sessionId: getImapSessionId(),
468
+ mailbox,
469
+ rawMimeData: arrayBufferToBase64(rawMimeData),
470
+ flags: flags ?? null,
471
+ });
472
+ }
473
+
474
+ // ─── SMTP ────────────────────────────────────────────────────
475
+
476
+ export async function smtpConnect(
477
+ config: SMTPConfig
478
+ ): Promise<SMTPConnectionResult> {
479
+ _smtpSessionId = null;
480
+ const result = await apiPost<SMTPConnectionResult & { sessionId?: string }>(
481
+ '/api/smtp/connect',
482
+ config,
483
+ 'SMTP connection failed'
484
+ );
485
+ return extractSmtpSession(result);
486
+ }
487
+
488
+ export async function smtpDisconnect(): Promise<void> {
489
+ const sid = _smtpSessionId;
490
+ _smtpSessionId = null;
491
+ if (sid) {
492
+ await apiPost('/api/smtp/disconnect', { sessionId: sid }).catch(() => {});
493
+ }
494
+ }
495
+
496
+ export async function smtpSendMail(
497
+ options: SendMailOptions
498
+ ): Promise<SendMailResult> {
499
+ // 先处理附件中的 ArrayBuffer → base64,再序列化
500
+ const processedOptions: any = {
501
+ from: options.from,
502
+ to: options.to,
503
+ cc: options.cc,
504
+ bcc: options.bcc,
505
+ replyTo: options.replyTo,
506
+ subject: options.subject,
507
+ textBody: options.textBody,
508
+ htmlBody: options.htmlBody,
509
+ priority: options.priority,
510
+ headers: options.headers,
511
+ inReplyTo: options.inReplyTo,
512
+ references: options.references,
513
+ readReceiptTo: options.readReceiptTo,
514
+ charset: options.charset,
515
+ encoding: options.encoding,
516
+ };
517
+
518
+ if (options.attachments && options.attachments.length > 0) {
519
+ processedOptions.attachments = options.attachments.map((att) => ({
520
+ filename: att.filename,
521
+ mimeType: att.mimeType,
522
+ isInline: att.isInline,
523
+ contentId: att.contentId,
524
+ disposition: (att as any).disposition,
525
+ data:
526
+ att.data instanceof ArrayBuffer
527
+ ? arrayBufferToBase64(att.data)
528
+ : att.data,
529
+ }));
530
+ }
531
+
532
+ return apiPost<SendMailResult>('/api/smtp/sendMail', {
533
+ sessionId: getSmtpSessionId(),
534
+ options: processedOptions,
535
+ });
536
+ }
537
+
538
+ export async function smtpVerifyConnection(): Promise<boolean> {
539
+ const result = await apiPost<{ success: boolean }>(
540
+ '/api/smtp/verifyConnection',
541
+ { sessionId: getSmtpSessionId() }
542
+ );
543
+ return result.success;
544
+ }