react-native-email-imap-smtp 0.3.29 → 0.3.30
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.
|
@@ -277,6 +277,45 @@ class EmailImapSmtp : HybridEmailImapSmtpSpec() {
|
|
|
277
277
|
)
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
/**
|
|
281
|
+
* 与 Web 端(imapflow bodyStructure walk)对齐的附件叶子判定:
|
|
282
|
+
* disposition=attachment / 有 filename / 非 text 类型 → 视为附件。
|
|
283
|
+
* 内嵌 CID 资源走 onEmbedded;其余(含内嵌无 CID)走 onAttachment。
|
|
284
|
+
*/
|
|
285
|
+
private fun handleAttachmentLeaf(
|
|
286
|
+
part: Part,
|
|
287
|
+
onEmbedded: (String) -> Unit,
|
|
288
|
+
onAttachment: (AttachmentInfo) -> Unit,
|
|
289
|
+
) {
|
|
290
|
+
val disp = part.disposition ?: ""
|
|
291
|
+
val isAttachment = disp.equals(Part.ATTACHMENT, ignoreCase = true)
|
|
292
|
+
val isInline = disp.equals(Part.INLINE, ignoreCase = true)
|
|
293
|
+
val contentId = (part as? MimeBodyPart)?.contentID
|
|
294
|
+
val fileName = part.fileName ?: contentId ?: "unnamed"
|
|
295
|
+
// 非 text 类型(无 disposition/无 contentId)也应作为附件,与 Web 端对齐
|
|
296
|
+
val primary = part.contentType?.substringBefore('/')?.trim()?.lowercase() ?: ""
|
|
297
|
+
val isNonText = primary.isNotEmpty() && primary != "text" && primary != "multipart"
|
|
298
|
+
if (isAttachment || isInline || contentId != null || isNonText) {
|
|
299
|
+
val info = readAttachmentData(part as BodyPart, fileName, contentId, isInline, disp)
|
|
300
|
+
if (isInline && contentId != null) {
|
|
301
|
+
onEmbedded(fileName)
|
|
302
|
+
}
|
|
303
|
+
if (isAttachment || (isInline && contentId == null) || (isNonText && !isInline)) {
|
|
304
|
+
onAttachment(info)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** 判断是否为「附件样」叶子(有 attachment/inline disposition、filename 或 contentId)。
|
|
310
|
+
* 用于 text 类型正文分支先判定附件、再当作正文(与 Web 端 imapflow walk 对齐)。 */
|
|
311
|
+
private fun isAttachmentLike(part: Part): Boolean {
|
|
312
|
+
val disp = part.disposition ?: ""
|
|
313
|
+
if (disp.equals(Part.ATTACHMENT, ignoreCase = true) || disp.equals(Part.INLINE, ignoreCase = true)) return true
|
|
314
|
+
if (part.fileName != null) return true
|
|
315
|
+
if ((part as? MimeBodyPart)?.contentID != null) return true
|
|
316
|
+
return false
|
|
317
|
+
}
|
|
318
|
+
|
|
280
319
|
private fun parseBodyParts(
|
|
281
320
|
part: Part,
|
|
282
321
|
onText: (String) -> Unit,
|
|
@@ -285,19 +324,35 @@ class EmailImapSmtp : HybridEmailImapSmtpSpec() {
|
|
|
285
324
|
onAttachment: (AttachmentInfo) -> Unit,
|
|
286
325
|
) {
|
|
287
326
|
if (part.isMimeType("text/plain")) {
|
|
288
|
-
|
|
327
|
+
// text/plain 也可能是附件(带 attachment disposition / filename / name):
|
|
328
|
+
// 先判定附件,再当作正文。与 Web 端一致(1.txt/2.txt 等 text/plain 附件)。
|
|
329
|
+
if (isAttachmentLike(part)) {
|
|
330
|
+
handleAttachmentLeaf(part, onEmbedded, onAttachment)
|
|
331
|
+
} else {
|
|
332
|
+
onText(part.content?.toString() ?: "")
|
|
333
|
+
}
|
|
289
334
|
} else if (part.isMimeType("text/html")) {
|
|
290
|
-
|
|
335
|
+
if (isAttachmentLike(part)) {
|
|
336
|
+
handleAttachmentLeaf(part, onEmbedded, onAttachment)
|
|
337
|
+
} else {
|
|
338
|
+
onHtml(part.content?.toString() ?: "")
|
|
339
|
+
}
|
|
291
340
|
} else if (part.isMimeType("multipart/alternative")) {
|
|
292
341
|
val mp = part.content as? Multipart ?: return
|
|
293
342
|
var foundHtml = false
|
|
294
343
|
var foundText = false
|
|
295
344
|
for (i in 0 until mp.count) {
|
|
296
345
|
val bp = mp.getBodyPart(i)
|
|
297
|
-
if (bp.isMimeType("text/html") && !foundHtml) {
|
|
346
|
+
if (bp.isMimeType("text/html") && !foundHtml && !isAttachmentLike(bp)) {
|
|
298
347
|
onHtml(bp.content?.toString() ?: ""); foundHtml = true
|
|
299
|
-
} else if (bp.isMimeType("text/plain") && !foundText) {
|
|
348
|
+
} else if (bp.isMimeType("text/plain") && !foundText && !isAttachmentLike(bp)) {
|
|
300
349
|
onText(bp.content?.toString() ?: ""); foundText = true
|
|
350
|
+
} else if (bp.isMimeType("multipart/*")) {
|
|
351
|
+
// alternative 内嵌套的子 multipart(related/mixed 等)→ 递归,不遗漏附件
|
|
352
|
+
parseBodyParts(bp, onText, onHtml, onEmbedded, onAttachment)
|
|
353
|
+
} else {
|
|
354
|
+
// alternative 内的非正文叶子(内嵌图/附件)→ 附件判定
|
|
355
|
+
handleAttachmentLeaf(bp, onEmbedded, onAttachment)
|
|
301
356
|
}
|
|
302
357
|
}
|
|
303
358
|
} else if (part.isMimeType("multipart/*")) {
|
|
@@ -306,21 +361,7 @@ class EmailImapSmtp : HybridEmailImapSmtpSpec() {
|
|
|
306
361
|
parseBodyParts(mp.getBodyPart(i), onText, onHtml, onEmbedded, onAttachment)
|
|
307
362
|
}
|
|
308
363
|
} else {
|
|
309
|
-
|
|
310
|
-
val isAttachment = disp.equals(Part.ATTACHMENT, ignoreCase = true)
|
|
311
|
-
val isInline = disp.equals(Part.INLINE, ignoreCase = true)
|
|
312
|
-
val contentId = (part as? MimeBodyPart)?.contentID
|
|
313
|
-
val fileName = part.fileName ?: contentId ?: "unnamed"
|
|
314
|
-
|
|
315
|
-
if (isAttachment || isInline || contentId != null) {
|
|
316
|
-
val info = readAttachmentData(part as BodyPart, fileName, contentId, isInline, disp)
|
|
317
|
-
if (isInline && contentId != null) {
|
|
318
|
-
onEmbedded(fileName)
|
|
319
|
-
}
|
|
320
|
-
if (isAttachment || (isInline && contentId == null)) {
|
|
321
|
-
onAttachment(info)
|
|
322
|
-
}
|
|
323
|
-
}
|
|
364
|
+
handleAttachmentLeaf(part, onEmbedded, onAttachment)
|
|
324
365
|
}
|
|
325
366
|
}
|
|
326
367
|
|
|
@@ -1131,45 +1172,42 @@ class EmailImapSmtp : HybridEmailImapSmtpSpec() {
|
|
|
1131
1172
|
|
|
1132
1173
|
@Suppress("UNCHECKED_CAST")
|
|
1133
1174
|
private fun findAttachmentByPartId(msg: Message, partId: String): AttachmentInfo? {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
val disp = part.disposition ?: Part.ATTACHMENT
|
|
1139
|
-
if (disp.equals(Part.ATTACHMENT, ignoreCase = true) || disp.equals(Part.INLINE, ignoreCase = true)) {
|
|
1140
|
-
val fileName = part.fileName ?: "attachment_$i"
|
|
1141
|
-
if (fileName == partId || i.toString() == partId) {
|
|
1142
|
-
return readAttachmentData(part, fileName, (part as? MimeBodyPart)?.contentID,
|
|
1143
|
-
disp.equals(Part.INLINE, ignoreCase = true), disp)
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
if (part.content is Multipart) {
|
|
1147
|
-
@Suppress("UNCHECKED_CAST")
|
|
1148
|
-
return findAttachmentByPartId(part as Message, partId)
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
return null
|
|
1175
|
+
// 与 collectAttachments 同一套附件判定,按 partId(或文件名)匹配
|
|
1176
|
+
val all = mutableListOf<AttachmentInfo>()
|
|
1177
|
+
collectAttachments(msg, all)
|
|
1178
|
+
return all.firstOrNull { it.partId == partId || it.filename == partId }
|
|
1153
1179
|
}
|
|
1154
1180
|
|
|
1155
1181
|
@Suppress("UNCHECKED_CAST")
|
|
1156
1182
|
private fun collectAttachments(msg: Message, result: MutableList<AttachmentInfo>) {
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
}
|
|
1167
|
-
if (part.content is Multipart) {
|
|
1168
|
-
@Suppress("UNCHECKED_CAST")
|
|
1169
|
-
collectAttachments(part as Message, result)
|
|
1183
|
+
// 与 Web 端(imapflow bodyStructure walk)完全一致的附件收集:
|
|
1184
|
+
// 递归所有 multipart 子节点到叶子,只收「附件叶子」,跳过纯文本正文
|
|
1185
|
+
// (text/plain、text/html 且无 attachment disposition/filename)。
|
|
1186
|
+
// 不做任何正文提取——正文由 note_profile.json 决定。
|
|
1187
|
+
fun walk(part: Part) {
|
|
1188
|
+
val content = part.content
|
|
1189
|
+
if (content is Multipart) {
|
|
1190
|
+
for (i in 0 until content.count) {
|
|
1191
|
+
walk(content.getBodyPart(i))
|
|
1170
1192
|
}
|
|
1193
|
+
return
|
|
1194
|
+
}
|
|
1195
|
+
// 叶子:与 Web 端 `isAttachment || primary !== 'text'` 等价
|
|
1196
|
+
val disp = part.disposition ?: ""
|
|
1197
|
+
val isAtt = disp.equals(Part.ATTACHMENT, ignoreCase = true)
|
|
1198
|
+
val hasName = part.fileName != null
|
|
1199
|
+
val primary = part.contentType?.substringBefore('/')?.trim()?.lowercase() ?: ""
|
|
1200
|
+
val isTextLeaf = primary == "text" && !isAtt && !hasName
|
|
1201
|
+
if (!isTextLeaf) {
|
|
1202
|
+
val contentId = (part as? MimeBodyPart)?.contentID
|
|
1203
|
+
val fileName = part.fileName ?: contentId ?: "unnamed"
|
|
1204
|
+
result.add(
|
|
1205
|
+
readAttachmentData(part as BodyPart, fileName, contentId,
|
|
1206
|
+
disp.equals(Part.INLINE, ignoreCase = true), disp)
|
|
1207
|
+
)
|
|
1171
1208
|
}
|
|
1172
1209
|
}
|
|
1210
|
+
walk(msg)
|
|
1173
1211
|
}
|
|
1174
1212
|
|
|
1175
1213
|
// ═══ IMAP — IDLE ═══════════════════════════════════════════
|
package/package.json
CHANGED