react-native-email-imap-smtp 0.3.30 → 0.3.31

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.
@@ -404,6 +404,53 @@ export async function unsubscribeFolder(
404
404
 
405
405
  // ─── 获取邮件 ──────────────────────────────────────────────
406
406
 
407
+ /** 从 bodyStructure 推导 text/plain 与 text/html 的 part key 与字符集(与 fetchAllAttachments 的 walk 同构) */
408
+ function findTextParts(
409
+ bs: any
410
+ ): {
411
+ plain?: { key: string; charset?: string };
412
+ html?: { key: string; charset?: string };
413
+ } {
414
+ const out: {
415
+ plain?: { key: string; charset?: string };
416
+ html?: { key: string; charset?: string };
417
+ } = {};
418
+ function walk(node: any, prefix: string) {
419
+ if (node.childNodes && node.childNodes.length > 0) {
420
+ node.childNodes.forEach((child: any, i: number) => {
421
+ walk(child, prefix ? `${prefix}.${i + 1}` : `${i + 1}`);
422
+ });
423
+ return;
424
+ }
425
+ const type = String(node.type || '').toLowerCase();
426
+ const sub = String(node.subType || '').toLowerCase();
427
+ const charset = node.parameters?.charset;
428
+ if (type === 'text' && sub === 'plain' && !out.plain) {
429
+ out.plain = { key: prefix || '1', charset };
430
+ } else if (type === 'text' && sub === 'html' && !out.html) {
431
+ out.html = { key: prefix || '1', charset };
432
+ }
433
+ }
434
+ walk(bs, '');
435
+ return out;
436
+ }
437
+
438
+ /** 解码 text part 内容(传输编码已由 imapflow 解码;这里处理字符集,GBK 等用 iconv-lite) */
439
+ function decodePartText(buf: Buffer, charset?: string): string {
440
+ const cs = String(charset || 'utf-8').toLowerCase().replace(/"/g, '').trim();
441
+ try {
442
+ const iconv = require('iconv-lite') as any;
443
+ if (iconv.encodingExists(cs)) return iconv.decode(buf, cs);
444
+ } catch {
445
+ /* 降级到 Buffer.toString */
446
+ }
447
+ try {
448
+ return buf.toString(cs as BufferEncoding);
449
+ } catch {
450
+ return buf.toString('utf-8');
451
+ }
452
+ }
453
+
407
454
  export async function fetchEmails(
408
455
  sessionId: string,
409
456
  options: any
@@ -413,7 +460,12 @@ export async function fetchEmails(
413
460
 
414
461
  const lock = await s.client.getMailboxLock(mailbox);
415
462
  try {
416
- const mbox = await s.client.mailboxOpen(mailbox);
463
+ // 已选中同一邮箱时跳过 SELECT,避免重复往返
464
+ const alreadySelected =
465
+ s.client.mailbox && s.client.mailbox.path === mailbox;
466
+ const mbox = alreadySelected
467
+ ? (s.client.mailbox as any)
468
+ : await s.client.mailboxOpen(mailbox);
417
469
  s.currentMailbox = mailbox;
418
470
  const total = mbox.exists ?? 0;
419
471
 
@@ -456,7 +508,8 @@ export async function fetchEmails(
456
508
  range = { ...range, ...df };
457
509
  }
458
510
 
459
- // 构建 fetch fields
511
+ // 构建 fetch fields(默认不拉全文;正文用第二段只取 text 部分,跳过附件下载)
512
+ const needBody = options.fetchBody !== false;
460
513
  const fields: any = {
461
514
  uid: true,
462
515
  flags: true,
@@ -466,22 +519,67 @@ export async function fetchEmails(
466
519
  internalDate: true,
467
520
  headers: true,
468
521
  };
469
- if (options.fetchBody !== false) {
470
- fields.source = true;
471
- }
472
522
 
473
- const messages: any[] = [];
523
+ // 第一段:元数据(含 bodyStructure),随后推导正文 part key
524
+ const entries: any[] = [];
525
+ const byUid = new Map<number, any>();
474
526
  let seq = 0;
475
-
527
+ let textParts: {
528
+ plain?: { key: string; charset?: string };
529
+ html?: { key: string; charset?: string };
530
+ } | null = null;
476
531
  for await (const msg of s.client.fetch(range, fields, fetchOpts)) {
477
532
  seq++;
478
- let body: { text?: string; html?: string } | undefined;
479
- if (msg.source) {
480
- body = await parseSource(msg.source);
533
+ const entry = { msg, seq };
534
+ entries.push(entry);
535
+ byUid.set(msg.uid, entry);
536
+ if (needBody && !textParts && msg.bodyStructure) {
537
+ textParts = findTextParts(msg.bodyStructure);
538
+ }
539
+ }
540
+
541
+ // 第二段:只取 text/plain 与 text/html 正文(不下载附件)
542
+ if (needBody && textParts && (textParts.plain || textParts.html)) {
543
+ const bodyParts: any[] = [];
544
+ if (textParts.plain) bodyParts.push({ key: textParts.plain.key });
545
+ if (textParts.html) bodyParts.push({ key: textParts.html.key });
546
+ for await (const b of s.client.fetch(
547
+ range,
548
+ { uid: true, bodyParts },
549
+ fetchOpts
550
+ )) {
551
+ const entry = byUid.get(b.uid);
552
+ if (entry && b.bodyParts) {
553
+ entry.bodyPartsMap = b.bodyParts;
554
+ }
481
555
  }
482
- messages.push(buildEmailObject(msg, seq, body));
483
556
  }
484
557
 
558
+ const messages = entries.map(({ msg, seq: seqNo, bodyPartsMap }: any) => {
559
+ let body: { text?: string; html?: string } | undefined;
560
+ if (needBody && bodyPartsMap && textParts) {
561
+ const getPart = (key: string) =>
562
+ bodyPartsMap.get(key) ?? bodyPartsMap.get(key.toUpperCase());
563
+ if (textParts.plain) {
564
+ const buf = getPart(textParts.plain.key);
565
+ if (buf)
566
+ body = {
567
+ ...(body ?? {}),
568
+ text: decodePartText(buf, textParts.plain.charset),
569
+ };
570
+ }
571
+ if (textParts.html) {
572
+ const buf = getPart(textParts.html.key);
573
+ if (buf)
574
+ body = {
575
+ ...(body ?? {}),
576
+ html: decodePartText(buf, textParts.html.charset),
577
+ };
578
+ }
579
+ }
580
+ return buildEmailObject(msg, seqNo, body);
581
+ });
582
+
485
583
  messages.sort((a, b) => b.uid - a.uid);
486
584
  return messages;
487
585
  } finally {
@@ -527,7 +625,12 @@ export async function searchEmails(
527
625
 
528
626
  const lock = await s.client.getMailboxLock(mailbox);
529
627
  try {
530
- await s.client.mailboxOpen(mailbox);
628
+ // 已选中同一邮箱时跳过 SELECT
629
+ const alreadySelected =
630
+ s.client.mailbox && s.client.mailbox.path === mailbox;
631
+ if (!alreadySelected) {
632
+ await s.client.mailboxOpen(mailbox);
633
+ }
531
634
 
532
635
  const searchQuery: SearchObject = {};
533
636
 
@@ -576,6 +679,8 @@ export async function searchEmails(
576
679
  uids = uids.slice(0, criteria.limit);
577
680
  }
578
681
 
682
+ // 只取元数据 + 只拉 text 正文(不下载附件),与 fetchEmails 一致
683
+ const needBody = criteria.fetchBody !== false;
579
684
  const fields: any = {
580
685
  uid: true,
581
686
  flags: true,
@@ -585,20 +690,71 @@ export async function searchEmails(
585
690
  internalDate: true,
586
691
  headers: true,
587
692
  };
588
- if (criteria.fetchBody !== false) fields.source = true;
589
693
 
590
- const messages: any[] = [];
694
+ // 第一段:元数据(含 bodyStructure),随后推导正文 part key
695
+ const entries: any[] = [];
696
+ const byUid = new Map<number, any>();
591
697
  let seq = 0;
698
+ let textParts: {
699
+ plain?: { key: string; charset?: string };
700
+ html?: { key: string; charset?: string };
701
+ } | null = null;
592
702
  for await (const msg of s.client.fetch(
593
703
  { uid: uids.join(',') as any },
594
704
  fields,
595
705
  { uid: true }
596
706
  )) {
597
707
  seq++;
598
- const body = msg.source ? await parseSource(msg.source) : undefined;
599
- messages.push(buildEmailObject(msg, seq, body));
708
+ const entry = { msg, seq };
709
+ entries.push(entry);
710
+ byUid.set(msg.uid, entry);
711
+ if (needBody && !textParts && msg.bodyStructure) {
712
+ textParts = findTextParts(msg.bodyStructure);
713
+ }
714
+ }
715
+
716
+ // 第二段:只取 text/plain 与 text/html 正文(不下载附件)
717
+ if (needBody && textParts && (textParts.plain || textParts.html)) {
718
+ const bodyParts: any[] = [];
719
+ if (textParts.plain) bodyParts.push({ key: textParts.plain.key });
720
+ if (textParts.html) bodyParts.push({ key: textParts.html.key });
721
+ for await (const b of s.client.fetch(
722
+ { uid: uids.join(',') as any },
723
+ { uid: true, bodyParts },
724
+ { uid: true }
725
+ )) {
726
+ const entry = byUid.get(b.uid);
727
+ if (entry && b.bodyParts) {
728
+ entry.bodyPartsMap = b.bodyParts;
729
+ }
730
+ }
600
731
  }
601
732
 
733
+ const messages = entries.map(({ msg, seq: seqNo, bodyPartsMap }: any) => {
734
+ let body: { text?: string; html?: string } | undefined;
735
+ if (needBody && bodyPartsMap && textParts) {
736
+ const getPart = (key: string) =>
737
+ bodyPartsMap.get(key) ?? bodyPartsMap.get(key.toUpperCase());
738
+ if (textParts.plain) {
739
+ const buf = getPart(textParts.plain.key);
740
+ if (buf)
741
+ body = {
742
+ ...(body ?? {}),
743
+ text: decodePartText(buf, textParts.plain.charset),
744
+ };
745
+ }
746
+ if (textParts.html) {
747
+ const buf = getPart(textParts.html.key);
748
+ if (buf)
749
+ body = {
750
+ ...(body ?? {}),
751
+ html: decodePartText(buf, textParts.html.charset),
752
+ };
753
+ }
754
+ }
755
+ return buildEmailObject(msg, seqNo, body);
756
+ });
757
+
602
758
  return messages;
603
759
  } finally {
604
760
  lock.release();
@@ -613,12 +769,11 @@ export async function markAsRead(
613
769
  silent: boolean
614
770
  ): Promise<void> {
615
771
  const s = getSession(sessionId);
616
- for (const uid of uids) {
617
- await s.client.messageFlagsAdd({ uid: String(uid) }, ['\\Seen'], {
618
- silent,
619
- uid: true,
620
- });
621
- }
772
+ // 批量:一条 range 命令完成所有 UID,避免逐封往返
773
+ await s.client.messageFlagsAdd({ uid: uids.join(',') }, ['\\Seen'], {
774
+ silent,
775
+ uid: true,
776
+ });
622
777
  }
623
778
 
624
779
  export async function markAsFlagged(
@@ -627,16 +782,11 @@ export async function markAsFlagged(
627
782
  flagged: boolean
628
783
  ): Promise<void> {
629
784
  const s = getSession(sessionId);
630
- for (const uid of uids) {
631
- if (flagged) {
632
- await s.client.messageFlagsAdd({ uid: String(uid) }, ['\\Flagged'], {
633
- uid: true,
634
- });
635
- } else {
636
- await s.client.messageFlagsRemove({ uid: String(uid) }, ['\\Flagged'], {
637
- uid: true,
638
- });
639
- }
785
+ const range = { uid: uids.join(',') };
786
+ if (flagged) {
787
+ await s.client.messageFlagsAdd(range, ['\\Flagged'], { uid: true });
788
+ } else {
789
+ await s.client.messageFlagsRemove(range, ['\\Flagged'], { uid: true });
640
790
  }
641
791
  }
642
792
 
@@ -646,11 +796,9 @@ export async function moveEmails(
646
796
  destinationMailbox: string
647
797
  ): Promise<void> {
648
798
  const s = getSession(sessionId);
649
- for (const uid of uids) {
650
- await s.client.messageMove({ uid: String(uid) }, destinationMailbox, {
651
- uid: true,
652
- });
653
- }
799
+ await s.client.messageMove({ uid: uids.join(',') }, destinationMailbox, {
800
+ uid: true,
801
+ });
654
802
  }
655
803
 
656
804
  export async function copyEmails(
@@ -659,11 +807,9 @@ export async function copyEmails(
659
807
  destinationMailbox: string
660
808
  ): Promise<void> {
661
809
  const s = getSession(sessionId);
662
- for (const uid of uids) {
663
- await s.client.messageCopy({ uid: String(uid) }, destinationMailbox, {
664
- uid: true,
665
- });
666
- }
810
+ await s.client.messageCopy({ uid: uids.join(',') }, destinationMailbox, {
811
+ uid: true,
812
+ });
667
813
  }
668
814
 
669
815
  export async function deleteEmails(
@@ -671,17 +817,17 @@ export async function deleteEmails(
671
817
  uids: number[]
672
818
  ): Promise<any> {
673
819
  const s = getSession(sessionId);
674
- const success: number[] = [];
675
- const failed: number[] = [];
676
- for (const uid of uids) {
677
- try {
678
- await s.client.messageDelete({ uid: String(uid) }, { uid: true });
679
- success.push(uid);
680
- } catch {
681
- failed.push(uid);
682
- }
820
+ // 批量:一次 messageDelete 完成;失败则整批计入 failed(不再逐封往返)
821
+ try {
822
+ await s.client.messageDelete({ uid: uids.join(',') }, { uid: true });
823
+ return { success: uids, failed: [] };
824
+ } catch (err: any) {
825
+ return {
826
+ success: [],
827
+ failed: uids,
828
+ error: err?.message ?? 'Delete failed',
829
+ };
683
830
  }
684
- return { success, failed };
685
831
  }
686
832
 
687
833
  export async function expunge(sessionId: string): Promise<void> {
@@ -763,8 +909,10 @@ export async function fetchAttachment(
763
909
  // 同时降低被 20s 超时误判的风险。不要设到接近无上限——imapflow 靠
764
910
  // 「块是否被填满」(chunk.length >= chunkSize) 判断是否还有更多,
765
911
  // 服务器若截断超大单次响应,会被误判为已结束 → 附件被静默截断。
912
+ // 分块大小默认 1MB,可由连接配置 downloadChunkSize 覆盖(与 Android 对齐)
913
+ const chunkSize = s.config?.downloadChunkSize ?? 1024 * 1024;
766
914
  const download = await withTimeout(
767
- s.client.download(String(uid), partId, { uid: true, chunkSize: 1024 * 1024 }),
915
+ s.client.download(String(uid), partId, { uid: true, chunkSize }),
768
916
  20000,
769
917
  `download ${partId}`,
770
918
  );
@@ -45,6 +45,8 @@ export interface ConnectionConfig {
45
45
  checkCertificate: boolean;
46
46
  /** 是否输出调试日志 */
47
47
  debug: boolean;
48
+ /** 下载分块大小(字节),默认 1048576(1MB);调大可提升大附件下载速度 */
49
+ downloadChunkSize?: number;
48
50
  }
49
51
 
50
52
  /**
package/src/native.ts CHANGED
@@ -51,6 +51,7 @@ export async function imapConnect(
51
51
  timeout: config.timeout ?? 30000,
52
52
  checkCertificate: config.checkCertificate ?? true,
53
53
  debug: config.debug ?? false,
54
+ downloadChunkSize: config.downloadChunkSize,
54
55
  }) as Promise<IMAPConnectionResult>;
55
56
  }
56
57
 
package/src/types.ts CHANGED
@@ -51,6 +51,12 @@ export interface IMAPConfig {
51
51
  checkCertificate?: boolean;
52
52
  /** 是否在控制台输出调试日志 */
53
53
  debug?: boolean;
54
+ /**
55
+ * 下载分块大小(字节),默认 1048576(1MB)。
56
+ * Web 与 Android 通用:调大可减少大附件的分段请求次数、提升下载速度。
57
+ * 建议 256KB ~ 4MB;过大可能被服务器截断单次响应。
58
+ */
59
+ downloadChunkSize?: number;
54
60
  }
55
61
 
56
62
  /**