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
|
@@ -23,6 +23,8 @@ exports.moveEmails = moveEmails;
|
|
|
23
23
|
exports.copyEmails = copyEmails;
|
|
24
24
|
exports.deleteEmails = deleteEmails;
|
|
25
25
|
exports.expunge = expunge;
|
|
26
|
+
exports.isConnectionError = isConnectionError;
|
|
27
|
+
exports.isSessionClosed = isSessionClosed;
|
|
26
28
|
exports.fetchAttachment = fetchAttachment;
|
|
27
29
|
exports.fetchAllAttachments = fetchAllAttachments;
|
|
28
30
|
exports.startIdle = startIdle;
|
|
@@ -42,6 +44,12 @@ function getSession(sessionId) {
|
|
|
42
44
|
}
|
|
43
45
|
return s;
|
|
44
46
|
}
|
|
47
|
+
/** 取会话并确保连接存活:若已断开则静默重建新 ImapFlow 并重选邮箱,再返回(静默重连入口) */
|
|
48
|
+
async function ensureSession(sessionId) {
|
|
49
|
+
const s = getSession(sessionId);
|
|
50
|
+
await ensureConnected(s);
|
|
51
|
+
return s;
|
|
52
|
+
}
|
|
45
53
|
function flagsSetToArray(flags) {
|
|
46
54
|
if (!flags)
|
|
47
55
|
return [];
|
|
@@ -212,7 +220,7 @@ async function disconnect(sessionId) {
|
|
|
212
220
|
}
|
|
213
221
|
// ─── 列出邮箱 ──────────────────────────────────────────────
|
|
214
222
|
async function listMailboxes(sessionId) {
|
|
215
|
-
const s =
|
|
223
|
+
const s = await ensureSession(sessionId);
|
|
216
224
|
const list = await s.client.list();
|
|
217
225
|
return list.map((mbox) => ({
|
|
218
226
|
name: mbox.name,
|
|
@@ -224,7 +232,7 @@ async function listMailboxes(sessionId) {
|
|
|
224
232
|
}
|
|
225
233
|
// ─── 选择邮箱 ──────────────────────────────────────────────
|
|
226
234
|
async function selectMailbox(sessionId, mailbox, readOnly) {
|
|
227
|
-
const s =
|
|
235
|
+
const s = await ensureSession(sessionId);
|
|
228
236
|
const lock = await s.client.getMailboxLock(mailbox);
|
|
229
237
|
try {
|
|
230
238
|
const mbox = await s.client.mailboxOpen(mailbox, { readOnly });
|
|
@@ -266,7 +274,7 @@ async function selectMailbox(sessionId, mailbox, readOnly) {
|
|
|
266
274
|
}
|
|
267
275
|
// ─── 刷新邮箱 ──────────────────────────────────────────────
|
|
268
276
|
async function refreshMailbox(sessionId) {
|
|
269
|
-
const s =
|
|
277
|
+
const s = await ensureSession(sessionId);
|
|
270
278
|
const mailbox = s.currentMailbox ?? 'INBOX';
|
|
271
279
|
const lock = await s.client.getMailboxLock(mailbox);
|
|
272
280
|
try {
|
|
@@ -307,7 +315,7 @@ async function refreshMailbox(sessionId) {
|
|
|
307
315
|
}
|
|
308
316
|
// ─── 获取文件夹列表 ─────────────────────────────────────────
|
|
309
317
|
async function fetchFolders(sessionId) {
|
|
310
|
-
const s =
|
|
318
|
+
const s = await ensureSession(sessionId);
|
|
311
319
|
const list = await s.client.list();
|
|
312
320
|
const folders = list.map((mbox) => ({
|
|
313
321
|
name: mbox.name,
|
|
@@ -346,32 +354,77 @@ async function fetchFolders(sessionId) {
|
|
|
346
354
|
}
|
|
347
355
|
// ─── 创建/删除/重命名/订阅文件夹 ──────────────────────────
|
|
348
356
|
async function createFolder(sessionId, folderName) {
|
|
349
|
-
const s =
|
|
357
|
+
const s = await ensureSession(sessionId);
|
|
350
358
|
await s.client.mailboxCreate(folderName);
|
|
351
359
|
}
|
|
352
360
|
async function deleteFolder(sessionId, folderName) {
|
|
353
|
-
const s =
|
|
361
|
+
const s = await ensureSession(sessionId);
|
|
354
362
|
await s.client.mailboxDelete(folderName);
|
|
355
363
|
}
|
|
356
364
|
async function renameFolder(sessionId, oldName, newName) {
|
|
357
|
-
const s =
|
|
365
|
+
const s = await ensureSession(sessionId);
|
|
358
366
|
await s.client.mailboxRename(oldName, newName);
|
|
359
367
|
}
|
|
360
368
|
async function subscribeFolder(sessionId, folderName) {
|
|
361
|
-
const s =
|
|
369
|
+
const s = await ensureSession(sessionId);
|
|
362
370
|
await s.client.mailboxSubscribe(folderName);
|
|
363
371
|
}
|
|
364
372
|
async function unsubscribeFolder(sessionId, folderName) {
|
|
365
|
-
const s =
|
|
373
|
+
const s = await ensureSession(sessionId);
|
|
366
374
|
await s.client.mailboxUnsubscribe(folderName);
|
|
367
375
|
}
|
|
368
376
|
// ─── 获取邮件 ──────────────────────────────────────────────
|
|
377
|
+
/** 从 bodyStructure 推导 text/plain 与 text/html 的 part key 与字符集(与 fetchAllAttachments 的 walk 同构) */
|
|
378
|
+
function findTextParts(bs) {
|
|
379
|
+
const out = {};
|
|
380
|
+
function walk(node, prefix) {
|
|
381
|
+
if (node.childNodes && node.childNodes.length > 0) {
|
|
382
|
+
node.childNodes.forEach((child, i) => {
|
|
383
|
+
walk(child, prefix ? `${prefix}.${i + 1}` : `${i + 1}`);
|
|
384
|
+
});
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const type = String(node.type || '').toLowerCase();
|
|
388
|
+
const sub = String(node.subType || '').toLowerCase();
|
|
389
|
+
const charset = node.parameters?.charset;
|
|
390
|
+
if (type === 'text' && sub === 'plain' && !out.plain) {
|
|
391
|
+
out.plain = { key: prefix || '1', charset };
|
|
392
|
+
}
|
|
393
|
+
else if (type === 'text' && sub === 'html' && !out.html) {
|
|
394
|
+
out.html = { key: prefix || '1', charset };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
walk(bs, '');
|
|
398
|
+
return out;
|
|
399
|
+
}
|
|
400
|
+
/** 解码 text part 内容(传输编码已由 imapflow 解码;这里处理字符集,GBK 等用 iconv-lite) */
|
|
401
|
+
function decodePartText(buf, charset) {
|
|
402
|
+
const cs = String(charset || 'utf-8').toLowerCase().replace(/"/g, '').trim();
|
|
403
|
+
try {
|
|
404
|
+
const iconv = require('iconv-lite');
|
|
405
|
+
if (iconv.encodingExists(cs))
|
|
406
|
+
return iconv.decode(buf, cs);
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
/* 降级到 Buffer.toString */
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
return buf.toString(cs);
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
return buf.toString('utf-8');
|
|
416
|
+
}
|
|
417
|
+
}
|
|
369
418
|
async function fetchEmails(sessionId, options) {
|
|
370
|
-
const s =
|
|
419
|
+
const s = await ensureSession(sessionId);
|
|
371
420
|
const mailbox = options.mailbox ?? 'INBOX';
|
|
372
421
|
const lock = await s.client.getMailboxLock(mailbox);
|
|
373
422
|
try {
|
|
374
|
-
|
|
423
|
+
// 已选中同一邮箱时跳过 SELECT,避免重复往返
|
|
424
|
+
const alreadySelected = s.client.mailbox && s.client.mailbox.path === mailbox;
|
|
425
|
+
const mbox = alreadySelected
|
|
426
|
+
? s.client.mailbox
|
|
427
|
+
: await s.client.mailboxOpen(mailbox);
|
|
375
428
|
s.currentMailbox = mailbox;
|
|
376
429
|
const total = mbox.exists ?? 0;
|
|
377
430
|
// 构建查询范围
|
|
@@ -417,7 +470,8 @@ async function fetchEmails(sessionId, options) {
|
|
|
417
470
|
df.before = new Date(options.before);
|
|
418
471
|
range = { ...range, ...df };
|
|
419
472
|
}
|
|
420
|
-
// 构建 fetch fields
|
|
473
|
+
// 构建 fetch fields(默认不拉全文;正文用第二段只取 text 部分,跳过附件下载)
|
|
474
|
+
const needBody = options.fetchBody !== false;
|
|
421
475
|
const fields = {
|
|
422
476
|
uid: true,
|
|
423
477
|
flags: true,
|
|
@@ -427,19 +481,57 @@ async function fetchEmails(sessionId, options) {
|
|
|
427
481
|
internalDate: true,
|
|
428
482
|
headers: true,
|
|
429
483
|
};
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const messages = [];
|
|
484
|
+
// 第一段:元数据(含 bodyStructure),随后推导正文 part key
|
|
485
|
+
const entries = [];
|
|
486
|
+
const byUid = new Map();
|
|
434
487
|
let seq = 0;
|
|
488
|
+
let textParts = null;
|
|
435
489
|
for await (const msg of s.client.fetch(range, fields, fetchOpts)) {
|
|
436
490
|
seq++;
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
491
|
+
const entry = { msg, seq };
|
|
492
|
+
entries.push(entry);
|
|
493
|
+
byUid.set(msg.uid, entry);
|
|
494
|
+
if (needBody && !textParts && msg.bodyStructure) {
|
|
495
|
+
textParts = findTextParts(msg.bodyStructure);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// 第二段:只取 text/plain 与 text/html 正文(不下载附件)
|
|
499
|
+
if (needBody && textParts && (textParts.plain || textParts.html)) {
|
|
500
|
+
const bodyParts = [];
|
|
501
|
+
if (textParts.plain)
|
|
502
|
+
bodyParts.push({ key: textParts.plain.key });
|
|
503
|
+
if (textParts.html)
|
|
504
|
+
bodyParts.push({ key: textParts.html.key });
|
|
505
|
+
for await (const b of s.client.fetch(range, { uid: true, bodyParts }, fetchOpts)) {
|
|
506
|
+
const entry = byUid.get(b.uid);
|
|
507
|
+
if (entry && b.bodyParts) {
|
|
508
|
+
entry.bodyPartsMap = b.bodyParts;
|
|
509
|
+
}
|
|
440
510
|
}
|
|
441
|
-
messages.push(buildEmailObject(msg, seq, body));
|
|
442
511
|
}
|
|
512
|
+
const messages = entries.map(({ msg, seq: seqNo, bodyPartsMap }) => {
|
|
513
|
+
let body;
|
|
514
|
+
if (needBody && bodyPartsMap && textParts) {
|
|
515
|
+
const getPart = (key) => bodyPartsMap.get(key) ?? bodyPartsMap.get(key.toUpperCase());
|
|
516
|
+
if (textParts.plain) {
|
|
517
|
+
const buf = getPart(textParts.plain.key);
|
|
518
|
+
if (buf)
|
|
519
|
+
body = {
|
|
520
|
+
...(body ?? {}),
|
|
521
|
+
text: decodePartText(buf, textParts.plain.charset),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
if (textParts.html) {
|
|
525
|
+
const buf = getPart(textParts.html.key);
|
|
526
|
+
if (buf)
|
|
527
|
+
body = {
|
|
528
|
+
...(body ?? {}),
|
|
529
|
+
html: decodePartText(buf, textParts.html.charset),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return buildEmailObject(msg, seqNo, body);
|
|
534
|
+
});
|
|
443
535
|
messages.sort((a, b) => b.uid - a.uid);
|
|
444
536
|
return messages;
|
|
445
537
|
}
|
|
@@ -449,7 +541,7 @@ async function fetchEmails(sessionId, options) {
|
|
|
449
541
|
}
|
|
450
542
|
// ─── 按UID获取单封邮件 ─────────────────────────────────────
|
|
451
543
|
async function fetchEmailByUID(sessionId, uid) {
|
|
452
|
-
const s =
|
|
544
|
+
const s = await ensureSession(sessionId);
|
|
453
545
|
for await (const msg of s.client.fetch({ uid: String(uid) }, {
|
|
454
546
|
uid: true,
|
|
455
547
|
flags: true,
|
|
@@ -467,11 +559,15 @@ async function fetchEmailByUID(sessionId, uid) {
|
|
|
467
559
|
}
|
|
468
560
|
// ─── 搜索邮件 ──────────────────────────────────────────────
|
|
469
561
|
async function searchEmails(sessionId, criteria) {
|
|
470
|
-
const s =
|
|
562
|
+
const s = await ensureSession(sessionId);
|
|
471
563
|
const mailbox = criteria.mailbox ?? 'INBOX';
|
|
472
564
|
const lock = await s.client.getMailboxLock(mailbox);
|
|
473
565
|
try {
|
|
474
|
-
|
|
566
|
+
// 已选中同一邮箱时跳过 SELECT
|
|
567
|
+
const alreadySelected = s.client.mailbox && s.client.mailbox.path === mailbox;
|
|
568
|
+
if (!alreadySelected) {
|
|
569
|
+
await s.client.mailboxOpen(mailbox);
|
|
570
|
+
}
|
|
475
571
|
const searchQuery = {};
|
|
476
572
|
if (criteria.text)
|
|
477
573
|
searchQuery.text = criteria.text;
|
|
@@ -529,6 +625,8 @@ async function searchEmails(sessionId, criteria) {
|
|
|
529
625
|
if (criteria.limit && criteria.limit > 0) {
|
|
530
626
|
uids = uids.slice(0, criteria.limit);
|
|
531
627
|
}
|
|
628
|
+
// 只取元数据 + 只拉 text 正文(不下载附件),与 fetchEmails 一致
|
|
629
|
+
const needBody = criteria.fetchBody !== false;
|
|
532
630
|
const fields = {
|
|
533
631
|
uid: true,
|
|
534
632
|
flags: true,
|
|
@@ -538,15 +636,57 @@ async function searchEmails(sessionId, criteria) {
|
|
|
538
636
|
internalDate: true,
|
|
539
637
|
headers: true,
|
|
540
638
|
};
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const
|
|
639
|
+
// 第一段:元数据(含 bodyStructure),随后推导正文 part key
|
|
640
|
+
const entries = [];
|
|
641
|
+
const byUid = new Map();
|
|
544
642
|
let seq = 0;
|
|
643
|
+
let textParts = null;
|
|
545
644
|
for await (const msg of s.client.fetch({ uid: uids.join(',') }, fields, { uid: true })) {
|
|
546
645
|
seq++;
|
|
547
|
-
const
|
|
548
|
-
|
|
646
|
+
const entry = { msg, seq };
|
|
647
|
+
entries.push(entry);
|
|
648
|
+
byUid.set(msg.uid, entry);
|
|
649
|
+
if (needBody && !textParts && msg.bodyStructure) {
|
|
650
|
+
textParts = findTextParts(msg.bodyStructure);
|
|
651
|
+
}
|
|
549
652
|
}
|
|
653
|
+
// 第二段:只取 text/plain 与 text/html 正文(不下载附件)
|
|
654
|
+
if (needBody && textParts && (textParts.plain || textParts.html)) {
|
|
655
|
+
const bodyParts = [];
|
|
656
|
+
if (textParts.plain)
|
|
657
|
+
bodyParts.push({ key: textParts.plain.key });
|
|
658
|
+
if (textParts.html)
|
|
659
|
+
bodyParts.push({ key: textParts.html.key });
|
|
660
|
+
for await (const b of s.client.fetch({ uid: uids.join(',') }, { uid: true, bodyParts }, { uid: true })) {
|
|
661
|
+
const entry = byUid.get(b.uid);
|
|
662
|
+
if (entry && b.bodyParts) {
|
|
663
|
+
entry.bodyPartsMap = b.bodyParts;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const messages = entries.map(({ msg, seq: seqNo, bodyPartsMap }) => {
|
|
668
|
+
let body;
|
|
669
|
+
if (needBody && bodyPartsMap && textParts) {
|
|
670
|
+
const getPart = (key) => bodyPartsMap.get(key) ?? bodyPartsMap.get(key.toUpperCase());
|
|
671
|
+
if (textParts.plain) {
|
|
672
|
+
const buf = getPart(textParts.plain.key);
|
|
673
|
+
if (buf)
|
|
674
|
+
body = {
|
|
675
|
+
...(body ?? {}),
|
|
676
|
+
text: decodePartText(buf, textParts.plain.charset),
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
if (textParts.html) {
|
|
680
|
+
const buf = getPart(textParts.html.key);
|
|
681
|
+
if (buf)
|
|
682
|
+
body = {
|
|
683
|
+
...(body ?? {}),
|
|
684
|
+
html: decodePartText(buf, textParts.html.charset),
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return buildEmailObject(msg, seqNo, body);
|
|
689
|
+
});
|
|
550
690
|
return messages;
|
|
551
691
|
}
|
|
552
692
|
finally {
|
|
@@ -555,62 +695,52 @@ async function searchEmails(sessionId, criteria) {
|
|
|
555
695
|
}
|
|
556
696
|
// ─── 标记操作 ──────────────────────────────────────────────
|
|
557
697
|
async function markAsRead(sessionId, uids, silent) {
|
|
558
|
-
const s =
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
698
|
+
const s = await ensureSession(sessionId);
|
|
699
|
+
// 批量:一条 range 命令完成所有 UID,避免逐封往返
|
|
700
|
+
await s.client.messageFlagsAdd({ uid: uids.join(',') }, ['\\Seen'], {
|
|
701
|
+
silent,
|
|
702
|
+
uid: true,
|
|
703
|
+
});
|
|
565
704
|
}
|
|
566
705
|
async function markAsFlagged(sessionId, uids, flagged) {
|
|
567
|
-
const s =
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
else {
|
|
575
|
-
await s.client.messageFlagsRemove({ uid: String(uid) }, ['\\Flagged'], {
|
|
576
|
-
uid: true,
|
|
577
|
-
});
|
|
578
|
-
}
|
|
706
|
+
const s = await ensureSession(sessionId);
|
|
707
|
+
const range = { uid: uids.join(',') };
|
|
708
|
+
if (flagged) {
|
|
709
|
+
await s.client.messageFlagsAdd(range, ['\\Flagged'], { uid: true });
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
await s.client.messageFlagsRemove(range, ['\\Flagged'], { uid: true });
|
|
579
713
|
}
|
|
580
714
|
}
|
|
581
715
|
async function moveEmails(sessionId, uids, destinationMailbox) {
|
|
582
|
-
const s =
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
});
|
|
587
|
-
}
|
|
716
|
+
const s = await ensureSession(sessionId);
|
|
717
|
+
await s.client.messageMove({ uid: uids.join(',') }, destinationMailbox, {
|
|
718
|
+
uid: true,
|
|
719
|
+
});
|
|
588
720
|
}
|
|
589
721
|
async function copyEmails(sessionId, uids, destinationMailbox) {
|
|
590
|
-
const s =
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
});
|
|
595
|
-
}
|
|
722
|
+
const s = await ensureSession(sessionId);
|
|
723
|
+
await s.client.messageCopy({ uid: uids.join(',') }, destinationMailbox, {
|
|
724
|
+
uid: true,
|
|
725
|
+
});
|
|
596
726
|
}
|
|
597
727
|
async function deleteEmails(sessionId, uids) {
|
|
598
|
-
const s =
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
failed
|
|
608
|
-
|
|
728
|
+
const s = await ensureSession(sessionId);
|
|
729
|
+
// 批量:一次 messageDelete 完成;失败则整批计入 failed(不再逐封往返)
|
|
730
|
+
try {
|
|
731
|
+
await s.client.messageDelete({ uid: uids.join(',') }, { uid: true });
|
|
732
|
+
return { success: uids, failed: [] };
|
|
733
|
+
}
|
|
734
|
+
catch (err) {
|
|
735
|
+
return {
|
|
736
|
+
success: [],
|
|
737
|
+
failed: uids,
|
|
738
|
+
error: err?.message ?? 'Delete failed',
|
|
739
|
+
};
|
|
609
740
|
}
|
|
610
|
-
return { success, failed };
|
|
611
741
|
}
|
|
612
742
|
async function expunge(sessionId) {
|
|
613
|
-
const s =
|
|
743
|
+
const s = await ensureSession(sessionId);
|
|
614
744
|
const mailbox = s.currentMailbox ?? 'INBOX';
|
|
615
745
|
const lock = await s.client.getMailboxLock(mailbox);
|
|
616
746
|
try {
|
|
@@ -634,7 +764,17 @@ async function ensureConnected(s) {
|
|
|
634
764
|
const alive = !client.isClosed && client.state !== client.states.LOGOUT;
|
|
635
765
|
if (alive)
|
|
636
766
|
return;
|
|
637
|
-
// 连接已关闭 →
|
|
767
|
+
// 连接已关闭 → 重建新实例。并发安全:多个请求同时发现断开时
|
|
768
|
+
// 共享同一个重连 Promise(s.reconnecting),只真正重连一次,避免重连风暴。
|
|
769
|
+
if (s.reconnecting) {
|
|
770
|
+
return s.reconnecting;
|
|
771
|
+
}
|
|
772
|
+
s.reconnecting = doReconnect(s).finally(() => {
|
|
773
|
+
s.reconnecting = null;
|
|
774
|
+
});
|
|
775
|
+
return s.reconnecting;
|
|
776
|
+
}
|
|
777
|
+
async function doReconnect(s) {
|
|
638
778
|
if (!s.config) {
|
|
639
779
|
throw new Error('IMAP connection lost, but no config to reconnect');
|
|
640
780
|
}
|
|
@@ -655,6 +795,16 @@ async function ensureConnected(s) {
|
|
|
655
795
|
logger: false,
|
|
656
796
|
});
|
|
657
797
|
await newClient.connect();
|
|
798
|
+
// 重连期间若会话已被显式断开,则放弃这条新连接
|
|
799
|
+
if (!s.connected) {
|
|
800
|
+
try {
|
|
801
|
+
await newClient.logout();
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
/* ignore */
|
|
805
|
+
}
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
658
808
|
s.client = newClient;
|
|
659
809
|
s.connected = true;
|
|
660
810
|
// 重建后需重新选中原邮箱(download/fetch 依赖当前 mailbox)
|
|
@@ -663,6 +813,24 @@ async function ensureConnected(s) {
|
|
|
663
813
|
}
|
|
664
814
|
console.log('[ensureConnected] reconnected, mailbox:', s.currentMailbox);
|
|
665
815
|
}
|
|
816
|
+
// ─── 断线判定(供路由层决定是否自动重试)──────────────────
|
|
817
|
+
/** 判断错误是否为连接断开类(message + code),供路由层决定是否重连重试 */
|
|
818
|
+
function isConnectionError(err) {
|
|
819
|
+
if (!err)
|
|
820
|
+
return false;
|
|
821
|
+
const msg = String(err?.message ?? '').toLowerCase();
|
|
822
|
+
const code = String(err?.code ?? '').toLowerCase();
|
|
823
|
+
const re = /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;
|
|
824
|
+
return re.test(msg) || re.test(code);
|
|
825
|
+
}
|
|
826
|
+
/** 会话客户端是否已死(补充判断:message 未命中但客户端确实断了,也应重连重试)。
|
|
827
|
+
* 会话不存在或已被显式断开 → 返回 false(不该自动重连)。 */
|
|
828
|
+
function isSessionClosed(sessionId) {
|
|
829
|
+
const s = sessions.get(sessionId);
|
|
830
|
+
if (!s || !s.connected)
|
|
831
|
+
return false;
|
|
832
|
+
return s.client.isClosed || s.client.state === s.client.states.LOGOUT;
|
|
833
|
+
}
|
|
666
834
|
/** 给 Promise 加超时,避免死连接上无限挂起 */
|
|
667
835
|
function withTimeout(p, ms, label) {
|
|
668
836
|
return new Promise((resolve, reject) => {
|
|
@@ -677,7 +845,9 @@ async function fetchAttachment(sessionId, uid, partId) {
|
|
|
677
845
|
// 同时降低被 20s 超时误判的风险。不要设到接近无上限——imapflow 靠
|
|
678
846
|
// 「块是否被填满」(chunk.length >= chunkSize) 判断是否还有更多,
|
|
679
847
|
// 服务器若截断超大单次响应,会被误判为已结束 → 附件被静默截断。
|
|
680
|
-
|
|
848
|
+
// 分块大小默认 1MB,可由连接配置 downloadChunkSize 覆盖(与 Android 对齐)
|
|
849
|
+
const chunkSize = s.config?.downloadChunkSize ?? 1024 * 1024;
|
|
850
|
+
const download = await withTimeout(s.client.download(String(uid), partId, { uid: true, chunkSize }), 20000, `download ${partId}`);
|
|
681
851
|
const chunks = [];
|
|
682
852
|
for await (const chunk of download.content) {
|
|
683
853
|
chunks.push(chunk);
|
|
@@ -712,7 +882,7 @@ async function fetchAttachment(sessionId, uid, partId) {
|
|
|
712
882
|
}
|
|
713
883
|
}
|
|
714
884
|
async function fetchAllAttachments(sessionId, uid) {
|
|
715
|
-
const s =
|
|
885
|
+
const s = await ensureSession(sessionId);
|
|
716
886
|
// 确保已选中 mailbox(download() 依赖 this.mailbox,未选中会静默返回 {})
|
|
717
887
|
const mailbox = s.currentMailbox ?? 'INBOX';
|
|
718
888
|
if (!s.client.mailbox || s.client.mailbox.path !== mailbox) {
|
|
@@ -841,7 +1011,7 @@ function stopIdle(sessionId) {
|
|
|
841
1011
|
}
|
|
842
1012
|
// ─── Quota ─────────────────────────────────────────────────
|
|
843
1013
|
async function getQuota(sessionId, root) {
|
|
844
|
-
const s =
|
|
1014
|
+
const s = await ensureSession(sessionId);
|
|
845
1015
|
const quota = await s.client.getQuota(root);
|
|
846
1016
|
if (!quota) {
|
|
847
1017
|
return {
|
|
@@ -862,7 +1032,7 @@ async function getQuota(sessionId, root) {
|
|
|
862
1032
|
}
|
|
863
1033
|
// ─── Append ───────────────────────────────────────────────
|
|
864
1034
|
async function appendMessage(sessionId, mailbox, rawMimeData, flags) {
|
|
865
|
-
const s =
|
|
1035
|
+
const s = await ensureSession(sessionId);
|
|
866
1036
|
const content = Buffer.from(rawMimeData, 'base64');
|
|
867
1037
|
// APPENDLIMIT(RFC 7889):服务器接受的最大单封邮件字节数。
|
|
868
1038
|
// imapflow 已把它解析成数字;未通告时返回 boolean|undefined → 跳过检查。
|