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.
- package/android/src/main/java/com/margelo/nitro/emailimapsmtp/EmailImapSmtp.kt +74 -58
- package/lib/module/native.js +2 -1
- package/lib/module/native.js.map +1 -1
- package/lib/typescript/server/src/imapHandler.d.ts.map +1 -1
- package/lib/typescript/src/EmailImapSmtp.nitro.d.ts +2 -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 +6 -0
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JConnectionConfig.hpp +7 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/emailimapsmtp/ConnectionConfig.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/shared/c++/ConnectionConfig.hpp +6 -2
- package/package.json +1 -1
- package/server/lib/imapHandler.d.ts.map +1 -1
- package/server/lib/imapHandler.js +179 -55
- package/server/lib/imapHandler.js.map +1 -1
- package/server/package-lock.json +1 -0
- package/server/package.json +1 -0
- package/server/src/imapHandler.ts +201 -53
- package/src/EmailImapSmtp.nitro.ts +2 -0
- package/src/native.ts +1 -0
- package/src/types.ts +6 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
599
|
-
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
failed
|
|
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
|
|
915
|
+
s.client.download(String(uid), partId, { uid: true, chunkSize }),
|
|
768
916
|
20000,
|
|
769
917
|
`download ${partId}`,
|
|
770
918
|
);
|
package/src/native.ts
CHANGED
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
|
/**
|