新增预警页面
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
package com.jeesite.modules.app.dao;
|
||||
|
||||
import com.jeesite.modules.biz.entity.BizMailAttachments;
|
||||
import com.jeesite.modules.biz.entity.BizMailReceived;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class MailReceived implements Serializable {
|
||||
|
||||
private BizMailReceived received;
|
||||
|
||||
private List<BizMailAttachments> attachments = new ArrayList<>();
|
||||
|
||||
|
||||
public MailReceived(BizMailReceived received, List<BizMailAttachments> attachments) {
|
||||
this.received = received;
|
||||
this.attachments = attachments;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -144,6 +144,7 @@ public class LoggerUtils {
|
||||
StandardCharsets.UTF_8 // 避免中文乱码
|
||||
)
|
||||
)) {
|
||||
System.out.print(logContent);
|
||||
writer.write(logContent.toString());
|
||||
writer.flush();
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -0,0 +1,666 @@
|
||||
package com.jeesite.modules.app.utils;
|
||||
|
||||
import com.jeesite.modules.app.dao.MailReceived;
|
||||
import com.jeesite.modules.biz.entity.BizMailAccount;
|
||||
import com.jeesite.modules.biz.entity.BizMailAttachments;
|
||||
import com.jeesite.modules.biz.entity.BizMailReceived;
|
||||
|
||||
import javax.activation.MimetypesFileTypeMap;
|
||||
import javax.mail.*;
|
||||
import javax.mail.internet.*;
|
||||
import javax.mail.search.FlagTerm;
|
||||
import java.io.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.*;
|
||||
|
||||
public class MailReceiveUtils {
|
||||
|
||||
// 连接重试配置
|
||||
private static final int CONNECT_RETRY_TIMES = 3;
|
||||
private static final long CONNECT_RETRY_INTERVAL = 1000L;
|
||||
private static final LoggerUtils logger = LoggerUtils.getInstance();
|
||||
|
||||
// 缓存ByteBuffer
|
||||
private static final ThreadLocal<ByteBuffer> BUFFER_LOCAL = ThreadLocal.withInitial(() -> ByteBuffer.allocate(8192));
|
||||
|
||||
// MIME类型映射(修复附件FileType识别)
|
||||
private static final MimetypesFileTypeMap MIME_TYPE_MAP = new MimetypesFileTypeMap();
|
||||
|
||||
static {
|
||||
// 扩展MIME类型映射,解决常见文件类型识别错误
|
||||
MIME_TYPE_MAP.addMimeTypes("application/pdf pdf");
|
||||
MIME_TYPE_MAP.addMimeTypes("application/msword doc");
|
||||
MIME_TYPE_MAP.addMimeTypes("application/vnd.openxmlformats-officedocument.wordprocessingml.document docx");
|
||||
MIME_TYPE_MAP.addMimeTypes("application/vnd.ms-excel xls");
|
||||
MIME_TYPE_MAP.addMimeTypes("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx");
|
||||
MIME_TYPE_MAP.addMimeTypes("application/vnd.ms-powerpoint ppt");
|
||||
MIME_TYPE_MAP.addMimeTypes("application/vnd.openxmlformats-officedocument.presentationml.presentation pptx");
|
||||
MIME_TYPE_MAP.addMimeTypes("image/jpeg jpg jpeg");
|
||||
MIME_TYPE_MAP.addMimeTypes("image/png png");
|
||||
MIME_TYPE_MAP.addMimeTypes("image/gif gif");
|
||||
MIME_TYPE_MAP.addMimeTypes("application/zip zip");
|
||||
MIME_TYPE_MAP.addMimeTypes("application/x-rar-compressed rar");
|
||||
MIME_TYPE_MAP.addMimeTypes("application/x-7z-compressed 7z");
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收未读邮件(优化:优先解析HTML格式正文)
|
||||
*/
|
||||
public static List<MailReceived> receiveUnreadMails(BizMailAccount mailAccount, String saveBasePath) throws Exception {
|
||||
List<MailReceived> receivedMailList = new ArrayList<>();
|
||||
Session session = createMailSession(mailAccount);
|
||||
Store store = null;
|
||||
Folder folder = null;
|
||||
|
||||
try {
|
||||
// 1. 建立IMAP连接(带重试)
|
||||
store = session.getStore("imap");
|
||||
boolean isConnected = false;
|
||||
int retryCount = 0;
|
||||
while (!isConnected) {
|
||||
try {
|
||||
store.connect(mailAccount.getHost(), mailAccount.getImapPort(),
|
||||
mailAccount.getUsername(), mailAccount.getPassword());
|
||||
isConnected = store.isConnected();
|
||||
if (isConnected) {
|
||||
logger.info("第" + (retryCount + 1) + "次连接IMAP成功:" + mailAccount.getHost());
|
||||
}
|
||||
} catch (AuthenticationFailedException e) {
|
||||
logger.error("账号/密码错误,直接返回", e);
|
||||
return receivedMailList;
|
||||
} catch (MessagingException e) {
|
||||
retryCount++;
|
||||
logger.error("第" + retryCount + "次连接失败:" + e.getMessage());
|
||||
if (retryCount >= CONNECT_RETRY_TIMES) {
|
||||
throw new IllegalStateException("IMAP连接失败(重试3次)", e);
|
||||
}
|
||||
Thread.sleep(CONNECT_RETRY_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 打开收件箱
|
||||
folder = store.getFolder("INBOX");
|
||||
if (folder == null || !folder.exists()) {
|
||||
logger.error("收件箱不存在");
|
||||
return receivedMailList;
|
||||
}
|
||||
folder.open(Folder.READ_WRITE);
|
||||
logger.info("未读邮件数:" + folder.getUnreadMessageCount());
|
||||
|
||||
// 3. 筛选未读邮件
|
||||
Message[] unreadMessages = folder.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
|
||||
if (unreadMessages == null || unreadMessages.length == 0) {
|
||||
logger.warn("无未读邮件");
|
||||
return receivedMailList;
|
||||
}
|
||||
|
||||
// 4. 处理每封邮件
|
||||
for (Message message : unreadMessages) {
|
||||
try {
|
||||
BizMailReceived bizMail = buildBizMailReceived(message, mailAccount);
|
||||
// 记录整封邮件附件下载开始时间
|
||||
long mailAttachDownloadStart = System.currentTimeMillis();
|
||||
logger.info("开始下载邮件[" + getMessageId(message) + "]的附件,开始时间:" + new Date(mailAttachDownloadStart));
|
||||
|
||||
List<BizMailAttachments> attachments = downloadExplicitAttachments(message, bizMail.getMessageId(), saveBasePath);
|
||||
|
||||
// 记录整封邮件附件下载结束时间
|
||||
long mailAttachDownloadEnd = System.currentTimeMillis();
|
||||
logger.info("完成下载邮件[" + getMessageId(message) + "]的附件,结束时间:" + new Date(mailAttachDownloadEnd) +
|
||||
",耗时:" + (mailAttachDownloadEnd - mailAttachDownloadStart) + "ms");
|
||||
|
||||
receivedMailList.add(new MailReceived(bizMail, attachments));
|
||||
message.setFlag(Flags.Flag.SEEN, true); // 标记已读
|
||||
} catch (Exception e) {
|
||||
logger.error("处理邮件失败(Message-ID:" + getMessageId(message) + "):" + e.getMessage(), e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
// 关闭资源
|
||||
if (folder != null && folder.isOpen()) {
|
||||
try {
|
||||
folder.close(true);
|
||||
} catch (MessagingException e) {
|
||||
logger.error("关闭文件夹失败", e);
|
||||
}
|
||||
}
|
||||
if (store != null && store.isConnected()) {
|
||||
try {
|
||||
store.close();
|
||||
} catch (MessagingException e) {
|
||||
logger.error("关闭连接失败", e);
|
||||
}
|
||||
}
|
||||
BUFFER_LOCAL.remove();
|
||||
}
|
||||
|
||||
return receivedMailList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建邮件会话(优化编码+IMAP配置)
|
||||
*/
|
||||
private static Session createMailSession(BizMailAccount mailAccount) {
|
||||
Properties props = new Properties();
|
||||
boolean sslEnable = "true".equals(mailAccount.getSslEnable());
|
||||
props.put("mail.imap.host", mailAccount.getHost());
|
||||
props.put("mail.imap.port", mailAccount.getImapPort());
|
||||
props.put("mail.imap.ssl.enable", sslEnable);
|
||||
props.put("mail.imap.ssl.protocols", "TLSv1.2"); // 强制TLS1.2,避免SSL漏洞
|
||||
props.put("mail.imap.auth", "true");
|
||||
props.put("mail.imap.connectiontimeout", "10000");
|
||||
props.put("mail.imap.timeout", "30000");
|
||||
props.put("mail.imap.partialfetch", "false"); // 禁用部分获取,避免内容截断
|
||||
props.put("mail.mime.charset", "UTF-8");
|
||||
props.put("mail.mime.base64.ignoreerrors", "true");
|
||||
props.put("mail.mime.decodefilenamehandler", "com.sun.mail.imap.protocol.IMAPUTF8DecodeHandler");
|
||||
// 关键:启用UTF-8文件名解码
|
||||
System.setProperty("mail.mime.encodeparameters", "false");
|
||||
|
||||
return Session.getInstance(props, new Authenticator() {
|
||||
@Override
|
||||
protected PasswordAuthentication getPasswordAuthentication() {
|
||||
return new PasswordAuthentication(mailAccount.getUsername(), mailAccount.getPassword());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建邮件实体(核心优化:优先解析HTML正文)
|
||||
*/
|
||||
private static BizMailReceived buildBizMailReceived(Message message, BizMailAccount mailAccount) throws Exception {
|
||||
BizMailReceived received = new BizMailReceived();
|
||||
received.setCreateTime(new Date());
|
||||
received.setMessageId(getMessageId(message));
|
||||
received.setAccountId(mailAccount.getId());
|
||||
received.setUstatus("1");
|
||||
received.setMailbox("INBOX");
|
||||
|
||||
// 发件人(修复编码)
|
||||
Address[] from = message.getFrom();
|
||||
if (from != null && from.length > 0) {
|
||||
InternetAddress fromAddr = (InternetAddress) from[0];
|
||||
received.setFromAddress(fromAddr.getAddress());
|
||||
// 优先解码发件人名称
|
||||
String personal = fromAddr.getPersonal();
|
||||
if (personal != null) {
|
||||
received.setFromName(decodeMimeText(personal));
|
||||
} else {
|
||||
received.setFromName(fromAddr.getAddress());
|
||||
}
|
||||
}
|
||||
|
||||
// 收件人/抄送/密送(修复编码)
|
||||
received.setToAddresses(convertAddresses(message.getRecipients(Message.RecipientType.TO)));
|
||||
received.setCcAddresses(convertAddresses(message.getRecipients(Message.RecipientType.CC)));
|
||||
received.setBccAddresses(convertAddresses(message.getRecipients(Message.RecipientType.BCC)));
|
||||
|
||||
// 主题(修复乱码)
|
||||
String subject = message.getSubject();
|
||||
received.setSubject(subject == null ? "" : decodeMimeText(subject));
|
||||
|
||||
// 核心优化:优先解析HTML格式正文
|
||||
String htmlContent = parseHtmlMailContent(message);
|
||||
// 最终空内容兜底
|
||||
received.setMailContent(htmlContent.isEmpty() ? "<p>(无正文内容)</p>" : htmlContent);
|
||||
|
||||
// 时间
|
||||
received.setReceivedTime(new Date());
|
||||
received.setSendTime(message.getSentDate() != null ? message.getSentDate() : new Date());
|
||||
|
||||
// 是否有显式附件
|
||||
received.setHasAttachment(hasExplicitAttachment(message) ? "1" : "0");
|
||||
|
||||
return received;
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心优化:专门解析HTML格式正文(优先提取HTML,无则将纯文本转为HTML)
|
||||
*/
|
||||
private static String parseHtmlMailContent(Message message) throws Exception {
|
||||
Object content = message.getContent();
|
||||
StringBuilder htmlSb = new StringBuilder();
|
||||
|
||||
// 递归提取HTML内容
|
||||
extractHtmlContentRecursive(content, htmlSb);
|
||||
|
||||
// 清理HTML内容,去除无效标签和空白
|
||||
String rawHtml = htmlSb.toString().replaceAll("\\n+", "").replaceAll("\\s+", " ").trim();
|
||||
// 如果没有提取到HTML内容,尝试提取纯文本并转为HTML格式
|
||||
if (rawHtml.isEmpty()) {
|
||||
String plainText = extractPlainTextContent(message);
|
||||
if (!plainText.isEmpty()) {
|
||||
// 将纯文本转为基本HTML格式(换行转<br>,空格转 )
|
||||
rawHtml = "<p>" + plainText.replaceAll("\\n", "<br>").replaceAll(" ", " ") + "</p>";
|
||||
}
|
||||
}
|
||||
|
||||
// 标准化HTML结构
|
||||
return standardizeHtml(rawHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归提取HTML内容(优先获取text/html类型)
|
||||
*/
|
||||
private static void extractHtmlContentRecursive(Object content, StringBuilder sb) throws Exception {
|
||||
if (content == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 直接是HTML文本(极少情况)
|
||||
if (content instanceof String text) {
|
||||
if (text.contains("<html>") || text.contains("<body>") || text.contains("<p>") || text.contains("<br>")) {
|
||||
sb.append(text);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. MimeMultipart:优先提取text/html部分
|
||||
if (content instanceof MimeMultipart multipart) {
|
||||
String multipartType = multipart.getContentType().split(";")[0].trim().toLowerCase();
|
||||
|
||||
// 处理multipart/alternative(多格式正文,优先取text/html)
|
||||
if ("multipart/alternative".equals(multipartType)) {
|
||||
for (int i = 0; i < multipart.getCount(); i++) {
|
||||
BodyPart part = multipart.getBodyPart(i);
|
||||
String partType = part.getContentType().split(";")[0].trim().toLowerCase();
|
||||
if ("text/html".equals(partType)) {
|
||||
// 读取HTML内容并追加
|
||||
sb.append(readBodyPartContent(part));
|
||||
return; // 找到HTML后直接返回,不再处理其他部分
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理其他multipart类型(递归提取所有HTML部分)
|
||||
for (int i = 0; i < multipart.getCount(); i++) {
|
||||
BodyPart bodyPart = multipart.getBodyPart(i);
|
||||
String disposition = bodyPart.getDisposition();
|
||||
|
||||
// 跳过显式附件
|
||||
if (Part.ATTACHMENT.equalsIgnoreCase(disposition)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 提取HTML内容
|
||||
String partType = bodyPart.getContentType().split(";")[0].trim().toLowerCase();
|
||||
if ("text/html".equals(partType)) {
|
||||
sb.append(readBodyPartContent(bodyPart));
|
||||
} else {
|
||||
// 递归解析子内容
|
||||
extractHtmlContentRecursive(bodyPart.getContent(), sb);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 单个MimeBodyPart:如果是HTML类型则读取
|
||||
if (content instanceof MimeBodyPart bodyPart) {
|
||||
String disposition = bodyPart.getDisposition();
|
||||
// 跳过显式附件
|
||||
if (Part.ATTACHMENT.equalsIgnoreCase(disposition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String partType = bodyPart.getContentType().split(";")[0].trim().toLowerCase();
|
||||
if ("text/html".equals(partType)) {
|
||||
sb.append(readBodyPartContent(bodyPart));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取纯文本内容(作为HTML的降级方案)
|
||||
*/
|
||||
private static String extractPlainTextContent(Message message) throws Exception {
|
||||
Object content = message.getContent();
|
||||
StringBuilder plainSb = new StringBuilder();
|
||||
|
||||
extractPlainTextRecursive(content, plainSb);
|
||||
|
||||
return plainSb.toString().replaceAll("^\\s+|\\s+$", "").replaceAll("\\n+", "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归提取纯文本内容
|
||||
*/
|
||||
private static void extractPlainTextRecursive(Object content, StringBuilder sb) throws Exception {
|
||||
if (content == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 纯文本直接追加
|
||||
if (content instanceof String) {
|
||||
String text = ((String) content).trim();
|
||||
if (!text.isEmpty()) {
|
||||
sb.append(text).append("\n");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理MimeMultipart
|
||||
if (content instanceof MimeMultipart multipart) {
|
||||
for (int i = 0; i < multipart.getCount(); i++) {
|
||||
BodyPart bodyPart = multipart.getBodyPart(i);
|
||||
// 跳过附件
|
||||
if (Part.ATTACHMENT.equalsIgnoreCase(bodyPart.getDisposition())) {
|
||||
continue;
|
||||
}
|
||||
// 提取纯文本
|
||||
extractPlainTextRecursive(bodyPart.getContent(), sb);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理单个BodyPart
|
||||
if (content instanceof MimeBodyPart bodyPart) {
|
||||
if (Part.ATTACHMENT.equalsIgnoreCase(bodyPart.getDisposition())) {
|
||||
return;
|
||||
}
|
||||
// 读取纯文本内容
|
||||
String partContent = readBodyPartContent(bodyPart);
|
||||
if (!partContent.isEmpty()) {
|
||||
sb.append(partContent).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化HTML结构(补全基本标签,确保格式合法)
|
||||
*/
|
||||
private static String standardizeHtml(String html) {
|
||||
if (html.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// 补全HTML基本结构
|
||||
if (!html.startsWith("<html>")) {
|
||||
html = "<html><head><meta charset=\"UTF-8\"></head><body>" + html + "</body></html>";
|
||||
}
|
||||
|
||||
// 修复常见的HTML格式问题
|
||||
html = html.replaceAll("<br>", "<br/>")
|
||||
.replaceAll("<hr>", "<hr/>")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<p>\\s*</p>", "") // 移除空p标签
|
||||
.replaceAll(">\\s+<", "><"); // 移除标签间多余空格
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取BodyPart内容(优先原始流,自动识别编码)
|
||||
*/
|
||||
private static String readBodyPartContent(BodyPart part) throws Exception {
|
||||
// 1. 获取ContentType和编码
|
||||
String contentType = part.getContentType() == null ? "" : part.getContentType().toLowerCase();
|
||||
Charset charset = getCharsetFromContentType(contentType);
|
||||
|
||||
// 2. 优先读取原始输入流(避免getContent()的自动转换错误)
|
||||
try (InputStream is = part.getInputStream();
|
||||
BufferedInputStream bis = new BufferedInputStream(is);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(bis, charset))) {
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
char[] buffer = new char[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = reader.read(buffer)) != -1) {
|
||||
sb.append(buffer, 0, bytesRead);
|
||||
}
|
||||
// 清理无效字符
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
logger.warn("读取BodyPart流失败:" + e.getMessage());
|
||||
Object fallbackContent = part.getContent();
|
||||
return fallbackContent instanceof String ? ((String) fallbackContent).trim() : "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邮件内容
|
||||
*/
|
||||
private static Charset getCharsetFromContentType(String contentType) {
|
||||
if (contentType == null || contentType.isEmpty()) {
|
||||
return StandardCharsets.UTF_8;
|
||||
}
|
||||
|
||||
// 提取charset参数(支持多种格式)
|
||||
String charsetStr = null;
|
||||
String[] parts = contentType.split(";");
|
||||
for (String part : parts) {
|
||||
part = part.trim().toLowerCase();
|
||||
if (part.startsWith("charset=")) {
|
||||
charsetStr = part.substring("charset=".length()).trim()
|
||||
.replace("\"", "").replace("'", "");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证编码有效性
|
||||
if (charsetStr != null && !charsetStr.isEmpty()) {
|
||||
try {
|
||||
return Charset.forName(charsetStr);
|
||||
} catch (Exception e) {
|
||||
logger.warn("不支持的编码:" + charsetStr + ",使用默认UTF-8");
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底编码(优先GBK,兼容中文邮件)
|
||||
try {
|
||||
return Charset.forName("GBK");
|
||||
} catch (Exception e) {
|
||||
return StandardCharsets.UTF_8;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码MIME编码的文本(主题/发件人名称等)
|
||||
*/
|
||||
private static String decodeMimeText(String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return MimeUtility.decodeText(text);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// 多级降级处理
|
||||
try {
|
||||
return new String(text.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
|
||||
} catch (Exception e1) {
|
||||
try {
|
||||
return new String(text.getBytes(StandardCharsets.ISO_8859_1), "GBK");
|
||||
} catch (Exception e2) {
|
||||
try {
|
||||
return new String(text.getBytes(StandardCharsets.ISO_8859_1), "GB2312");
|
||||
} catch (Exception e3) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载显式附件(保持原有逻辑)
|
||||
*/
|
||||
private static List<BizMailAttachments> downloadExplicitAttachments(Message message, String messageId, String saveBasePath) throws Exception {
|
||||
List<BizMailAttachments> attachments = new ArrayList<>();
|
||||
if (!message.isMimeType("multipart/*")) {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
MimeMultipart multipart = (MimeMultipart) message.getContent();
|
||||
File saveDir = new File(saveBasePath);
|
||||
if (!saveDir.exists()) {
|
||||
Files.createDirectories(saveDir.toPath());
|
||||
}
|
||||
|
||||
// 遍历所有显式附件
|
||||
for (int i = 0; i < multipart.getCount(); i++) {
|
||||
BodyPart part = multipart.getBodyPart(i);
|
||||
if (Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition())) {
|
||||
try {
|
||||
BizMailAttachments attach = downloadSingleAttachment(part, messageId, saveDir);
|
||||
attachments.add(attach);
|
||||
} catch (Exception e) {
|
||||
logger.error("单附件下载失败(文件名:" + part.getFileName() + "):" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载单个附件(保持原有逻辑)
|
||||
*/
|
||||
private static BizMailAttachments downloadSingleAttachment(BodyPart part, String messageId, File saveDir) throws Exception {
|
||||
BizMailAttachments attachment = new BizMailAttachments();
|
||||
|
||||
// 1. 记录单个附件下载开始时间
|
||||
long attachDownloadStartTime = System.currentTimeMillis();
|
||||
String originalFileName = part.getFileName();
|
||||
String fileNameForLog = originalFileName == null ? "未知文件名" : decodeMimeText(originalFileName);
|
||||
logger.info("开始下载附件[" + fileNameForLog + "],开始时间:" + new Date(attachDownloadStartTime));
|
||||
|
||||
// 2. 修复:文件名解码(解决乱码)
|
||||
if (originalFileName != null) {
|
||||
originalFileName = decodeMimeText(originalFileName);
|
||||
} else {
|
||||
originalFileName = "unknown_" + System.currentTimeMillis();
|
||||
}
|
||||
|
||||
// 3. 修复:附件FileType(优先从文件扩展名识别,解决ContentType错误)
|
||||
String fileExt = getFileExtension(originalFileName);
|
||||
String fileType = MIME_TYPE_MAP.getContentType(originalFileName);
|
||||
// 降级处理:如果MIME类型为空,使用BodyPart的ContentType
|
||||
if (fileType == null || fileType.isEmpty() || fileType.equals("application/octet-stream")) {
|
||||
fileType = part.getContentType().split(";")[0].trim(); // 去除charset等参数
|
||||
}
|
||||
|
||||
// 4. 计算MD5+保存文件
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
ByteBuffer buffer = BUFFER_LOCAL.get();
|
||||
buffer.clear();
|
||||
|
||||
// 临时文件
|
||||
File tempFile = new File(saveDir, UUID.randomUUID().toString());
|
||||
try (InputStream is = part.getInputStream();
|
||||
FileChannel channel = FileChannel.open(tempFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
|
||||
|
||||
int bytesRead;
|
||||
while ((bytesRead = is.read(buffer.array())) != -1) {
|
||||
md.update(buffer.array(), 0, bytesRead);
|
||||
buffer.limit(bytesRead);
|
||||
channel.write(buffer);
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// MD5命名(避免重名)
|
||||
String md5 = bytesToHex(md.digest());
|
||||
File finalFile = new File(saveDir, md5 + (fileExt.isEmpty() ? "" : "." + fileExt));
|
||||
|
||||
// 处理文件已存在
|
||||
if (finalFile.exists()) {
|
||||
logger.warn("附件已存在(MD5:" + md5 + "),跳过重复保存");
|
||||
} else {
|
||||
if (!tempFile.renameTo(finalFile)) {
|
||||
logger.warn("附件重命名失败,使用临时文件名:" + tempFile.getName());
|
||||
finalFile = tempFile;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 记录单个附件下载结束时间
|
||||
long attachDownloadEndTime = System.currentTimeMillis();
|
||||
long costTime = attachDownloadEndTime - attachDownloadStartTime;
|
||||
logger.info("完成下载附件[" + originalFileName + "],结束时间:" + new Date(attachDownloadEndTime) +
|
||||
",耗时:" + costTime + "ms,文件路径:" + finalFile.getAbsolutePath());
|
||||
|
||||
// 6. 封装附件信息(修复FileType,增加下载时间字段)
|
||||
attachment.setStoragePath(finalFile.getAbsolutePath());
|
||||
attachment.setFileSize(finalFile.length());
|
||||
attachment.setCreateTime(new Date());
|
||||
attachment.setTid(System.currentTimeMillis());
|
||||
attachment.setMailId(System.currentTimeMillis());
|
||||
attachment.setMessageId(messageId);
|
||||
attachment.setFileName(originalFileName);
|
||||
attachment.setFileType(fileType); // 修复后的MIME类型
|
||||
attachment.setFileExt(fileExt);
|
||||
attachment.setFileMd5(md5);
|
||||
attachment.setDownloadCount(0);
|
||||
attachment.setIsCompressed(isCompressedFile(originalFileName) ? "1" : "0");
|
||||
attachment.setIsEncrypted("N");
|
||||
attachment.setDownloadStartTime(new Date(attachDownloadStartTime)); // 附件下载开始时间
|
||||
attachment.setDownloadEndTime(new Date(attachDownloadEndTime)); // 附件下载结束时间
|
||||
attachment.setDownloadCostTime(costTime); // 附件下载耗时(毫秒)
|
||||
|
||||
return attachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否有显式附件
|
||||
*/
|
||||
private static boolean hasExplicitAttachment(Message message) throws Exception {
|
||||
if (!message.isMimeType("multipart/*")) {
|
||||
return false;
|
||||
}
|
||||
MimeMultipart multipart = (MimeMultipart) message.getContent();
|
||||
for (int i = 0; i < multipart.getCount(); i++) {
|
||||
if (Part.ATTACHMENT.equalsIgnoreCase(multipart.getBodyPart(i).getDisposition())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ------------------------ 工具方法 ------------------------
|
||||
private static String getMessageId(Message message) throws MessagingException {
|
||||
String[] ids = message.getHeader("Message-ID");
|
||||
if (ids != null && ids.length > 0) {
|
||||
return ids[0].replace("<", "").replace(">", "").trim();
|
||||
}
|
||||
return UUID.randomUUID().toString() + "@" + message.getSession().getProperty("mail.imap.host");
|
||||
}
|
||||
|
||||
private static String convertAddresses(Address[] addresses) throws Exception {
|
||||
if (addresses == null || addresses.length == 0) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Address addr : addresses) {
|
||||
sb.append(decodeMimeText(addr.toString())).append(",");
|
||||
}
|
||||
return !sb.isEmpty() ? sb.substring(0, sb.length() - 1) : "";
|
||||
}
|
||||
|
||||
private static String getFileExtension(String fileName) {
|
||||
if (fileName == null || !fileName.contains(".")) {
|
||||
return "";
|
||||
}
|
||||
return fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
|
||||
}
|
||||
|
||||
private static boolean isCompressedFile(String fileName) {
|
||||
Set<String> exts = new HashSet<>(Arrays.asList("zip", "rar", "7z", "tar", "gz", "bz2"));
|
||||
return exts.contains(getFileExtension(fileName));
|
||||
}
|
||||
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
char[] hex = new char[bytes.length * 2];
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
int v = bytes[i] & 0xFF;
|
||||
hex[i * 2] = Character.forDigit(v >>> 4, 16);
|
||||
hex[i * 2 + 1] = Character.forDigit(v & 0x0F, 16);
|
||||
}
|
||||
return new String(hex).toLowerCase();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user