u
This commit is contained in:
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);
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.email.core.fetch;
|
||||
|
||||
public class ImapCommandEmailFetcher {
|
||||
}
|
||||
+163
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+19
@@ -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;
|
||||
}
|
||||
+15
@@ -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 {
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.email.core.send;
|
||||
|
||||
public class SmtpCommandEmailSender {
|
||||
}
|
||||
+4
@@ -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; //相应信息
|
||||
|
||||
Reference in new issue
Block a user