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.
Files changed (43) hide show
  1. package/README.md +1 -1
  2. package/android/src/main/java/com/margelo/nitro/emailimapsmtp/EmailImapSmtp.kt +199 -114
  3. package/lib/module/native.js +2 -1
  4. package/lib/module/native.js.map +1 -1
  5. package/lib/typescript/server/src/imapHandler.d.ts +5 -0
  6. package/lib/typescript/server/src/imapHandler.d.ts.map +1 -1
  7. package/lib/typescript/server/src/index.d.ts.map +1 -1
  8. package/lib/typescript/server/src/smtpHandler.d.ts.map +1 -1
  9. package/lib/typescript/src/EmailImapSmtp.nitro.d.ts +4 -0
  10. package/lib/typescript/src/EmailImapSmtp.nitro.d.ts.map +1 -1
  11. package/lib/typescript/src/native.d.ts.map +1 -1
  12. package/lib/typescript/src/types.d.ts +8 -0
  13. package/lib/typescript/src/types.d.ts.map +1 -1
  14. package/nitrogen/generated/android/c++/JConnectionConfig.hpp +7 -3
  15. package/nitrogen/generated/android/c++/JSmtpServerInfo.hpp +8 -3
  16. package/nitrogen/generated/android/c++/JVariant_ImapServerInfo_SmtpServerInfo.hpp +1 -0
  17. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/ConnectionConfig.kt +9 -4
  18. package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/SmtpServerInfo.kt +9 -4
  19. package/nitrogen/generated/ios/EmailImapSmtp-Swift-Cxx-Bridge.hpp +15 -15
  20. package/nitrogen/generated/ios/swift/ConnectionConfig.swift +20 -2
  21. package/nitrogen/generated/ios/swift/SmtpServerInfo.swift +19 -1
  22. package/nitrogen/generated/shared/c++/ConnectionConfig.hpp +6 -2
  23. package/nitrogen/generated/shared/c++/SmtpServerInfo.hpp +7 -2
  24. package/package.json +1 -1
  25. package/server/lib/imapHandler.d.ts +5 -0
  26. package/server/lib/imapHandler.d.ts.map +1 -1
  27. package/server/lib/imapHandler.js +247 -77
  28. package/server/lib/imapHandler.js.map +1 -1
  29. package/server/lib/index.d.ts.map +1 -1
  30. package/server/lib/index.js +24 -9
  31. package/server/lib/index.js.map +1 -1
  32. package/server/lib/smtpHandler.d.ts.map +1 -1
  33. package/server/lib/smtpHandler.js +76 -8
  34. package/server/lib/smtpHandler.js.map +1 -1
  35. package/server/package-lock.json +1 -0
  36. package/server/package.json +1 -0
  37. package/server/src/imapHandler.ts +273 -76
  38. package/server/src/imapflow.d.ts +31 -0
  39. package/server/src/index.ts +50 -16
  40. package/server/src/smtpHandler.ts +310 -242
  41. package/src/EmailImapSmtp.nitro.ts +4 -0
  42. package/src/native.ts +1 -0
  43. 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 = getSession(sessionId);
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 = getSession(sessionId);
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 = getSession(sessionId);
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 = getSession(sessionId);
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 = getSession(sessionId);
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 = getSession(sessionId);
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 = getSession(sessionId);
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 = getSession(sessionId);
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 = getSession(sessionId);
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 = getSession(sessionId);
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
- const mbox = await s.client.mailboxOpen(mailbox);
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
- if (options.fetchBody !== false) {
431
- fields.source = true;
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
- let body;
438
- if (msg.source) {
439
- body = await parseSource(msg.source);
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 = getSession(sessionId);
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 = getSession(sessionId);
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
- await s.client.mailboxOpen(mailbox);
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
- if (criteria.fetchBody !== false)
542
- fields.source = true;
543
- const messages = [];
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 body = msg.source ? await parseSource(msg.source) : undefined;
548
- messages.push(buildEmailObject(msg, seq, body));
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 = getSession(sessionId);
559
- for (const uid of uids) {
560
- await s.client.messageFlagsAdd({ uid: String(uid) }, ['\\Seen'], {
561
- silent,
562
- uid: true,
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 = getSession(sessionId);
568
- for (const uid of uids) {
569
- if (flagged) {
570
- await s.client.messageFlagsAdd({ uid: String(uid) }, ['\\Flagged'], {
571
- uid: true,
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 = getSession(sessionId);
583
- for (const uid of uids) {
584
- await s.client.messageMove({ uid: String(uid) }, destinationMailbox, {
585
- uid: true,
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 = getSession(sessionId);
591
- for (const uid of uids) {
592
- await s.client.messageCopy({ uid: String(uid) }, destinationMailbox, {
593
- uid: true,
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 = getSession(sessionId);
599
- const success = [];
600
- const failed = [];
601
- for (const uid of uids) {
602
- try {
603
- await s.client.messageDelete({ uid: String(uid) }, { uid: true });
604
- success.push(uid);
605
- }
606
- catch {
607
- failed.push(uid);
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 = getSession(sessionId);
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
- const download = await withTimeout(s.client.download(String(uid), partId, { uid: true, chunkSize: 1024 * 1024 }), 20000, `download ${partId}`);
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 = getSession(sessionId);
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 = getSession(sessionId);
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 = getSession(sessionId);
1035
+ const s = await ensureSession(sessionId);
866
1036
  const content = Buffer.from(rawMimeData, 'base64');
867
1037
  // APPENDLIMIT(RFC 7889):服务器接受的最大单封邮件字节数。
868
1038
  // imapflow 已把它解析成数字;未通告时返回 boolean|undefined → 跳过检查。