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
- onText(part.content?.toString() ?: "")
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
- onHtml(part.content?.toString() ?: "")
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
- val disp = part.disposition ?: ""
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
- val content = msg.content
1135
- if (content is Multipart) {
1136
- for (i in 0 until content.count) {
1137
- val part = content.getBodyPart(i)
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
- val content = msg.content
1158
- if (content is Multipart) {
1159
- for (i in 0 until content.count) {
1160
- val part = content.getBodyPart(i)
1161
- val disp = part.disposition ?: Part.ATTACHMENT
1162
- if (disp.equals(Part.ATTACHMENT, ignoreCase = true) || disp.equals(Part.INLINE, ignoreCase = true)) {
1163
- result.add(readAttachmentData(part, part.fileName ?: "attachment_$i",
1164
- (part as? MimeBodyPart)?.contentID,
1165
- disp.equals(Part.INLINE, ignoreCase = true), disp))
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-email-imap-smtp",
3
- "version": "0.3.29",
3
+ "version": "0.3.30",
4
4
  "description": "The best of imap and smtp client of react native",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",