This commit is contained in:
oneao committed 2025-07-10 17:29:30 +08:00
1 parent a96f04630e
commit 7837b87959
27 files changed
+508 -894

No files matched your search

@@ -1,6 +1,46 @@
package com.email;
import com.email.core.fetch.EmailFetcher;
import com.email.core.fetch.ImapJavaMailEmailFetcher;
import com.email.domain.entity.UserAccount;
import javax.mail.*;
import javax.mail.Flags;
import javax.mail.search.FlagTerm;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.io.*;
import java.util.Properties;
public class Test {
public static void main(String[] args) throws Exception {
UserAccount userAccount = new UserAccount();
userAccount.setEmail("717406575@qq.com");
userAccount.setPassword("dhqesrwzgblrbcac");
userAccount.setImapHost("imap.qq.com");
userAccount.setImapPort("993");
EmailFetcher emailFetcher = new ImapJavaMailEmailFetcher();
emailFetcher.fetchAll(
userAccount,
// progressConsumer: 打印进度
progress -> System.out.printf("📩 [%s] 已拉取 %d / %d 封邮件%n",
progress.getFolderName(),
progress.getCurrent(),
progress.getTotal()
),
// folderConsumer: 开始处理某个文件夹
folder -> {
},
// fetchDataConsumer: 每封邮件拉取完成后处理
emailData -> {
}
);
}
}
@@ -35,7 +35,7 @@ public class EmailImapClient {
props.put("mail.imaps.partialfetch", "false");
// 在连接参数中强制指定ID信息
// 在连接参数中强制指定ID信息
props.put("mail.imap.id.mechanism", "OAUTH2");
props.put("mail.imap.id.name", "your_client_name");
props.put("mail.imap.id.version", "1.0.0");
@@ -0,0 +1,36 @@
package com.email.client;
import com.email.domain.entity.UserAccount;
import javax.mail.Session;
import java.util.HashMap;
import java.util.Properties;
public class EmailImapClient2 {
private UserAccount userAccount;
public void connect(){
Properties props = new Properties();
props.put("mail.store.protocol", "imap");
props.put("mail.imap.host", userAccount.getImapHost());
props.put("mail.imap.port", userAccount.getImapPort());
props.put("mail.imap.ssl.enable", "true");
props.put("mail.imap.starttls.enable", "true");
props.put("mail.imap.partialfetch", "false");
props.put("mail.imaps.partialfetch", "false");
// 在连接参数中强制指定ID信息
props.put("mail.imap.id.mechanism", "OAUTH2");
props.put("mail.imap.id.name", userAccount.getDisplayName());
props.put("mail.imap.id.version", "1.0.0");
props.put("mail.imap.fetchsize", "4194304");
HashMap<String, String> imapId = new HashMap<>();
imapId.put("name", String.valueOf(userAccount.getId()));
imapId.put("version", "1.0.0");
imapId.put("vendor", "vendor");
imapId.put("support-email", userAccount.getEmail());
Session.getInstance(props);
}
}
@@ -1,36 +0,0 @@
package com.email.client;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class ImapUtf7 {
public static String decode(String encoded) {
if (encoded == null) return null;
StringBuilder sb = new StringBuilder();
int i = 0;
while (i < encoded.length()) {
char ch = encoded.charAt(i);
if (ch == '&') {
int start = ++i;
while (i < encoded.length() && encoded.charAt(i) != '-') {
i++;
}
if (i == start) {
sb.append('&');
i++; // 跳过 '-'
} else {
String base64 = encoded.substring(start, i).replace(',', '/');
byte[] utf16Bytes = Base64.getDecoder().decode(base64);
sb.append(new String(utf16Bytes, StandardCharsets.UTF_16BE));
i++; // 跳过 '-'
}
} else {
sb.append(ch);
i++;
}
}
return sb.toString();
}
}
@@ -0,0 +1,15 @@
package com.email.core.fetch;
import com.email.core.fetch.model.EmailFetchData;
import com.email.core.fetch.model.EmailFetchProgress;
import com.email.domain.entity.EmailFolder;
import com.email.domain.entity.UserAccount;
import java.util.function.Consumer;
public interface EmailFetcher {
void fetchAll(UserAccount userAccount,
Consumer<EmailFetchProgress> emailFetchProgressConsumer,
Consumer<EmailFolder> emailFolderConsumer,
Consumer<EmailFetchData> emailFetchDataConsumer);
}
@@ -0,0 +1,4 @@
package com.email.core.fetch;
public class ImapCommandEmailFetcher {
}
@@ -0,0 +1,163 @@
package com.email.core.fetch;
import com.email.constants.ResponseStatusConstants;
import com.email.core.fetch.model.EmailFetchData;
import com.email.core.fetch.model.EmailFetchProgress;
import com.email.domain.entity.EmailFolder;
import com.email.domain.entity.UserAccount;
import com.email.enums.ResponseEnum;
import com.email.exception.auth.AuthException;
import com.sun.javaws.progress.Progress;
import com.sun.mail.imap.IMAPFolder;
import lombok.extern.slf4j.Slf4j;
import javax.mail.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
@Slf4j
public class ImapJavaMailEmailFetcher implements EmailFetcher {
private final ExecutorService folderExecutor = Executors.newFixedThreadPool(10);
@Override
public void fetchAll(UserAccount userAccount, Consumer<EmailFetchProgress> progressConsumer, Consumer<EmailFolder> folderConsumer, Consumer<EmailFetchData> fetchDataConsumer) {
Store store = null;
try {
store = connect(userAccount); // connect 内部已处理异常
List<Folder> allFolders = listAllFolders(store.getDefaultFolder());
CountDownLatch folderLatch = new CountDownLatch(allFolders.size());
for (Folder folder : allFolders) {
folderExecutor.submit(() -> {
try {
processFolder(folder, progressConsumer, folderConsumer, fetchDataConsumer);
} catch (Exception e) {
log.error("处理文件夹 [{}] 失败:{}", folder.getFullName(), e.getMessage(), e);
} finally {
folderLatch.countDown();
}
});
}
folderLatch.await(); // 等待所有文件夹处理完
} catch (Exception e) {
throw new RuntimeException("抓取邮件失败:" + e.getMessage(), e);
} finally {
if (store != null && store.isConnected()) {
try {
store.close();
} catch (MessagingException closeEx) {
log.warn("关闭邮箱连接时异常:{}", closeEx.getMessage());
}
}
}
}
// 递归获取所有文件夹(包括子文件夹)
private List<Folder> listAllFolders(Folder folder) throws MessagingException {
List<Folder> allFolders = new ArrayList<>();
if ((folder.getType() & Folder.HOLDS_MESSAGES) != 0) {
allFolders.add(folder);
}
if ((folder.getType() & Folder.HOLDS_FOLDERS) != 0) {
Folder[] subFolders = folder.list();
for (Folder sub : subFolders) {
allFolders.addAll(listAllFolders(sub));
}
}
return allFolders;
}
private void processFolder(Folder folder,
Consumer<EmailFetchProgress> progressConsumer,
Consumer<EmailFolder> folderConsumer,
Consumer<EmailFetchData> fetchDataConsumer) throws Exception {
if (!(folder instanceof IMAPFolder)) return;
folder.open(Folder.READ_ONLY);
int totalMessages = folder.getMessageCount();
if (totalMessages == 0) {
folder.close(false);
return;
}
EmailFolder emailFolder = new EmailFolder();
folderConsumer.accept(emailFolder);
FetchProfile fetchProfile = new FetchProfile();
fetchProfile.add(FetchProfile.Item.ENVELOPE);
fetchProfile.add(FetchProfile.Item.CONTENT_INFO);
Message[] messages = folder.getMessages();
folder.fetch(messages, fetchProfile);
for (int i = 0; i < messages.length; i++) {
Message message = messages[i];
try {
progressConsumer.accept(new EmailFetchProgress(folder.getFullName(), i + 1, totalMessages));
} catch (Exception e) {
log.warn("处理邮件失败 folder={} index={} error={}", folder.getFullName(), i, e.getMessage(), e);
}
}
folder.close(false);
}
private Store connect(UserAccount account) {
Properties props = new Properties();
props.put("mail.store.protocol", "imaps");
props.put("mail.imap.ssl.enable", "true");
props.put("mail.imap.connectiontimeout", "10000");
props.put("mail.imap.timeout", "10000");
try {
Session session = Session.getInstance(props);
Store store = session.getStore("imaps");
store.connect(
account.getImapHost(),
Integer.parseInt(account.getImapPort()),
account.getEmail(),
account.getPassword()
);
return store;
} catch (AuthenticationFailedException e) {
log.warn("邮箱认证失败:账号={},原因={}", account.getEmail(), e.getMessage(), e);
throw new AuthException(ResponseEnum.AUTH_EMAIL_INVALID_CREDENTIALS);
} catch (NoSuchProviderException e) {
log.warn("邮箱协议不支持:账号={},原因={}", account.getEmail(), e.getMessage(), e);
throw new AuthException(ResponseEnum.AUTH_EMAIL_PROTOCOL_UNSUPPORTED);
} catch (MessagingException e) {
log.warn("邮箱服务器连接失败:账号={},Host={},Port={},SSL={},原因={}",
account.getEmail(),
account.getImapHost(),
account.getImapPort(),
account.getImapSsl(),
e.getMessage(),
e
);
throw new AuthException(ResponseEnum.AUTH_EMAIL_CONNECTION_FAILED);
} catch (Exception e) {
log.error("邮箱连接发生未知错误:账号={},原因={}", account.getEmail(), e.getMessage(), e);
throw new AuthException(ResponseEnum.AUTH_EMAIL_UNKNOWN_ERROR);
}
}
}
@@ -0,0 +1,19 @@
package com.email.core.fetch.model;
import com.email.domain.entity.EmailAddress;
import com.email.domain.entity.EmailFile;
import com.email.domain.entity.EmailSummary;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EmailFetchData {
private EmailSummary emailSummary;
private List<EmailAddress> emailAddressList;
private List<EmailFile> emailFileList;
}
@@ -0,0 +1,15 @@
package com.email.core.fetch.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class EmailFetchProgress {
private String folderName; // 文件夹名称
private Integer current; // 当前页
private Integer total; // 总数
}
@@ -0,0 +1,4 @@
package com.email.core.send;
public interface EmailSender {
}
@@ -0,0 +1,4 @@
package com.email.core.send;
public class SmtpCommandEmailSender {
}
@@ -0,0 +1,4 @@
package com.email.core.send;
public class SmtpJavaMailEmailSender {
}
@@ -19,7 +19,7 @@ public class EmailSentDTO {
private String body; // 邮件正文内容
private String from; // 发件人
private String fromName; // 发件人
private String fromName; // 发件人名称
private Integer isSingSent; // 是否群发单显(1 是,0 否)
private Integer isHtml;
@@ -15,14 +15,14 @@ import lombok.experimental.Accessors;
@NoArgsConstructor
@TableName("email_file")
@Accessors(chain = true)
public class EmailFile {
public class EmailFile {
@TableId
@JsonSerialize(using = ToStringSerializer.class)
private Long id; // 主键ID
private String fileName; // 文件名(不含路径)
private String fileExtension; // 文件扩展名,例如:jpg、pdf
private Long fileSize; // 文件大小,单位字节
private String filePath; // 文件路径(建议存相对路径)
private String filePath; // 文件路径(相对路径)
private Integer fileType; // 文件类型:0-临时,1-附件,2-内嵌资源,3-模板等
private Long emailId; // 关联的邮件ID
@@ -31,4 +31,3 @@ public class EmailFile {
@TableField(exist = false)
private String fileUrl;
}
@@ -18,7 +18,13 @@ public enum ResponseEnum {
AUTH_SMTP_PROTOCOL_ERROR(ResponseStatusConstants.UNAUTHORIZED, "SMTP 协议不支持或配置错误"),
AUTH_IMAP_PROTOCOL_ERROR(ResponseStatusConstants.UNAUTHORIZED, "IMAP 协议不支持或配置错误"),
AUTH_SERVER_CONNECTION_FAILED(ResponseStatusConstants.UNAUTHORIZED, "邮箱服务器连接失败,请检查是否已经开通 IMAP/SMTP 协议"),
AUTH_UNKNOWN_ERROR(ResponseStatusConstants.UNAUTHORIZED,"邮箱服务器连接失败,请重新登录");
AUTH_UNKNOWN_ERROR(ResponseStatusConstants.UNAUTHORIZED,"邮箱服务器连接失败,请重新登录"),
AUTH_EMAIL_INVALID_CREDENTIALS(ResponseStatusConstants.UNAUTHORIZED,"邮箱账户或密码错误"),
AUTH_EMAIL_PROTOCOL_UNSUPPORTED(ResponseStatusConstants.UNAUTHORIZED,"邮箱协议不支持"),
AUTH_EMAIL_CONNECTION_FAILED(ResponseStatusConstants.UNAUTHORIZED,"邮箱服务器连接失败,请检查配置"),
AUTH_EMAIL_UNKNOWN_ERROR(ResponseStatusConstants.UNAUTHORIZED,"连接邮箱时发生未知错误");
private final Integer code; //响应码
private final String message; //相应信息