JavaMail下载Gmail CSV附件末尾截断 内容不完整问题排查
JavaMail下载Gmail CSV附件末尾内容截断问题排查
问题表现
- 服务配置、参数设置均验证正常,程序运行过程中无报错
- 附件可正常触发下载流程,大部分内容可正常保存,但最终生成的CSV文件丢失最后一整行,倒数第二行存在部分内容截断,无法获取完整文件
- 已调整代码中for循环逻辑,问题未解决
- 已排查缓冲区溢出、缓冲区大小设置问题:待下载CSV文件体积极小,单文件最多仅包含140行数据,不存在触发该类问题的条件
问题代码
public void searchEmailByID (int emailID) { Properties properties = new Properties(); //server setting properties.put("mail.imap.host", GmailHost); properties.put("mail.imap.port", GmailPort); //SSL setting properties.setProperty("mail.imap.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); properties.setProperty("mail.imap.socketFactory.fallback", "false"); properties.setProperty("mail.imap.socketFactory.port", String.valueOf(GmailPort)); Session session = Session.getDefaultInstance(properties); try { //connects to message store Store store = session.getStore("imap"); store.connect(GmailUser, GmailPassword); //opens the inbox folder Folder folderInbox = store.getFolder("Inbox"); folderInbox.open(Folder.READ_ONLY); //creates the search criteria SearchTerm searchCondition = new SearchTerm() { @Override public boolean match(Message message) { try { if (message.getMessageNumber() == emailID) { return true; } } catch (NullPointerException ex) { System.out.println("Null exception at line 50, may be due to variation in timing"); //ex.printStackTrace(); } return false; } }; //performs search through the folder Message[] foundMessages = folderInbox.search(searchCondition); setRecentlySearchMsg(foundMessages[0]); for (int i = 0; i < foundMessages.length; i++) { Message message = foundMessages[i]; String subject = message.getSubject(); //subject in the target email Date date = message.getReceivedDate(); //date the email was received System.out.println("Found message #" + i + ": " + subject + ", Date Received: " + date.toString()); String contentType = message.getContentType(); System.out.println("Content type: " + contentType); Multipart multipart = (Multipart) message.getContent(); int count = 0; //count object to ensure overflows aren't triggered for (int j = 0; j < multipart.getCount(); j++) { MimeBodyPart part = (MimeBodyPart) multipart.getBodyPart(j); //using the i loop to establish connection to message if(part.getContentType().contains("TEXT/PLAIN")) { continue; } else if(part.getContentType().contains("TEXT/HTML")) { break; } if (Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition())) { String dateToday = new SimpleDateFormat("MM-dd-yyyy").format(new Date()); //Retrieving today's date String path = "/file/location/here/" + dateToday + "_" + part.getFileName(); if(part.getFileName().contains("User")) { userFileLocation = path; } else if(part.getFileName().contains("Teams")) { teamFileLocation = path; } part.saveFile(path); System.out.println("File successfully saved in folder with name: " + path); System.out.println("File is located at: " + path); count++; } if (count == 2){ break; } } } //disconnect folderInbox.close(false); store.close(); } catch (NoSuchProviderException ex) { System.out.println("No provider."); ex.printStackTrace(); } catch (MessagingException ex) { System.out.println("Could not connect to the message store."); ex.printStackTrace(); } catch (IOException ex) { System.out.println("Error parsing message attachment"); ex.printStackTrace(); } }
根因定位
代码存在4处逻辑缺陷,共同导致附件内容截断:
- 遍历逻辑错误:遍历邮件MIME部件时,只要遇到Content-Type包含
TEXT/HTML的部件就直接break终止整个遍历循环。主流邮件客户端构造的带附件邮件MIME结构顺序不固定,常出现text/plain正文 -> text/html正文 -> 附件的顺序,该逻辑会导致附件流还未完全读取就被终止。 - 未处理嵌套MIME结构:大部分邮件的MIME结构为多层嵌套(最外层为multipart/mixed,内层嵌套multipart/alternative存储正文,外层同级节点存储附件),代码仅遍历第一层MIME部件,遇到嵌套结构内的text/html部件就会误触发终止逻辑,无法读全附件流。
- IMAP配置缺失:未关闭IMAP协议的部分拉取(partialfetch)特性,Gmail IMAP服务对分块拉取的响应在旧版JavaMail中存在流截断bug,小体积文本类附件极易出现末尾字节丢失的问题。
- 附件保存逻辑不可靠:直接调用
MimeBodyPart.saveFile()方法保存附件,该方法内部的流拷贝逻辑未显式校验流读取结束标记,且Session.getDefaultInstance()会复用JVM内已有的Session实例,可能导致自定义配置不生效,进一步加剧流读取不完整的问题。
修复方案
按以下步骤修改代码即可解决问题:
- 删除遍历部件时遇到
TEXT/HTML就break的逻辑,正文部件直接跳过即可,不要终止整个遍历循环 - 增加嵌套Multipart递归处理逻辑,遍历所有层级的MIME部件,避免嵌套结构导致的读取中断
- 在IMAP配置中新增参数关闭部分拉取特性,规避Gmail IMAP的分块响应bug:
properties.put("mail.imap.partialfetch", "false"); - 将
Session.getDefaultInstance(properties)替换为Session.getInstance(properties),确保自定义配置全部生效 - 替换直接调用
part.saveFile()的保存逻辑,手动读取附件输入流并写入文件,显式触发缓冲区刷新和流关闭,确保所有内容完整写入磁盘:try (InputStream is = part.getInputStream(); FileOutputStream fos = new FileOutputStream(path)) { byte[] buffer = new byte[4096]; int readLen; while ((readLen = is.read(buffer)) != -1) { fos.write(buffer, 0, readLen); } fos.flush(); }
内容的提问来源于stack exchange,提问作者natethegreat2022
相关产品推荐
相关产品推荐

