u
This commit is contained in:
1 parent
6c057cbdca
commit
34deecb157
3 files changed
+530
-58
No files matched your search
@@ -2,6 +2,7 @@ package com.email;
|
|||||||
|
|
||||||
import com.email.core.fetch.EmailFetcher;
|
import com.email.core.fetch.EmailFetcher;
|
||||||
import com.email.core.fetch.ImapJavaMailEmailFetcher;
|
import com.email.core.fetch.ImapJavaMailEmailFetcher;
|
||||||
|
import com.email.core.fetch.model.EmailFetchData;
|
||||||
import com.email.domain.entity.UserAccount;
|
import com.email.domain.entity.UserAccount;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@@ -31,9 +32,13 @@ public class Test {
|
|||||||
},
|
},
|
||||||
// folderConsumer: 开始处理某个文件夹
|
// folderConsumer: 开始处理某个文件夹
|
||||||
folder -> {
|
folder -> {
|
||||||
|
log.info("folder:{}",folder.getName());
|
||||||
},
|
},
|
||||||
// fetchDataConsumer: 每封邮件拉取完成后处理
|
// fetchDataConsumer: 每封邮件拉取完成后处理
|
||||||
emailData -> {
|
emailData -> {
|
||||||
|
for (EmailFetchData emailDatum : emailData) {
|
||||||
|
log.info("subject:{}",emailDatum.getEmailSummary().getSubject());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
package com.email.core;
|
||||||
|
|
||||||
|
import com.email.prop.EmailStorageProperties;
|
||||||
|
import com.email.utils.entity.EmailUploadResult;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
public class EmailFileUtils {
|
||||||
|
private static final ExecutorService ATTACHMENT_EXECUTOR =
|
||||||
|
Executors.newFixedThreadPool(4); // 或使用 ThreadPoolExecutor 自定义
|
||||||
|
|
||||||
|
private static String LOCAL_BASE_PATH; // e.g. D:/data/email
|
||||||
|
private static String LOCAL_SERVER_URL; // e.g. http://127.0.0.1:8081
|
||||||
|
private static String LOCAL_STATIC_URL; // e.g. /static/email
|
||||||
|
|
||||||
|
private static final String BODY_PREFIX = "Body"; // 正文
|
||||||
|
private static final String ATTACHMENT_PREFIX = "Attachment"; // 附件以及内联资源
|
||||||
|
private static final String TEMPLATE_PREFIX = "Template"; // 模板
|
||||||
|
|
||||||
|
public static void init(EmailStorageProperties properties) {
|
||||||
|
LOCAL_BASE_PATH = properties.getBasePath();
|
||||||
|
LOCAL_SERVER_URL = properties.getServerUrl();
|
||||||
|
LOCAL_STATIC_URL = properties.getStaticPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化邮件在存储时候的格式,将 @ 格式化为 _
|
||||||
|
* @param email 邮件地址
|
||||||
|
* @return 格式化后的邮件地址
|
||||||
|
*/
|
||||||
|
private static String normalizeEmail(String email) {
|
||||||
|
if (email == null) return "unknown";
|
||||||
|
return email.replace("@", "_").replace(".", "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过文件名称后去文件的后缀
|
||||||
|
* @param fileName 文件名称
|
||||||
|
* @return 文件后缀
|
||||||
|
*/
|
||||||
|
private static String getFileExtension(String fileName) {
|
||||||
|
int idx = fileName.lastIndexOf('.');
|
||||||
|
return (idx != -1 && idx < fileName.length() - 1) ? fileName.substring(idx + 1) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存邮件内容
|
||||||
|
* @param email 邮件地址
|
||||||
|
* @param bodyContent 邮件内容
|
||||||
|
* @param charset 字符集
|
||||||
|
* @return 保存后的信息
|
||||||
|
*/
|
||||||
|
public static EmailUploadResult uploadBody(String email, String bodyContent, String charset) {
|
||||||
|
if (bodyContent == null) {
|
||||||
|
bodyContent = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream inputStream = new ByteArrayInputStream(
|
||||||
|
bodyContent.getBytes(java.nio.charset.Charset.forName(charset))
|
||||||
|
);
|
||||||
|
|
||||||
|
return doUpload(email, "body.html", inputStream, BODY_PREFIX, "html");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存邮件附件
|
||||||
|
* @param email 邮件地址
|
||||||
|
* @param originalFileName 附件名称
|
||||||
|
* @param inputStream 文件流
|
||||||
|
* @return 保存后的信息
|
||||||
|
*/
|
||||||
|
public static EmailUploadResult uploadAttachment(String email, String originalFileName, InputStream inputStream) {
|
||||||
|
return doUpload(email, originalFileName, inputStream, ATTACHMENT_PREFIX, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EmailUploadResult doUpload(String email, String originalFileName, InputStream inputStream,
|
||||||
|
String typePrefix, String forcedExtension) {
|
||||||
|
try {
|
||||||
|
LocalDate today = LocalDate.now();
|
||||||
|
String datePath = today.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
|
||||||
|
String emailPath = normalizeEmail(email);
|
||||||
|
|
||||||
|
String extension = forcedExtension;
|
||||||
|
if (StringUtils.isEmpty(forcedExtension)) {
|
||||||
|
extension = getFileExtension(originalFileName);
|
||||||
|
if(StringUtils.isEmpty(extension)){
|
||||||
|
extension = "dat";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String fileName = System.currentTimeMillis() + "" + ThreadLocalRandom.current().nextInt(100,1000) + "." + extension;
|
||||||
|
String relativePath = joinFilePath(emailPath,typePrefix,datePath,fileName);
|
||||||
|
String fullPathStr = joinFilePath(LOCAL_BASE_PATH, relativePath);
|
||||||
|
Path fullPath = Paths.get(fullPathStr);
|
||||||
|
|
||||||
|
Path parentDir = fullPath.getParent();
|
||||||
|
if (parentDir != null && !Files.exists(parentDir)) {
|
||||||
|
Files.createDirectories(parentDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.copy(inputStream, fullPath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
|
||||||
|
return new EmailUploadResult(
|
||||||
|
originalFileName,
|
||||||
|
extension,
|
||||||
|
Files.size(fullPath),
|
||||||
|
relativePath.replace("\\", "/")
|
||||||
|
);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("文件上传失败,路径:" + LOCAL_BASE_PATH + ",错误:" + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建本地
|
||||||
|
* @param relativePath 717406575_qq_com/Attachment/2025/06/03/1748927695653847.jpeg
|
||||||
|
* @return D:/data/email/717406575_qq_com/Attachment/2025/06/03/1748927695653847.jpeg
|
||||||
|
*/
|
||||||
|
public static String buildLocalFilePath(String relativePath) {
|
||||||
|
return joinFilePath(LOCAL_BASE_PATH, relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String buildPublicFileUrl(String relativePath) {
|
||||||
|
return joinUrl(LOCAL_SERVER_URL, LOCAL_STATIC_URL, relativePath).replace("\\", "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拼接文件路径,自动忽略空路径,并进行路径标准化。
|
||||||
|
* 等价于 Paths.get(...).normalize().toString(),但支持多个参数并自动忽略空值。
|
||||||
|
* 示例:
|
||||||
|
* joinFilePath("D:/data", "email", "Body") => D:\data\email\Body
|
||||||
|
*
|
||||||
|
* @param paths 路径片段
|
||||||
|
* @return 合并后的标准路径字符串
|
||||||
|
*/
|
||||||
|
private static String joinFilePath(String... paths) {
|
||||||
|
if (paths == null || paths.length == 0) return "";
|
||||||
|
|
||||||
|
Path result = null;
|
||||||
|
|
||||||
|
for (String p : paths) {
|
||||||
|
if (p == null || p.trim().isEmpty()) continue; // 跳过空路径
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
result = Paths.get(p.trim()); // 初始化路径
|
||||||
|
} else {
|
||||||
|
result = result.resolve(p.trim()); // 追加路径
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (result == null) ? "" : result.normalize().toString(); // 返回标准化路径
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拼接 URL 路径,自动处理斜杠,确保不会出现重复或缺失的斜杠。
|
||||||
|
* 示例:
|
||||||
|
* joinUrl("http://localhost:8080/", "/static/email/", "/img.png") => http://localhost:8080/static/email/img.png
|
||||||
|
*
|
||||||
|
* @param parts URL 各部分片段
|
||||||
|
* @return 合并后的完整 URL 字符串
|
||||||
|
*/
|
||||||
|
private static String joinUrl(String... parts) {
|
||||||
|
if (parts == null || parts.length == 0) return "";
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
for (int i = 0; i < parts.length; i++) {
|
||||||
|
String part = parts[i] == null ? "" : parts[i].trim();
|
||||||
|
if (part.isEmpty()) continue;
|
||||||
|
|
||||||
|
if (i == 0) {
|
||||||
|
// 第一个部分:去除尾部斜杠
|
||||||
|
sb.append(part.replaceAll("/+$", ""));
|
||||||
|
} else {
|
||||||
|
sb.append("/");
|
||||||
|
// 中间和结尾部分:去除前后多余的斜杠
|
||||||
|
sb.append(part.replaceAll("^/+", "").replaceAll("/+$", ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
+329
-58
@@ -2,24 +2,35 @@ package com.email.core.fetch;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||||
import com.email.constants.ResponseStatusConstants;
|
import com.email.constants.ResponseStatusConstants;
|
||||||
|
import com.email.core.EmailFileUtils;
|
||||||
import com.email.core.fetch.model.EmailFetchData;
|
import com.email.core.fetch.model.EmailFetchData;
|
||||||
import com.email.core.fetch.model.EmailFetchProgress;
|
import com.email.core.fetch.model.EmailFetchProgress;
|
||||||
import com.email.core.fetch.model.FolderWrapper;
|
import com.email.core.fetch.model.FolderWrapper;
|
||||||
import com.email.domain.entity.*;
|
import com.email.domain.entity.*;
|
||||||
import com.email.enums.ResponseEnum;
|
import com.email.enums.ResponseEnum;
|
||||||
|
import com.email.enums.email.EmailAddressTypeEnum;
|
||||||
import com.email.enums.email.EmailBodyTypeEnum;
|
import com.email.enums.email.EmailBodyTypeEnum;
|
||||||
|
import com.email.enums.email.EmailFileTypeEnum;
|
||||||
import com.email.enums.email.EmailFolderTypeEnum;
|
import com.email.enums.email.EmailFolderTypeEnum;
|
||||||
import com.email.exception.auth.AuthException;
|
import com.email.exception.auth.AuthException;
|
||||||
|
import com.email.utils.entity.EmailUploadResult;
|
||||||
import com.sun.javaws.progress.Progress;
|
import com.sun.javaws.progress.Progress;
|
||||||
import com.sun.mail.imap.IMAPFolder;
|
import com.sun.mail.imap.IMAPFolder;
|
||||||
import com.sun.mail.imap.IMAPStore;
|
import com.sun.mail.imap.IMAPStore;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.util.IdGenerator;
|
import org.springframework.util.IdGenerator;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
|
|
||||||
import javax.mail.*;
|
import javax.mail.*;
|
||||||
|
import javax.mail.internet.InternetAddress;
|
||||||
import javax.mail.internet.MailDateFormat;
|
import javax.mail.internet.MailDateFormat;
|
||||||
import javax.mail.internet.MimeMessage;
|
import javax.mail.internet.MimeMessage;
|
||||||
|
import javax.mail.internet.MimeUtility;
|
||||||
|
import javax.validation.constraints.Email;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
@@ -70,7 +81,7 @@ public class ImapJavaMailEmailFetcher implements EmailFetcher {
|
|||||||
try {
|
try {
|
||||||
threadStore = connect(userAccount);
|
threadStore = connect(userAccount);
|
||||||
|
|
||||||
processSingleFolder(threadStore, emailFolder, progressConsumer, folderConsumer, fetchDataConsumer);
|
processSingleFolder(emailFolder,userAccount, threadStore, progressConsumer, folderConsumer, fetchDataConsumer);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("处理文件夹 [{}] 异常:{}", emailFolder.getFullName(), e.getMessage(), e);
|
log.error("处理文件夹 [{}] 异常:{}", emailFolder.getFullName(), e.getMessage(), e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -89,11 +100,13 @@ public class ImapJavaMailEmailFetcher implements EmailFetcher {
|
|||||||
folderLatch.await();
|
folderLatch.await();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processSingleFolder(Store store,
|
private void processSingleFolder(
|
||||||
EmailFolder emailFolder,
|
EmailFolder emailFolder,
|
||||||
|
UserAccount userAccount,
|
||||||
|
Store store,
|
||||||
Consumer<EmailFetchProgress> progressConsumer,
|
Consumer<EmailFetchProgress> progressConsumer,
|
||||||
Consumer<EmailFolder> folderConsumer,
|
Consumer<EmailFolder> folderConsumer,
|
||||||
Consumer<List<EmailFetchData>> fetchDataConsumer) {
|
Consumer<List<EmailFetchData>> emailConsumer) {
|
||||||
IMAPFolder imapFolder = null;
|
IMAPFolder imapFolder = null;
|
||||||
try {
|
try {
|
||||||
imapFolder = (IMAPFolder) store.getFolder(emailFolder.getFullName());
|
imapFolder = (IMAPFolder) store.getFolder(emailFolder.getFullName());
|
||||||
@@ -105,10 +118,13 @@ public class ImapJavaMailEmailFetcher implements EmailFetcher {
|
|||||||
|
|
||||||
int totalMessages = imapFolder.getMessageCount();
|
int totalMessages = imapFolder.getMessageCount();
|
||||||
if (totalMessages > 0) {
|
if (totalMessages > 0) {
|
||||||
processEmailsInFolder(imapFolder,emailFolder);
|
try {
|
||||||
|
processEmailsInFolder(emailFolder,userAccount,imapFolder,emailConsumer);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (MessagingException e) {
|
} catch (MessagingException e) {
|
||||||
log.error("打开或处理文件夹 [{}] 失败: {}", emailFolder.getFullName(), e.getMessage(), e);
|
log.error("打开或处理文件夹 [{}] 失败: {}", emailFolder.getFullName(), e.getMessage(), e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -122,63 +138,163 @@ public class ImapJavaMailEmailFetcher implements EmailFetcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processEmailsInFolder(IMAPFolder imapFolder,EmailFolder emailFolder) throws MessagingException {
|
private void processEmailsInFolder(EmailFolder emailFolder,
|
||||||
|
UserAccount userAccount,
|
||||||
|
IMAPFolder imapFolder,
|
||||||
|
Consumer<List<EmailFetchData>> emailConsumer) throws Exception {
|
||||||
if (!imapFolder.isOpen()) {
|
if (!imapFolder.isOpen()) {
|
||||||
imapFolder.open(Folder.READ_ONLY);
|
imapFolder.open(Folder.READ_ONLY);
|
||||||
}
|
}
|
||||||
|
|
||||||
Message[] messages = imapFolder.getMessages();
|
Message[] messages = imapFolder.getMessages();
|
||||||
if (messages == null || messages.length == 0) {
|
if (messages == null || messages.length == 0) return;
|
||||||
log.info("文件夹 [{}] 没有邮件,跳过", imapFolder.getFullName());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Message> messageList = Arrays.asList(messages);
|
// ✅ 可选:预拉字段避免 getSubject 慢
|
||||||
int totalMessages = messageList.size();
|
FetchProfile fetchProfile = new FetchProfile();
|
||||||
log.info("文件夹 [{}] 邮件总数: {}", imapFolder.getFullName(), totalMessages);
|
fetchProfile.add(FetchProfile.Item.ENVELOPE);
|
||||||
|
fetchProfile.add(FetchProfile.Item.FLAGS);
|
||||||
|
fetchProfile.add(FetchProfile.Item.CONTENT_INFO);
|
||||||
|
imapFolder.fetch(messages, fetchProfile);
|
||||||
|
|
||||||
int batchCount = (totalMessages + BATCH_SIZE - 1) / BATCH_SIZE;
|
CountDownLatch latch = new CountDownLatch(messages.length);
|
||||||
CountDownLatch latch = new CountDownLatch(batchCount);
|
|
||||||
|
|
||||||
for (int i = 0; i < totalMessages; i += BATCH_SIZE) {
|
|
||||||
int end = Math.min(i + BATCH_SIZE, totalMessages);
|
|
||||||
List<Message> batchMessages = messageList.subList(i, end);
|
|
||||||
|
|
||||||
|
for (Message message : messages) {
|
||||||
emailExecutor.submit(() -> {
|
emailExecutor.submit(() -> {
|
||||||
try {
|
try {
|
||||||
for (Message message : batchMessages) {
|
EmailFetchData data = processSingleEmail(imapFolder, message, emailFolder.getId(), userAccount.getEmail());
|
||||||
|
log.info("subject:{}",data.getEmailSummary().getSubject());
|
||||||
}
|
// 可选:收集并统一处理结果(如使用 synchronizedList)
|
||||||
|
// emailConsumer.accept(Collections.singletonList(data));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("批量任务异常", e);
|
log.error("处理邮件异常: {}", e.getMessage(), e);
|
||||||
} finally {
|
} finally {
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
latch.await(); // 等待所有任务完成
|
||||||
latch.await();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
// if (!imapFolder.isOpen()) {
|
||||||
imapFolder.close(false);
|
// imapFolder.open(Folder.READ_ONLY);
|
||||||
} catch (MessagingException e) {
|
// }
|
||||||
log.warn("关闭文件夹异常", e);
|
//
|
||||||
}
|
// Message[] messages = imapFolder.getMessages();
|
||||||
|
// for (Message message : messages) {
|
||||||
|
// EmailFetchData data = processSingleEmail(imapFolder, message, emailFolder.getId(), userAccount.getEmail());
|
||||||
|
// System.out.println(data.getEmailSummary().getSubject());
|
||||||
|
// }
|
||||||
|
|
||||||
|
//
|
||||||
|
// long uidNext = imapFolder.getUIDNext(); // 下一封邮件 UID(不包含)
|
||||||
|
// long minUid = findMinUid(imapFolder); // 当前文件夹最小 UID
|
||||||
|
//
|
||||||
|
// if (minUid >= uidNext) {
|
||||||
|
// log.info("📭 文件夹 {} 无可拉取邮件", emailFolder.getFullName());
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// List<long[]> uidRanges = new ArrayList<>();
|
||||||
|
// for (long start = minUid; start < uidNext; start += BATCH_SIZE) {
|
||||||
|
// long end = Math.min(start + BATCH_SIZE - 1, uidNext - 1);
|
||||||
|
// uidRanges.add(new long[]{start, end});
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// CountDownLatch latch = new CountDownLatch(uidRanges.size());
|
||||||
|
//
|
||||||
|
// for (long[] range : uidRanges) {
|
||||||
|
// long startUid = range[0];
|
||||||
|
// long endUid = range[1];
|
||||||
|
//
|
||||||
|
// emailExecutor.submit(() -> {
|
||||||
|
// Store threadStore = null;
|
||||||
|
// IMAPFolder threadFolder = null;
|
||||||
|
// try {
|
||||||
|
// threadStore = connect(userAccount); // 每线程自己的连接
|
||||||
|
// threadFolder = (IMAPFolder) threadStore.getFolder(emailFolder.getFullName());
|
||||||
|
// threadFolder.open(Folder.READ_ONLY);
|
||||||
|
//
|
||||||
|
// Message[] messages = threadFolder.getMessagesByUID(startUid, endUid);
|
||||||
|
// if (messages == null || messages.length == 0) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // ✅ 预拉字段,避免后续每个字段都触发 IO
|
||||||
|
// FetchProfile fp = new FetchProfile();
|
||||||
|
// fp.add(FetchProfile.Item.ENVELOPE);
|
||||||
|
// fp.add(FetchProfile.Item.FLAGS);
|
||||||
|
// fp.add(FetchProfile.Item.CONTENT_INFO);
|
||||||
|
// threadFolder.fetch(messages, fp);
|
||||||
|
//
|
||||||
|
// List<EmailFetchData> fetchDataList = new ArrayList<>(messages.length);
|
||||||
|
// for (Message message : messages) {
|
||||||
|
// try {
|
||||||
|
// EmailFetchData data = processSingleEmail(threadFolder, message, emailFolder.getId(), userAccount.getEmail());
|
||||||
|
// fetchDataList.add(data);
|
||||||
|
// } catch (Exception e) {
|
||||||
|
// log.error("处理邮件 UID={} 异常: {}", threadFolder.getUID(message), e.getMessage(), e);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (!fetchDataList.isEmpty()) {
|
||||||
|
// emailConsumer.accept(fetchDataList);
|
||||||
|
// }
|
||||||
|
// } catch (Exception e) {
|
||||||
|
// log.error("线程拉取 UID [{}-{}] 邮件异常: {}", startUid, endUid, e.getMessage(), e);
|
||||||
|
// } finally {
|
||||||
|
// try {
|
||||||
|
// if (threadFolder != null && threadFolder.isOpen()) threadFolder.close(false);
|
||||||
|
// if (threadStore != null && threadStore.isConnected()) threadStore.close();
|
||||||
|
// } catch (MessagingException e) {
|
||||||
|
// log.warn("关闭线程 IMAPFolder 或 Store 异常", e);
|
||||||
|
// }
|
||||||
|
// latch.countDown();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// try {
|
||||||
|
// latch.await();
|
||||||
|
// } catch (InterruptedException e) {
|
||||||
|
// Thread.currentThread().interrupt();
|
||||||
|
// throw new RuntimeException("等待多线程拉取邮件中断", e);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private long findMinUid(IMAPFolder folder) throws MessagingException {
|
||||||
|
Message[] msgs = folder.getMessages();
|
||||||
|
long min = Long.MAX_VALUE;
|
||||||
|
for (Message msg : msgs) {
|
||||||
|
long uid = folder.getUID(msg);
|
||||||
|
if (uid > 0 && uid < min) {
|
||||||
|
min = uid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return min == Long.MAX_VALUE ? 1L : min;
|
||||||
|
}
|
||||||
|
|
||||||
private EmailFetchData processSingleEmail(Message message,EmailFolder emailFolder){
|
private EmailFetchData processSingleEmail(Folder folder,Message message,Long folderId,String userEmail) throws Exception {
|
||||||
EmailFetchData fetchData = new EmailFetchData();
|
EmailFetchData fetchData = new EmailFetchData();
|
||||||
|
EmailSummary emailSummary = processSingleEmailSummary(folder, message, folderId);
|
||||||
|
|
||||||
|
Long emailId = emailSummary.getId();
|
||||||
|
// emailAddress
|
||||||
|
List<EmailAddress> addressList = processSingleEmailAddress(message,emailId);
|
||||||
|
|
||||||
|
// emailAttachments
|
||||||
|
List<EmailFile> fileList = processSingleEmailAttachment(message, emailId, userEmail);
|
||||||
|
|
||||||
|
fetchData.setEmailSummary(emailSummary);
|
||||||
|
fetchData.setEmailAddressList(addressList);
|
||||||
|
fetchData.setEmailFileList(fileList);
|
||||||
|
|
||||||
return fetchData;
|
return fetchData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private EmailSummary processSingleEmailSummary(Folder folder,Message message,Long folderId) throws Exception {
|
private EmailSummary processSingleEmailSummary(Folder folder,Message message,Long folderId) throws Exception {
|
||||||
EmailSummary emailSummary = new EmailSummary();
|
EmailSummary emailSummary = new EmailSummary();
|
||||||
|
// emailId
|
||||||
|
long emailId = IdWorker.getId();
|
||||||
// subject
|
// subject
|
||||||
String subject = message.getSubject();
|
String subject = message.getSubject();
|
||||||
// messageId
|
// messageId
|
||||||
@@ -191,8 +307,79 @@ public class ImapJavaMailEmailFetcher implements EmailFetcher {
|
|||||||
Date sentDate = resolveMailSentDate(message);
|
Date sentDate = resolveMailSentDate(message);
|
||||||
// isSeen
|
// isSeen
|
||||||
Integer isSeen = message.isSet(Flags.Flag.SEEN) ? 1 : 0;
|
Integer isSeen = message.isSet(Flags.Flag.SEEN) ? 1 : 0;
|
||||||
// emailId
|
|
||||||
long emailId = IdWorker.getId();
|
// emailContent
|
||||||
|
EmailSummary emailContent = processSingleEmailContent(message);
|
||||||
|
|
||||||
|
emailSummary.setId(emailId)
|
||||||
|
.setSubject(subject)
|
||||||
|
.setMessageId(messageId)
|
||||||
|
.setImapUid(uid)
|
||||||
|
.setSentTime(sentDate)
|
||||||
|
.setIsSeen(isSeen)
|
||||||
|
.setBodyEncoding(emailContent.getBodyEncoding())
|
||||||
|
.setBodyCharset(emailContent.getBodyCharset())
|
||||||
|
.setBodyType(emailContent.getBodyType())
|
||||||
|
.setBodyPath(emailContent.getBodyPath())
|
||||||
|
.setBodySize(emailContent.getBodySize())
|
||||||
|
.setFolderId(folderId);
|
||||||
|
|
||||||
|
return emailSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理单个邮件的各种地址
|
||||||
|
* @param message api
|
||||||
|
* @param emailId 该邮件的id
|
||||||
|
* @return 该封邮件返回的地址集合
|
||||||
|
* @throws MessagingException 处理异常
|
||||||
|
*/
|
||||||
|
private List<EmailAddress> processSingleEmailAddress(Message message, Long emailId) throws MessagingException {
|
||||||
|
List<EmailAddress> addressList = new ArrayList<>();
|
||||||
|
|
||||||
|
// 发件人
|
||||||
|
addAddresses(addressList, message.getFrom(), EmailAddressTypeEnum.FROM, emailId);
|
||||||
|
|
||||||
|
// 回复地址
|
||||||
|
addAddresses(addressList, message.getReplyTo(), EmailAddressTypeEnum.REPLY_TO, emailId);
|
||||||
|
|
||||||
|
// TO
|
||||||
|
addAddresses(addressList, message.getRecipients(Message.RecipientType.TO), EmailAddressTypeEnum.TO, emailId);
|
||||||
|
|
||||||
|
// CC
|
||||||
|
addAddresses(addressList, message.getRecipients(Message.RecipientType.CC), EmailAddressTypeEnum.CC, emailId);
|
||||||
|
|
||||||
|
// BCC
|
||||||
|
addAddresses(addressList, message.getRecipients(Message.RecipientType.BCC), EmailAddressTypeEnum.BCC, emailId);
|
||||||
|
|
||||||
|
return addressList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAddresses(List<EmailAddress> list, Address[] addresses, EmailAddressTypeEnum type, Long emailId) {
|
||||||
|
if (addresses == null) return;
|
||||||
|
|
||||||
|
for (Address address : addresses) {
|
||||||
|
if (address instanceof InternetAddress) {
|
||||||
|
InternetAddress ia = (InternetAddress) address;
|
||||||
|
String email = ia.getAddress();
|
||||||
|
String name = ia.getPersonal() != null ? ia.getPersonal() : email;
|
||||||
|
|
||||||
|
list.add(new EmailAddress()
|
||||||
|
.setAddress(email)
|
||||||
|
.setAddressName(name)
|
||||||
|
.setEmailId(emailId)
|
||||||
|
.setAddressType(type.getType()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理邮件内容
|
||||||
|
* @param message api
|
||||||
|
* @throws Exception 处理邮件内容发生的异常
|
||||||
|
*/
|
||||||
|
private EmailSummary processSingleEmailContent(Message message) throws Exception {
|
||||||
|
EmailSummary emailSummary = new EmailSummary();
|
||||||
// 解析内容
|
// 解析内容
|
||||||
Part part = message;
|
Part part = message;
|
||||||
// bodyCharset
|
// bodyCharset
|
||||||
@@ -210,34 +397,20 @@ public class ImapJavaMailEmailFetcher implements EmailFetcher {
|
|||||||
: "";
|
: "";
|
||||||
// bodyContent
|
// bodyContent
|
||||||
String bodyContent = resolveMailContent(part);
|
String bodyContent = resolveMailContent(part);
|
||||||
|
// 保存邮件到本地
|
||||||
|
EmailUploadResult emailUploadResult = EmailFileUtils.uploadBody(String.valueOf(emailSummary.getId()), bodyContent, bodyCharset);
|
||||||
|
|
||||||
|
emailSummary.setBodyCharset(bodyContent);
|
||||||
|
emailSummary.setBodyType(bodyType);
|
||||||
|
emailSummary.setBodyEncoding(bodyEncoding);
|
||||||
|
emailSummary.setBodyPath(emailUploadResult.getFilePath());
|
||||||
|
emailSummary.setBodySize(emailUploadResult.getFileSize());
|
||||||
|
|
||||||
return emailSummary;
|
return emailSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<EmailAddress> processSingleEmailAddress(Message message) throws MessagingException {
|
|
||||||
List<EmailAddress> addressList = new ArrayList<>();
|
|
||||||
// 发件人
|
|
||||||
Address[] froms = message.getFrom();
|
|
||||||
// 回复地址
|
|
||||||
Address[] replyTos = message.getReplyTo();
|
|
||||||
// TO
|
|
||||||
Address[] tos = message.getRecipients(Message.RecipientType.TO);
|
|
||||||
// CC
|
|
||||||
Address[] ccs = message.getRecipients(Message.RecipientType.CC);
|
|
||||||
// BCC
|
|
||||||
Address[] bccs = message.getRecipients(Message.RecipientType.BCC);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return addressList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<EmailFile> processSingleEmailFile(){
|
|
||||||
List<EmailFile> fileList = new ArrayList<>();
|
|
||||||
|
|
||||||
return fileList;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveMailContent(Part part) throws Exception {
|
private String resolveMailContent(Part part) throws Exception {
|
||||||
if (part.isMimeType("text/html")) {
|
if (part.isMimeType("text/html")) {
|
||||||
return (String) part.getContent();
|
return (String) part.getContent();
|
||||||
@@ -271,6 +444,104 @@ public class ImapJavaMailEmailFetcher implements EmailFetcher {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理邮件附件/内嵌资源
|
||||||
|
* @param message api
|
||||||
|
* @param emailId 邮件ID
|
||||||
|
* @param userEmail 用户邮箱(保存附件时候需要)
|
||||||
|
* @return 邮件文件集合
|
||||||
|
* @throws MessagingException 异常
|
||||||
|
* @throws IOException 异常
|
||||||
|
*/
|
||||||
|
private List<EmailFile> processSingleEmailAttachment(Message message,Long emailId, String userEmail)
|
||||||
|
throws MessagingException, IOException {
|
||||||
|
List<EmailFile> attachments = new ArrayList<>();
|
||||||
|
MimeMessage mimeMessage = (MimeMessage) message;
|
||||||
|
Object content = mimeMessage.getContent();
|
||||||
|
|
||||||
|
if (content instanceof Multipart) {
|
||||||
|
processMultipart((Multipart) content,attachments,emailId,userEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
return attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processMultipart(Multipart multipart, List<EmailFile> fileList, Long emailId, String userEmail)
|
||||||
|
throws MessagingException, IOException {
|
||||||
|
for (int i = 0; i < multipart.getCount(); i++) {
|
||||||
|
BodyPart part = multipart.getBodyPart(i);
|
||||||
|
Object partContent = part.getContent();
|
||||||
|
|
||||||
|
if (partContent instanceof Multipart) {
|
||||||
|
processMultipart((Multipart) partContent, fileList,emailId,userEmail);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String disposition = part.getDisposition();
|
||||||
|
String contentType = part.getContentType().toLowerCase();
|
||||||
|
String[] cidHeaders = part.getHeader("Content-ID");
|
||||||
|
|
||||||
|
boolean isInline = cidHeaders != null && cidHeaders.length > 0;
|
||||||
|
boolean isAttachment = Part.ATTACHMENT.equalsIgnoreCase(disposition)
|
||||||
|
|| (disposition == null && part.getFileName() != null);
|
||||||
|
|
||||||
|
// 只处理内嵌图片或合法附件
|
||||||
|
if (!(isAttachment || (isInline && contentType.startsWith("image")))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理文件名并解码
|
||||||
|
String fileName = part.getFileName();
|
||||||
|
if (StringUtils.isBlank(fileName)) {
|
||||||
|
if (isInline && contentType.startsWith("image")) {
|
||||||
|
String cid = cidHeaders[0].replaceAll("[<>]", "");
|
||||||
|
fileName = "inline_" + cid + ".png";
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fileName = decodeMimeName(fileName); // 解码文件名
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
// 如果解码失败,继续使用原始文件名
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 替换非法字符(如 < > 等)为下划线或其他合法字符
|
||||||
|
fileName = fileName.replaceAll("[<>:\"/\\|?*]", "_");
|
||||||
|
|
||||||
|
EmailFile emailFile = new EmailFile();
|
||||||
|
emailFile.setEmailId(emailId);
|
||||||
|
|
||||||
|
// 选择上传路径和文件
|
||||||
|
InputStream inputStream = part.getInputStream();
|
||||||
|
EmailUploadResult emailUploadResult = com.email.utils.EmailFileUtils.uploadAttachment(userEmail, fileName, inputStream);
|
||||||
|
|
||||||
|
emailFile.setFileName(emailUploadResult.getFileName());
|
||||||
|
emailFile.setFileExtension(emailUploadResult.getFileExtension());
|
||||||
|
emailFile.setFileSize(emailUploadResult.getFileSize());
|
||||||
|
emailFile.setFilePath(emailUploadResult.getFilePath());
|
||||||
|
|
||||||
|
// 设置 CID(如果是内嵌图片)
|
||||||
|
if (isInline) {
|
||||||
|
String cid = cidHeaders[0].replaceAll("[<>]", "");
|
||||||
|
emailFile.setEmailCid(cid);
|
||||||
|
emailFile.setFileType(EmailFileTypeEnum.INLINE.getCode());
|
||||||
|
}else{
|
||||||
|
emailFile.setEmailCid("");
|
||||||
|
emailFile.setFileType(EmailFileTypeEnum.ATTACHMENT.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
fileList.add(emailFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String decodeMimeName(String encodedName) throws UnsupportedEncodingException {
|
||||||
|
return MimeUtility.decodeText(encodedName);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析邮件的 发送/接受 时间
|
* 解析邮件的 发送/接受 时间
|
||||||
* @param message 消息
|
* @param message 消息
|
||||||
|
|||||||
Reference in new issue
Block a user