react-native-email-imap-smtp 0.3.30 → 0.3.32
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 +1 -1
- package/android/src/main/java/com/margelo/nitro/emailimapsmtp/EmailImapSmtp.kt +199 -114
- package/lib/module/native.js +2 -1
- package/lib/module/native.js.map +1 -1
- package/lib/typescript/server/src/imapHandler.d.ts +5 -0
- package/lib/typescript/server/src/imapHandler.d.ts.map +1 -1
- package/lib/typescript/server/src/index.d.ts.map +1 -1
- package/lib/typescript/server/src/smtpHandler.d.ts.map +1 -1
- package/lib/typescript/src/EmailImapSmtp.nitro.d.ts +4 -0
- package/lib/typescript/src/EmailImapSmtp.nitro.d.ts.map +1 -1
- package/lib/typescript/src/native.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +8 -0
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JConnectionConfig.hpp +7 -3
- package/nitrogen/generated/android/c++/JSmtpServerInfo.hpp +8 -3
- package/nitrogen/generated/android/c++/JVariant_ImapServerInfo_SmtpServerInfo.hpp +1 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/ConnectionConfig.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/SmtpServerInfo.kt +9 -4
- package/nitrogen/generated/ios/EmailImapSmtp-Swift-Cxx-Bridge.hpp +15 -15
- package/nitrogen/generated/ios/swift/ConnectionConfig.swift +20 -2
- package/nitrogen/generated/ios/swift/SmtpServerInfo.swift +19 -1
- package/nitrogen/generated/shared/c++/ConnectionConfig.hpp +6 -2
- package/nitrogen/generated/shared/c++/SmtpServerInfo.hpp +7 -2
- package/package.json +1 -1
- package/server/lib/imapHandler.d.ts +5 -0
- package/server/lib/imapHandler.d.ts.map +1 -1
- package/server/lib/imapHandler.js +247 -77
- package/server/lib/imapHandler.js.map +1 -1
- package/server/lib/index.d.ts.map +1 -1
- package/server/lib/index.js +24 -9
- package/server/lib/index.js.map +1 -1
- package/server/lib/smtpHandler.d.ts.map +1 -1
- package/server/lib/smtpHandler.js +76 -8
- package/server/lib/smtpHandler.js.map +1 -1
- package/server/package-lock.json +1 -0
- package/server/package.json +1 -0
- package/server/src/imapHandler.ts +273 -76
- package/server/src/imapflow.d.ts +31 -0
- package/server/src/index.ts +50 -16
- package/server/src/smtpHandler.ts +310 -242
- package/src/EmailImapSmtp.nitro.ts +4 -0
- package/src/native.ts +1 -0
- package/src/types.ts +8 -0
|
@@ -14,6 +14,8 @@ interface ImapSession {
|
|
|
14
14
|
currentMailbox?: string;
|
|
15
15
|
/** 连接配置(host/port/auth 等),用于断线后重建新实例 */
|
|
16
16
|
config?: any;
|
|
17
|
+
/** 进行中的重连 Promise(并发请求共享,避免重连风暴) */
|
|
18
|
+
reconnecting?: Promise<void> | null;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
const sessions = new Map<string, ImapSession>();
|
|
@@ -28,6 +30,13 @@ function getSession(sessionId: string): ImapSession {
|
|
|
28
30
|
return s;
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/** 取会话并确保连接存活:若已断开则静默重建新 ImapFlow 并重选邮箱,再返回(静默重连入口) */
|
|
34
|
+
async function ensureSession(sessionId: string): Promise<ImapSession> {
|
|
35
|
+
const s = getSession(sessionId);
|
|
36
|
+
await ensureConnected(s);
|
|
37
|
+
return s;
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
function flagsSetToArray(flags: Set<string> | undefined): string[] {
|
|
32
41
|
if (!flags) return [];
|
|
33
42
|
return Array.from(flags);
|
|
@@ -212,7 +221,7 @@ export async function disconnect(sessionId: string): Promise<void> {
|
|
|
212
221
|
// ─── 列出邮箱 ──────────────────────────────────────────────
|
|
213
222
|
|
|
214
223
|
export async function listMailboxes(sessionId: string): Promise<any[]> {
|
|
215
|
-
const s =
|
|
224
|
+
const s = await ensureSession(sessionId);
|
|
216
225
|
const list = await s.client.list();
|
|
217
226
|
return list.map((mbox) => ({
|
|
218
227
|
name: mbox.name,
|
|
@@ -230,7 +239,7 @@ export async function selectMailbox(
|
|
|
230
239
|
mailbox: string,
|
|
231
240
|
readOnly: boolean
|
|
232
241
|
): Promise<any> {
|
|
233
|
-
const s =
|
|
242
|
+
const s = await ensureSession(sessionId);
|
|
234
243
|
const lock = await s.client.getMailboxLock(mailbox);
|
|
235
244
|
try {
|
|
236
245
|
const mbox = await s.client.mailboxOpen(mailbox, { readOnly });
|
|
@@ -275,7 +284,7 @@ export async function selectMailbox(
|
|
|
275
284
|
// ─── 刷新邮箱 ──────────────────────────────────────────────
|
|
276
285
|
|
|
277
286
|
export async function refreshMailbox(sessionId: string): Promise<any> {
|
|
278
|
-
const s =
|
|
287
|
+
const s = await ensureSession(sessionId);
|
|
279
288
|
const mailbox = s.currentMailbox ?? 'INBOX';
|
|
280
289
|
const lock = await s.client.getMailboxLock(mailbox);
|
|
281
290
|
try {
|
|
@@ -319,7 +328,7 @@ export async function refreshMailbox(sessionId: string): Promise<any> {
|
|
|
319
328
|
// ─── 获取文件夹列表 ─────────────────────────────────────────
|
|
320
329
|
|
|
321
330
|
export async function fetchFolders(sessionId: string): Promise<any[]> {
|
|
322
|
-
const s =
|
|
331
|
+
const s = await ensureSession(sessionId);
|
|
323
332
|
const list = await s.client.list();
|
|
324
333
|
|
|
325
334
|
const folders = list.map((mbox) => ({
|
|
@@ -365,7 +374,7 @@ export async function createFolder(
|
|
|
365
374
|
sessionId: string,
|
|
366
375
|
folderName: string
|
|
367
376
|
): Promise<void> {
|
|
368
|
-
const s =
|
|
377
|
+
const s = await ensureSession(sessionId);
|
|
369
378
|
await s.client.mailboxCreate(folderName);
|
|
370
379
|
}
|
|
371
380
|
|
|
@@ -373,7 +382,7 @@ export async function deleteFolder(
|
|
|
373
382
|
sessionId: string,
|
|
374
383
|
folderName: string
|
|
375
384
|
): Promise<void> {
|
|
376
|
-
const s =
|
|
385
|
+
const s = await ensureSession(sessionId);
|
|
377
386
|
await s.client.mailboxDelete(folderName);
|
|
378
387
|
}
|
|
379
388
|
|
|
@@ -382,7 +391,7 @@ export async function renameFolder(
|
|
|
382
391
|
oldName: string,
|
|
383
392
|
newName: string
|
|
384
393
|
): Promise<void> {
|
|
385
|
-
const s =
|
|
394
|
+
const s = await ensureSession(sessionId);
|
|
386
395
|
await s.client.mailboxRename(oldName, newName);
|
|
387
396
|
}
|
|
388
397
|
|
|
@@ -390,7 +399,7 @@ export async function subscribeFolder(
|
|
|
390
399
|
sessionId: string,
|
|
391
400
|
folderName: string
|
|
392
401
|
): Promise<void> {
|
|
393
|
-
const s =
|
|
402
|
+
const s = await ensureSession(sessionId);
|
|
394
403
|
await s.client.mailboxSubscribe(folderName);
|
|
395
404
|
}
|
|
396
405
|
|
|
@@ -398,22 +407,74 @@ export async function unsubscribeFolder(
|
|
|
398
407
|
sessionId: string,
|
|
399
408
|
folderName: string
|
|
400
409
|
): Promise<void> {
|
|
401
|
-
const s =
|
|
410
|
+
const s = await ensureSession(sessionId);
|
|
402
411
|
await s.client.mailboxUnsubscribe(folderName);
|
|
403
412
|
}
|
|
404
413
|
|
|
405
414
|
// ─── 获取邮件 ──────────────────────────────────────────────
|
|
406
415
|
|
|
416
|
+
/** 从 bodyStructure 推导 text/plain 与 text/html 的 part key 与字符集(与 fetchAllAttachments 的 walk 同构) */
|
|
417
|
+
function findTextParts(
|
|
418
|
+
bs: any
|
|
419
|
+
): {
|
|
420
|
+
plain?: { key: string; charset?: string };
|
|
421
|
+
html?: { key: string; charset?: string };
|
|
422
|
+
} {
|
|
423
|
+
const out: {
|
|
424
|
+
plain?: { key: string; charset?: string };
|
|
425
|
+
html?: { key: string; charset?: string };
|
|
426
|
+
} = {};
|
|
427
|
+
function walk(node: any, prefix: string) {
|
|
428
|
+
if (node.childNodes && node.childNodes.length > 0) {
|
|
429
|
+
node.childNodes.forEach((child: any, i: number) => {
|
|
430
|
+
walk(child, prefix ? `${prefix}.${i + 1}` : `${i + 1}`);
|
|
431
|
+
});
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const type = String(node.type || '').toLowerCase();
|
|
435
|
+
const sub = String(node.subType || '').toLowerCase();
|
|
436
|
+
const charset = node.parameters?.charset;
|
|
437
|
+
if (type === 'text' && sub === 'plain' && !out.plain) {
|
|
438
|
+
out.plain = { key: prefix || '1', charset };
|
|
439
|
+
} else if (type === 'text' && sub === 'html' && !out.html) {
|
|
440
|
+
out.html = { key: prefix || '1', charset };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
walk(bs, '');
|
|
444
|
+
return out;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** 解码 text part 内容(传输编码已由 imapflow 解码;这里处理字符集,GBK 等用 iconv-lite) */
|
|
448
|
+
function decodePartText(buf: Buffer, charset?: string): string {
|
|
449
|
+
const cs = String(charset || 'utf-8').toLowerCase().replace(/"/g, '').trim();
|
|
450
|
+
try {
|
|
451
|
+
const iconv = require('iconv-lite') as any;
|
|
452
|
+
if (iconv.encodingExists(cs)) return iconv.decode(buf, cs);
|
|
453
|
+
} catch {
|
|
454
|
+
/* 降级到 Buffer.toString */
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
return buf.toString(cs as BufferEncoding);
|
|
458
|
+
} catch {
|
|
459
|
+
return buf.toString('utf-8');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
407
463
|
export async function fetchEmails(
|
|
408
464
|
sessionId: string,
|
|
409
465
|
options: any
|
|
410
466
|
): Promise<any[]> {
|
|
411
|
-
const s =
|
|
467
|
+
const s = await ensureSession(sessionId);
|
|
412
468
|
const mailbox = options.mailbox ?? 'INBOX';
|
|
413
469
|
|
|
414
470
|
const lock = await s.client.getMailboxLock(mailbox);
|
|
415
471
|
try {
|
|
416
|
-
|
|
472
|
+
// 已选中同一邮箱时跳过 SELECT,避免重复往返
|
|
473
|
+
const alreadySelected =
|
|
474
|
+
s.client.mailbox && s.client.mailbox.path === mailbox;
|
|
475
|
+
const mbox = alreadySelected
|
|
476
|
+
? (s.client.mailbox as any)
|
|
477
|
+
: await s.client.mailboxOpen(mailbox);
|
|
417
478
|
s.currentMailbox = mailbox;
|
|
418
479
|
const total = mbox.exists ?? 0;
|
|
419
480
|
|
|
@@ -456,7 +517,8 @@ export async function fetchEmails(
|
|
|
456
517
|
range = { ...range, ...df };
|
|
457
518
|
}
|
|
458
519
|
|
|
459
|
-
// 构建 fetch fields
|
|
520
|
+
// 构建 fetch fields(默认不拉全文;正文用第二段只取 text 部分,跳过附件下载)
|
|
521
|
+
const needBody = options.fetchBody !== false;
|
|
460
522
|
const fields: any = {
|
|
461
523
|
uid: true,
|
|
462
524
|
flags: true,
|
|
@@ -466,22 +528,67 @@ export async function fetchEmails(
|
|
|
466
528
|
internalDate: true,
|
|
467
529
|
headers: true,
|
|
468
530
|
};
|
|
469
|
-
if (options.fetchBody !== false) {
|
|
470
|
-
fields.source = true;
|
|
471
|
-
}
|
|
472
531
|
|
|
473
|
-
|
|
532
|
+
// 第一段:元数据(含 bodyStructure),随后推导正文 part key
|
|
533
|
+
const entries: any[] = [];
|
|
534
|
+
const byUid = new Map<number, any>();
|
|
474
535
|
let seq = 0;
|
|
475
|
-
|
|
536
|
+
let textParts: {
|
|
537
|
+
plain?: { key: string; charset?: string };
|
|
538
|
+
html?: { key: string; charset?: string };
|
|
539
|
+
} | null = null;
|
|
476
540
|
for await (const msg of s.client.fetch(range, fields, fetchOpts)) {
|
|
477
541
|
seq++;
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
542
|
+
const entry = { msg, seq };
|
|
543
|
+
entries.push(entry);
|
|
544
|
+
byUid.set(msg.uid, entry);
|
|
545
|
+
if (needBody && !textParts && msg.bodyStructure) {
|
|
546
|
+
textParts = findTextParts(msg.bodyStructure);
|
|
481
547
|
}
|
|
482
|
-
messages.push(buildEmailObject(msg, seq, body));
|
|
483
548
|
}
|
|
484
549
|
|
|
550
|
+
// 第二段:只取 text/plain 与 text/html 正文(不下载附件)
|
|
551
|
+
if (needBody && textParts && (textParts.plain || textParts.html)) {
|
|
552
|
+
const bodyParts: any[] = [];
|
|
553
|
+
if (textParts.plain) bodyParts.push({ key: textParts.plain.key });
|
|
554
|
+
if (textParts.html) bodyParts.push({ key: textParts.html.key });
|
|
555
|
+
for await (const b of s.client.fetch(
|
|
556
|
+
range,
|
|
557
|
+
{ uid: true, bodyParts },
|
|
558
|
+
fetchOpts
|
|
559
|
+
)) {
|
|
560
|
+
const entry = byUid.get(b.uid);
|
|
561
|
+
if (entry && b.bodyParts) {
|
|
562
|
+
entry.bodyPartsMap = b.bodyParts;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const messages = entries.map(({ msg, seq: seqNo, bodyPartsMap }: any) => {
|
|
568
|
+
let body: { text?: string; html?: string } | undefined;
|
|
569
|
+
if (needBody && bodyPartsMap && textParts) {
|
|
570
|
+
const getPart = (key: string) =>
|
|
571
|
+
bodyPartsMap.get(key) ?? bodyPartsMap.get(key.toUpperCase());
|
|
572
|
+
if (textParts.plain) {
|
|
573
|
+
const buf = getPart(textParts.plain.key);
|
|
574
|
+
if (buf)
|
|
575
|
+
body = {
|
|
576
|
+
...(body ?? {}),
|
|
577
|
+
text: decodePartText(buf, textParts.plain.charset),
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
if (textParts.html) {
|
|
581
|
+
const buf = getPart(textParts.html.key);
|
|
582
|
+
if (buf)
|
|
583
|
+
body = {
|
|
584
|
+
...(body ?? {}),
|
|
585
|
+
html: decodePartText(buf, textParts.html.charset),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return buildEmailObject(msg, seqNo, body);
|
|
590
|
+
});
|
|
591
|
+
|
|
485
592
|
messages.sort((a, b) => b.uid - a.uid);
|
|
486
593
|
return messages;
|
|
487
594
|
} finally {
|
|
@@ -495,7 +602,7 @@ export async function fetchEmailByUID(
|
|
|
495
602
|
sessionId: string,
|
|
496
603
|
uid: number
|
|
497
604
|
): Promise<any | null> {
|
|
498
|
-
const s =
|
|
605
|
+
const s = await ensureSession(sessionId);
|
|
499
606
|
for await (const msg of s.client.fetch(
|
|
500
607
|
{ uid: String(uid) },
|
|
501
608
|
{
|
|
@@ -522,12 +629,17 @@ export async function searchEmails(
|
|
|
522
629
|
sessionId: string,
|
|
523
630
|
criteria: any
|
|
524
631
|
): Promise<any[]> {
|
|
525
|
-
const s =
|
|
632
|
+
const s = await ensureSession(sessionId);
|
|
526
633
|
const mailbox = criteria.mailbox ?? 'INBOX';
|
|
527
634
|
|
|
528
635
|
const lock = await s.client.getMailboxLock(mailbox);
|
|
529
636
|
try {
|
|
530
|
-
|
|
637
|
+
// 已选中同一邮箱时跳过 SELECT
|
|
638
|
+
const alreadySelected =
|
|
639
|
+
s.client.mailbox && s.client.mailbox.path === mailbox;
|
|
640
|
+
if (!alreadySelected) {
|
|
641
|
+
await s.client.mailboxOpen(mailbox);
|
|
642
|
+
}
|
|
531
643
|
|
|
532
644
|
const searchQuery: SearchObject = {};
|
|
533
645
|
|
|
@@ -576,6 +688,8 @@ export async function searchEmails(
|
|
|
576
688
|
uids = uids.slice(0, criteria.limit);
|
|
577
689
|
}
|
|
578
690
|
|
|
691
|
+
// 只取元数据 + 只拉 text 正文(不下载附件),与 fetchEmails 一致
|
|
692
|
+
const needBody = criteria.fetchBody !== false;
|
|
579
693
|
const fields: any = {
|
|
580
694
|
uid: true,
|
|
581
695
|
flags: true,
|
|
@@ -585,20 +699,71 @@ export async function searchEmails(
|
|
|
585
699
|
internalDate: true,
|
|
586
700
|
headers: true,
|
|
587
701
|
};
|
|
588
|
-
if (criteria.fetchBody !== false) fields.source = true;
|
|
589
702
|
|
|
590
|
-
|
|
703
|
+
// 第一段:元数据(含 bodyStructure),随后推导正文 part key
|
|
704
|
+
const entries: any[] = [];
|
|
705
|
+
const byUid = new Map<number, any>();
|
|
591
706
|
let seq = 0;
|
|
707
|
+
let textParts: {
|
|
708
|
+
plain?: { key: string; charset?: string };
|
|
709
|
+
html?: { key: string; charset?: string };
|
|
710
|
+
} | null = null;
|
|
592
711
|
for await (const msg of s.client.fetch(
|
|
593
712
|
{ uid: uids.join(',') as any },
|
|
594
713
|
fields,
|
|
595
714
|
{ uid: true }
|
|
596
715
|
)) {
|
|
597
716
|
seq++;
|
|
598
|
-
const
|
|
599
|
-
|
|
717
|
+
const entry = { msg, seq };
|
|
718
|
+
entries.push(entry);
|
|
719
|
+
byUid.set(msg.uid, entry);
|
|
720
|
+
if (needBody && !textParts && msg.bodyStructure) {
|
|
721
|
+
textParts = findTextParts(msg.bodyStructure);
|
|
722
|
+
}
|
|
600
723
|
}
|
|
601
724
|
|
|
725
|
+
// 第二段:只取 text/plain 与 text/html 正文(不下载附件)
|
|
726
|
+
if (needBody && textParts && (textParts.plain || textParts.html)) {
|
|
727
|
+
const bodyParts: any[] = [];
|
|
728
|
+
if (textParts.plain) bodyParts.push({ key: textParts.plain.key });
|
|
729
|
+
if (textParts.html) bodyParts.push({ key: textParts.html.key });
|
|
730
|
+
for await (const b of s.client.fetch(
|
|
731
|
+
{ uid: uids.join(',') as any },
|
|
732
|
+
{ uid: true, bodyParts },
|
|
733
|
+
{ uid: true }
|
|
734
|
+
)) {
|
|
735
|
+
const entry = byUid.get(b.uid);
|
|
736
|
+
if (entry && b.bodyParts) {
|
|
737
|
+
entry.bodyPartsMap = b.bodyParts;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const messages = entries.map(({ msg, seq: seqNo, bodyPartsMap }: any) => {
|
|
743
|
+
let body: { text?: string; html?: string } | undefined;
|
|
744
|
+
if (needBody && bodyPartsMap && textParts) {
|
|
745
|
+
const getPart = (key: string) =>
|
|
746
|
+
bodyPartsMap.get(key) ?? bodyPartsMap.get(key.toUpperCase());
|
|
747
|
+
if (textParts.plain) {
|
|
748
|
+
const buf = getPart(textParts.plain.key);
|
|
749
|
+
if (buf)
|
|
750
|
+
body = {
|
|
751
|
+
...(body ?? {}),
|
|
752
|
+
text: decodePartText(buf, textParts.plain.charset),
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
if (textParts.html) {
|
|
756
|
+
const buf = getPart(textParts.html.key);
|
|
757
|
+
if (buf)
|
|
758
|
+
body = {
|
|
759
|
+
...(body ?? {}),
|
|
760
|
+
html: decodePartText(buf, textParts.html.charset),
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return buildEmailObject(msg, seqNo, body);
|
|
765
|
+
});
|
|
766
|
+
|
|
602
767
|
return messages;
|
|
603
768
|
} finally {
|
|
604
769
|
lock.release();
|
|
@@ -612,13 +777,12 @@ export async function markAsRead(
|
|
|
612
777
|
uids: number[],
|
|
613
778
|
silent: boolean
|
|
614
779
|
): Promise<void> {
|
|
615
|
-
const s =
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
}
|
|
780
|
+
const s = await ensureSession(sessionId);
|
|
781
|
+
// 批量:一条 range 命令完成所有 UID,避免逐封往返
|
|
782
|
+
await s.client.messageFlagsAdd({ uid: uids.join(',') }, ['\\Seen'], {
|
|
783
|
+
silent,
|
|
784
|
+
uid: true,
|
|
785
|
+
});
|
|
622
786
|
}
|
|
623
787
|
|
|
624
788
|
export async function markAsFlagged(
|
|
@@ -626,17 +790,12 @@ export async function markAsFlagged(
|
|
|
626
790
|
uids: number[],
|
|
627
791
|
flagged: boolean
|
|
628
792
|
): Promise<void> {
|
|
629
|
-
const s =
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
} else {
|
|
636
|
-
await s.client.messageFlagsRemove({ uid: String(uid) }, ['\\Flagged'], {
|
|
637
|
-
uid: true,
|
|
638
|
-
});
|
|
639
|
-
}
|
|
793
|
+
const s = await ensureSession(sessionId);
|
|
794
|
+
const range = { uid: uids.join(',') };
|
|
795
|
+
if (flagged) {
|
|
796
|
+
await s.client.messageFlagsAdd(range, ['\\Flagged'], { uid: true });
|
|
797
|
+
} else {
|
|
798
|
+
await s.client.messageFlagsRemove(range, ['\\Flagged'], { uid: true });
|
|
640
799
|
}
|
|
641
800
|
}
|
|
642
801
|
|
|
@@ -645,12 +804,10 @@ export async function moveEmails(
|
|
|
645
804
|
uids: number[],
|
|
646
805
|
destinationMailbox: string
|
|
647
806
|
): Promise<void> {
|
|
648
|
-
const s =
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
});
|
|
653
|
-
}
|
|
807
|
+
const s = await ensureSession(sessionId);
|
|
808
|
+
await s.client.messageMove({ uid: uids.join(',') }, destinationMailbox, {
|
|
809
|
+
uid: true,
|
|
810
|
+
});
|
|
654
811
|
}
|
|
655
812
|
|
|
656
813
|
export async function copyEmails(
|
|
@@ -658,34 +815,32 @@ export async function copyEmails(
|
|
|
658
815
|
uids: number[],
|
|
659
816
|
destinationMailbox: string
|
|
660
817
|
): Promise<void> {
|
|
661
|
-
const s =
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
});
|
|
666
|
-
}
|
|
818
|
+
const s = await ensureSession(sessionId);
|
|
819
|
+
await s.client.messageCopy({ uid: uids.join(',') }, destinationMailbox, {
|
|
820
|
+
uid: true,
|
|
821
|
+
});
|
|
667
822
|
}
|
|
668
823
|
|
|
669
824
|
export async function deleteEmails(
|
|
670
825
|
sessionId: string,
|
|
671
826
|
uids: number[]
|
|
672
827
|
): Promise<any> {
|
|
673
|
-
const s =
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
failed
|
|
682
|
-
|
|
828
|
+
const s = await ensureSession(sessionId);
|
|
829
|
+
// 批量:一次 messageDelete 完成;失败则整批计入 failed(不再逐封往返)
|
|
830
|
+
try {
|
|
831
|
+
await s.client.messageDelete({ uid: uids.join(',') }, { uid: true });
|
|
832
|
+
return { success: uids, failed: [] };
|
|
833
|
+
} catch (err: any) {
|
|
834
|
+
return {
|
|
835
|
+
success: [],
|
|
836
|
+
failed: uids,
|
|
837
|
+
error: err?.message ?? 'Delete failed',
|
|
838
|
+
};
|
|
683
839
|
}
|
|
684
|
-
return { success, failed };
|
|
685
840
|
}
|
|
686
841
|
|
|
687
842
|
export async function expunge(sessionId: string): Promise<void> {
|
|
688
|
-
const s =
|
|
843
|
+
const s = await ensureSession(sessionId);
|
|
689
844
|
const mailbox = s.currentMailbox ?? 'INBOX';
|
|
690
845
|
const lock = await s.client.getMailboxLock(mailbox);
|
|
691
846
|
try {
|
|
@@ -705,12 +860,23 @@ export async function expunge(sessionId: string): Promise<void> {
|
|
|
705
860
|
* ⚠️ 不要用 s.client.connected 判断(该属性不存在,永远是 undefined),
|
|
706
861
|
* 要用 isClosed + state。
|
|
707
862
|
*/
|
|
708
|
-
async function ensureConnected(s:
|
|
863
|
+
async function ensureConnected(s: ImapSession): Promise<void> {
|
|
709
864
|
const client = s.client;
|
|
710
865
|
const alive = !client.isClosed && client.state !== client.states.LOGOUT;
|
|
711
866
|
if (alive) return;
|
|
712
867
|
|
|
713
|
-
// 连接已关闭 →
|
|
868
|
+
// 连接已关闭 → 重建新实例。并发安全:多个请求同时发现断开时
|
|
869
|
+
// 共享同一个重连 Promise(s.reconnecting),只真正重连一次,避免重连风暴。
|
|
870
|
+
if (s.reconnecting) {
|
|
871
|
+
return s.reconnecting;
|
|
872
|
+
}
|
|
873
|
+
s.reconnecting = doReconnect(s).finally(() => {
|
|
874
|
+
s.reconnecting = null;
|
|
875
|
+
});
|
|
876
|
+
return s.reconnecting;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
async function doReconnect(s: ImapSession): Promise<void> {
|
|
714
880
|
if (!s.config) {
|
|
715
881
|
throw new Error('IMAP connection lost, but no config to reconnect');
|
|
716
882
|
}
|
|
@@ -731,6 +897,15 @@ async function ensureConnected(s: any): Promise<void> {
|
|
|
731
897
|
logger: false,
|
|
732
898
|
});
|
|
733
899
|
await newClient.connect();
|
|
900
|
+
// 重连期间若会话已被显式断开,则放弃这条新连接
|
|
901
|
+
if (!s.connected) {
|
|
902
|
+
try {
|
|
903
|
+
await newClient.logout();
|
|
904
|
+
} catch {
|
|
905
|
+
/* ignore */
|
|
906
|
+
}
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
734
909
|
s.client = newClient;
|
|
735
910
|
s.connected = true;
|
|
736
911
|
// 重建后需重新选中原邮箱(download/fetch 依赖当前 mailbox)
|
|
@@ -740,6 +915,26 @@ async function ensureConnected(s: any): Promise<void> {
|
|
|
740
915
|
console.log('[ensureConnected] reconnected, mailbox:', s.currentMailbox);
|
|
741
916
|
}
|
|
742
917
|
|
|
918
|
+
// ─── 断线判定(供路由层决定是否自动重试)──────────────────
|
|
919
|
+
|
|
920
|
+
/** 判断错误是否为连接断开类(message + code),供路由层决定是否重连重试 */
|
|
921
|
+
export function isConnectionError(err: any): boolean {
|
|
922
|
+
if (!err) return false;
|
|
923
|
+
const msg = String(err?.message ?? '').toLowerCase();
|
|
924
|
+
const code = String(err?.code ?? '').toLowerCase();
|
|
925
|
+
const re =
|
|
926
|
+
/connection closed|connection lost|disconnected|socket closed|socket hang up|econnreset|etimedout|econnrefused|broken pipe|server closed|closed connection|network error|operation timed out/i;
|
|
927
|
+
return re.test(msg) || re.test(code);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/** 会话客户端是否已死(补充判断:message 未命中但客户端确实断了,也应重连重试)。
|
|
931
|
+
* 会话不存在或已被显式断开 → 返回 false(不该自动重连)。 */
|
|
932
|
+
export function isSessionClosed(sessionId: string): boolean {
|
|
933
|
+
const s = sessions.get(sessionId);
|
|
934
|
+
if (!s || !s.connected) return false;
|
|
935
|
+
return s.client.isClosed || s.client.state === s.client.states.LOGOUT;
|
|
936
|
+
}
|
|
937
|
+
|
|
743
938
|
/** 给 Promise 加超时,避免死连接上无限挂起 */
|
|
744
939
|
function withTimeout<T>(p: Promise<T>, ms: number, label: string): Promise<T> {
|
|
745
940
|
return new Promise<T>((resolve, reject) => {
|
|
@@ -763,8 +958,10 @@ export async function fetchAttachment(
|
|
|
763
958
|
// 同时降低被 20s 超时误判的风险。不要设到接近无上限——imapflow 靠
|
|
764
959
|
// 「块是否被填满」(chunk.length >= chunkSize) 判断是否还有更多,
|
|
765
960
|
// 服务器若截断超大单次响应,会被误判为已结束 → 附件被静默截断。
|
|
961
|
+
// 分块大小默认 1MB,可由连接配置 downloadChunkSize 覆盖(与 Android 对齐)
|
|
962
|
+
const chunkSize = s.config?.downloadChunkSize ?? 1024 * 1024;
|
|
766
963
|
const download = await withTimeout(
|
|
767
|
-
s.client.download(String(uid), partId, { uid: true, chunkSize
|
|
964
|
+
s.client.download(String(uid), partId, { uid: true, chunkSize }),
|
|
768
965
|
20000,
|
|
769
966
|
`download ${partId}`,
|
|
770
967
|
);
|
|
@@ -805,7 +1002,7 @@ export async function fetchAllAttachments(
|
|
|
805
1002
|
sessionId: string,
|
|
806
1003
|
uid: number
|
|
807
1004
|
): Promise<any[]> {
|
|
808
|
-
const s =
|
|
1005
|
+
const s = await ensureSession(sessionId);
|
|
809
1006
|
|
|
810
1007
|
// 确保已选中 mailbox(download() 依赖 this.mailbox,未选中会静默返回 {})
|
|
811
1008
|
const mailbox = s.currentMailbox ?? 'INBOX';
|
|
@@ -956,7 +1153,7 @@ export function stopIdle(sessionId: string): void {
|
|
|
956
1153
|
// ─── Quota ─────────────────────────────────────────────────
|
|
957
1154
|
|
|
958
1155
|
export async function getQuota(sessionId: string, root?: string): Promise<any> {
|
|
959
|
-
const s =
|
|
1156
|
+
const s = await ensureSession(sessionId);
|
|
960
1157
|
const quota = await s.client.getQuota(root);
|
|
961
1158
|
if (!quota) {
|
|
962
1159
|
return {
|
|
@@ -984,7 +1181,7 @@ export async function appendMessage(
|
|
|
984
1181
|
rawMimeData: string,
|
|
985
1182
|
flags: string[] | null
|
|
986
1183
|
): Promise<any> {
|
|
987
|
-
const s =
|
|
1184
|
+
const s = await ensureSession(sessionId);
|
|
988
1185
|
const content = Buffer.from(rawMimeData, 'base64');
|
|
989
1186
|
|
|
990
1187
|
// APPENDLIMIT(RFC 7889):服务器接受的最大单封邮件字节数。
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// imapflow 类型补充声明(module augmentation)
|
|
3
|
+
// ============================================================
|
|
4
|
+
//
|
|
5
|
+
// imapflow v1.5.0 的官方 .d.ts(lib/imap-flow.d.ts)未声明以下运行时属性,
|
|
6
|
+
// 但源码 lib/imap-flow.js 中确实存在(this.isClosed / this.state / this.states):
|
|
7
|
+
// - isClosed: boolean 连接已关闭时为 true
|
|
8
|
+
// - states: { ... } 连接状态常量枚举
|
|
9
|
+
// - state: number 当前连接状态值(对应 states 中的常量)
|
|
10
|
+
//
|
|
11
|
+
// 通过 declare module 合并到 ImapFlow 类上,避免用 any 绕过类型检查。
|
|
12
|
+
// 不要删除本文件:断线检测(ensureConnected / isSessionClosed)依赖这些字段。
|
|
13
|
+
// ============================================================
|
|
14
|
+
|
|
15
|
+
import 'imapflow';
|
|
16
|
+
|
|
17
|
+
declare module 'imapflow' {
|
|
18
|
+
interface ImapFlow {
|
|
19
|
+
/** 连接是否已关闭(服务器断开后为 true) */
|
|
20
|
+
isClosed: boolean;
|
|
21
|
+
/** 当前连接状态值(对应 states 中的常量) */
|
|
22
|
+
state: number;
|
|
23
|
+
/** 连接状态常量枚举:NOT_AUTHENTICATED / AUTHENTICATED / SELECTED / LOGOUT */
|
|
24
|
+
states: {
|
|
25
|
+
NOT_AUTHENTICATED: number;
|
|
26
|
+
AUTHENTICATED: number;
|
|
27
|
+
SELECTED: number;
|
|
28
|
+
LOGOUT: number;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|