初始化前端目录
This commit is contained in:
2
pom.xml
2
pom.xml
@@ -46,7 +46,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springdoc</groupId>
|
<groupId>org.springdoc</groupId>
|
||||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
<version>2.2.0</version>
|
<version>2.7.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -13,4 +13,8 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
|||||||
*/
|
*/
|
||||||
public interface MailAccountService extends IService<MailAccount> {
|
public interface MailAccountService extends IService<MailAccount> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取启用的邮件账户(默认取第一个,可扩展为多账户轮询)
|
||||||
|
*/
|
||||||
|
MailAccount getEnabledAccount();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,4 +13,5 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
|||||||
*/
|
*/
|
||||||
public interface MailReceivedService extends IService<MailReceived> {
|
public interface MailReceivedService extends IService<MailReceived> {
|
||||||
|
|
||||||
|
void receiveUnreadMail();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.mini.capi.biz.service;
|
|||||||
|
|
||||||
import com.mini.capi.biz.domain.MailSent;
|
import com.mini.capi.biz.domain.MailSent;
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@@ -13,4 +14,5 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
|||||||
*/
|
*/
|
||||||
public interface MailSentService extends IService<MailSent> {
|
public interface MailSentService extends IService<MailSent> {
|
||||||
|
|
||||||
|
void sendMail(String[] toAddresses, String[] ccAddresses, String subject, String content, MultipartFile[] attachments);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.mini.capi.biz.service.impl;
|
package com.mini.capi.biz.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.mini.capi.biz.domain.MailAccount;
|
import com.mini.capi.biz.domain.MailAccount;
|
||||||
import com.mini.capi.biz.mapper.MailAccountMapper;
|
import com.mini.capi.biz.mapper.MailAccountMapper;
|
||||||
import com.mini.capi.biz.service.MailAccountService;
|
import com.mini.capi.biz.service.MailAccountService;
|
||||||
@@ -17,4 +18,14 @@ import org.springframework.stereotype.Service;
|
|||||||
@Service
|
@Service
|
||||||
public class MailAccountServiceImpl extends ServiceImpl<MailAccountMapper, MailAccount> implements MailAccountService {
|
public class MailAccountServiceImpl extends ServiceImpl<MailAccountMapper, MailAccount> implements MailAccountService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取启用的账户(status=1,按创建时间倒序取第一个)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public MailAccount getEnabledAccount() {
|
||||||
|
return getOne(new LambdaQueryWrapper<MailAccount>()
|
||||||
|
.eq(MailAccount::getStatus, Boolean.TRUE) // 1-启用
|
||||||
|
.orderByDesc(MailAccount::getCreateTime)
|
||||||
|
.last("limit 1"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,30 @@
|
|||||||
package com.mini.capi.biz.service.impl;
|
package com.mini.capi.biz.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.mini.capi.config.JavaMailConfig;
|
||||||
|
import com.mini.capi.biz.domain.MailAccount;
|
||||||
|
import com.mini.capi.biz.domain.MailAttachment;
|
||||||
import com.mini.capi.biz.domain.MailReceived;
|
import com.mini.capi.biz.domain.MailReceived;
|
||||||
import com.mini.capi.biz.mapper.MailReceivedMapper;
|
import com.mini.capi.biz.mapper.MailReceivedMapper;
|
||||||
|
import com.mini.capi.biz.service.MailAccountService;
|
||||||
|
import com.mini.capi.biz.service.MailAttachmentService;
|
||||||
import com.mini.capi.biz.service.MailReceivedService;
|
import com.mini.capi.biz.service.MailReceivedService;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.mini.capi.utils.FileUtils;
|
||||||
|
import com.mini.capi.utils.MailParseUtils;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.mail.*;
|
||||||
|
import javax.mail.internet.MimeMessage;
|
||||||
|
import javax.mail.internet.MimeUtility;
|
||||||
|
import javax.mail.search.FlagTerm;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* 接收邮件表 服务实现类
|
* 接收邮件表 服务实现类
|
||||||
@@ -17,4 +36,187 @@ import org.springframework.stereotype.Service;
|
|||||||
@Service
|
@Service
|
||||||
public class MailReceivedServiceImpl extends ServiceImpl<MailReceivedMapper, MailReceived> implements MailReceivedService {
|
public class MailReceivedServiceImpl extends ServiceImpl<MailReceivedMapper, MailReceived> implements MailReceivedService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private MailAccountService mailAccountService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private MailAttachmentService mailAttachmentService;
|
||||||
|
|
||||||
|
@Qualifier("attachmentThreadPool")
|
||||||
|
@Resource
|
||||||
|
private ExecutorService attachmentThreadPool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收INBOX未读邮件:同步保存邮件基本信息,异步保存附件
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void receiveUnreadMail() {
|
||||||
|
// 1. 获取启用的邮件账户
|
||||||
|
MailAccount account = mailAccountService.getEnabledAccount();
|
||||||
|
if (account == null) {
|
||||||
|
throw new RuntimeException("无启用的邮件账户,无法接收邮件");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 创建IMAP Session并连接邮箱
|
||||||
|
Session session = JavaMailConfig.createImapSession(account);
|
||||||
|
Store store = null;
|
||||||
|
Folder inbox = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
store = session.getStore("imap");
|
||||||
|
store.connect(account.getHost(), account.getUsername(), account.getPassword());
|
||||||
|
|
||||||
|
// 3. 打开INBOX文件夹(READ_WRITE模式,支持设置已读)
|
||||||
|
inbox = store.getFolder("INBOX");
|
||||||
|
inbox.open(Folder.READ_WRITE);
|
||||||
|
|
||||||
|
// 4. 获取未读邮件(Flags.Flag.SEEN=false)
|
||||||
|
Message[] unreadMessages = inbox.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
|
||||||
|
if (unreadMessages == null || unreadMessages.length == 0) {
|
||||||
|
System.out.print("INBOX无未读邮件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 遍历未读邮件,处理基本信息和附件
|
||||||
|
for (Message message : unreadMessages) {
|
||||||
|
MimeMessage mimeMsg = (MimeMessage) message;
|
||||||
|
handleSingleMail(mimeMsg, account, inbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("接收邮件失败", e);
|
||||||
|
} finally {
|
||||||
|
// 6. 关闭资源
|
||||||
|
try {
|
||||||
|
if (inbox != null && inbox.isOpen()) {
|
||||||
|
inbox.close(false); // false:不删除邮件
|
||||||
|
}
|
||||||
|
if (store != null && store.isConnected()) {
|
||||||
|
store.close();
|
||||||
|
}
|
||||||
|
} catch (MessagingException e) {
|
||||||
|
System.out.print(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理单封邮件:同步存基本信息,异步存附件
|
||||||
|
*/
|
||||||
|
private void handleSingleMail(MimeMessage message, MailAccount account, Folder inbox) throws Exception {
|
||||||
|
// -------------------------- 同步保存邮件基本信息 --------------------------
|
||||||
|
MailReceived mailReceived = new MailReceived();
|
||||||
|
// 基础字段
|
||||||
|
mailReceived.setAccountId(account.getId());
|
||||||
|
mailReceived.setMessageId(message.getMessageID());
|
||||||
|
mailReceived.setFromAddress(MailParseUtils.parseFrom(message));
|
||||||
|
mailReceived.setToAddresses(MailParseUtils.parseTo(message));
|
||||||
|
mailReceived.setCcAddresses(MailParseUtils.parseCc(message));
|
||||||
|
mailReceived.setSubject(MimeUtility.decodeText(message.getSubject())); // 处理中文主题乱码
|
||||||
|
mailReceived.setContent(MailParseUtils.parseContent(message));
|
||||||
|
// 时间字段(邮件发送时间 -> 转为LocalDateTime)
|
||||||
|
mailReceived.setSendTime(LocalDateTime.ofInstant(message.getSentDate().toInstant(), java.time.ZoneId.systemDefault()));
|
||||||
|
mailReceived.setReceiveTime(LocalDateTime.now());
|
||||||
|
// 状态字段
|
||||||
|
mailReceived.setIsRead(Boolean.FALSE); // 初始未读,附件处理后设为已读
|
||||||
|
List<MailParseUtils.AttachmentInfo> attachments = MailParseUtils.extractAttachments(message);
|
||||||
|
mailReceived.setHasAttachment(!attachments.isEmpty() ? Boolean.TRUE : Boolean.FALSE);
|
||||||
|
// 公共字段
|
||||||
|
mailReceived.setCreateTime(LocalDateTime.now());
|
||||||
|
mailReceived.setUpdateTime(LocalDateTime.now());
|
||||||
|
mailReceived.setFTenantId(account.getFTenantId()); // 继承账户的租户ID
|
||||||
|
mailReceived.setFFlowId(account.getFFlowId());
|
||||||
|
mailReceived.setFFlowTaskId(account.getFFlowTaskId());
|
||||||
|
mailReceived.setFFlowState(account.getFFlowState());
|
||||||
|
|
||||||
|
// 保存到接收表,获取主键ID(用于附件关联)
|
||||||
|
save(mailReceived);
|
||||||
|
Long receivedId = mailReceived.getId();
|
||||||
|
if (receivedId == null) {
|
||||||
|
throw new RuntimeException("保存接收邮件失败,主键ID为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------- 异步保存附件(多线程) --------------------------
|
||||||
|
if (!attachments.isEmpty()) {
|
||||||
|
attachmentThreadPool.submit(() -> {
|
||||||
|
try {
|
||||||
|
saveAttachments(attachments, receivedId, account);
|
||||||
|
// 附件保存完成后,设置邮件为已读
|
||||||
|
message.setFlag(Flags.Flag.SEEN, true);
|
||||||
|
// 更新接收表的已读状态
|
||||||
|
MailReceived updateRead = new MailReceived();
|
||||||
|
updateRead.setId(receivedId);
|
||||||
|
updateRead.setIsRead(Boolean.TRUE);
|
||||||
|
updateById(updateRead);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.print(e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 无附件,直接设为已读
|
||||||
|
message.setFlag(Flags.Flag.SEEN, true);
|
||||||
|
MailReceived updateRead = new MailReceived();
|
||||||
|
updateRead.setId(receivedId);
|
||||||
|
updateRead.setIsRead(Boolean.TRUE);
|
||||||
|
updateById(updateRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存附件到本地目录和数据库
|
||||||
|
*
|
||||||
|
* @param attachments 附件信息列表
|
||||||
|
* @param refId 关联的接收邮件ID
|
||||||
|
* @param account 邮件账户(用于租户等公共字段)
|
||||||
|
*/
|
||||||
|
private void saveAttachments(List<MailParseUtils.AttachmentInfo> attachments, Long refId, MailAccount account) {
|
||||||
|
for (MailParseUtils.AttachmentInfo attachment : attachments) {
|
||||||
|
try (InputStream inputStream = attachment.getInputStream()) { // 自动关闭流
|
||||||
|
// 1. 生成附件存储信息
|
||||||
|
String originalFileName = attachment.getOriginalFileName();
|
||||||
|
String randomFileName = FileUtils.generate32RandomFileName(originalFileName);
|
||||||
|
String storagePath = FileUtils.ATTACHMENT_ROOT_DIR + "/" + randomFileName; // 完整存储路径
|
||||||
|
String fileNo = randomFileName.substring(0, 16); // 文件编号(取32位随机名前16位)
|
||||||
|
String directory = FileUtils.ATTACHMENT_ROOT_DIR; // 目录(根目录)
|
||||||
|
// 2. 保存附件到本地目录
|
||||||
|
FileUtils.saveFile(inputStream, storagePath);
|
||||||
|
File savedFile = new File(storagePath);
|
||||||
|
Long fileSize = FileUtils.getFileSize(savedFile);
|
||||||
|
// 3. 保存附件信息到数据库(类型1:收件附件)
|
||||||
|
MailAttachment mailAttachment = new MailAttachment();
|
||||||
|
mailAttachment.setFileNo(fileNo);
|
||||||
|
mailAttachment.setDirectory(directory);
|
||||||
|
mailAttachment.setOriginalFileName(originalFileName);
|
||||||
|
mailAttachment.setStoragePath(storagePath);
|
||||||
|
mailAttachment.setFileSize(fileSize);
|
||||||
|
mailAttachment.setType(Boolean.TRUE); // 1-收件附件
|
||||||
|
mailAttachment.setRefId(refId); // 关联接收邮件ID
|
||||||
|
mailAttachment.setContentType(getContentType(originalFileName)); // 简单判断文件类型
|
||||||
|
mailAttachment.setDownloadCount(0); // 初始下载次数0
|
||||||
|
// 公共字段
|
||||||
|
mailAttachment.setCreateTime(LocalDateTime.now());
|
||||||
|
mailAttachment.setUpdateTime(LocalDateTime.now());
|
||||||
|
mailAttachment.setFTenantId(account.getFTenantId());
|
||||||
|
mailAttachment.setFFlowId(account.getFFlowId());
|
||||||
|
mailAttachment.setFFlowTaskId(account.getFFlowTaskId());
|
||||||
|
mailAttachment.setFFlowState(account.getFFlowState());
|
||||||
|
mailAttachmentService.save(mailAttachment);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.print(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单判断文件类型(可扩展为更精准的判断)
|
||||||
|
*/
|
||||||
|
private String getContentType(String fileName) {
|
||||||
|
if (fileName.endsWith(".txt")) return "text/plain";
|
||||||
|
if (fileName.endsWith(".pdf")) return "application/pdf";
|
||||||
|
if (fileName.endsWith(".doc") || fileName.endsWith(".docx")) return "application/msword";
|
||||||
|
if (fileName.endsWith(".xls") || fileName.endsWith(".xlsx")) return "application/vnd.ms-excel";
|
||||||
|
if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) return "image/jpeg";
|
||||||
|
if (fileName.endsWith(".png")) return "image/png";
|
||||||
|
return "application/octet-stream"; // 默认二进制流
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,25 @@
|
|||||||
package com.mini.capi.biz.service.impl;
|
package com.mini.capi.biz.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.mini.capi.config.JavaMailConfig;
|
||||||
|
import com.mini.capi.biz.domain.MailAccount;
|
||||||
|
import com.mini.capi.biz.domain.MailAttachment;
|
||||||
import com.mini.capi.biz.domain.MailSent;
|
import com.mini.capi.biz.domain.MailSent;
|
||||||
import com.mini.capi.biz.mapper.MailSentMapper;
|
import com.mini.capi.biz.mapper.MailSentMapper;
|
||||||
|
import com.mini.capi.biz.service.MailAccountService;
|
||||||
|
import com.mini.capi.biz.service.MailAttachmentService;
|
||||||
import com.mini.capi.biz.service.MailSentService;
|
import com.mini.capi.biz.service.MailSentService;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.mini.capi.utils.FileUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import javax.activation.DataHandler;
|
||||||
|
import javax.mail.*;
|
||||||
|
import javax.mail.internet.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@@ -17,4 +32,221 @@ import org.springframework.stereotype.Service;
|
|||||||
@Service
|
@Service
|
||||||
public class MailSentServiceImpl extends ServiceImpl<MailSentMapper, MailSent> implements MailSentService {
|
public class MailSentServiceImpl extends ServiceImpl<MailSentMapper, MailSent> implements MailSentService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MailAccountService mailAccountService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MailAttachmentService mailAttachmentService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送邮件(支持多收件人、多抄送、多附件)
|
||||||
|
*
|
||||||
|
* @param toAddresses 收件人列表(数组)
|
||||||
|
* @param ccAddresses 抄送列表(数组,可为null)
|
||||||
|
* @param subject 邮件主题
|
||||||
|
* @param content 邮件内容(支持HTML)
|
||||||
|
* @param attachments 附件列表(可为null)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void sendMail(String[] toAddresses, String[] ccAddresses, String subject, String content, MultipartFile[] attachments) {
|
||||||
|
// 1. 校验参数
|
||||||
|
if (toAddresses == null || toAddresses.length == 0) {
|
||||||
|
throw new IllegalArgumentException("收件人不能为空");
|
||||||
|
}
|
||||||
|
if (subject == null || subject.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("邮件主题不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取启用的邮件账户
|
||||||
|
MailAccount account = mailAccountService.getEnabledAccount();
|
||||||
|
if (account == null) {
|
||||||
|
throw new RuntimeException("无启用的邮件账户,无法发送邮件");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 创建SMTP Session和MimeMessage
|
||||||
|
Session session = JavaMailConfig.createSmtpSession(account);
|
||||||
|
MimeMessage message = new MimeMessage(session);
|
||||||
|
Transport transport = null;
|
||||||
|
try {
|
||||||
|
// -------------------------- 构建邮件内容 --------------------------
|
||||||
|
// 3.1 设置发件人
|
||||||
|
message.setFrom(new InternetAddress(account.getFromAddress()));
|
||||||
|
|
||||||
|
// 3.2 设置收件人
|
||||||
|
InternetAddress[] toAddrs = Arrays.stream(toAddresses)
|
||||||
|
.map(addr -> {
|
||||||
|
try {
|
||||||
|
return new InternetAddress(addr);
|
||||||
|
} catch (AddressException e) {
|
||||||
|
throw new RuntimeException("收件人地址格式错误:" + addr, e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.toArray(InternetAddress[]::new);
|
||||||
|
message.setRecipients(Message.RecipientType.TO, toAddrs);
|
||||||
|
|
||||||
|
// 3.3 设置抄送(可选)
|
||||||
|
if (ccAddresses != null && ccAddresses.length > 0) {
|
||||||
|
InternetAddress[] ccAddrs = Arrays.stream(ccAddresses)
|
||||||
|
.map(addr -> {
|
||||||
|
try {
|
||||||
|
return new InternetAddress(addr);
|
||||||
|
} catch (AddressException e) {
|
||||||
|
throw new RuntimeException("抄送地址格式错误:" + addr, e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.toArray(InternetAddress[]::new);
|
||||||
|
message.setRecipients(Message.RecipientType.CC, ccAddrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.4 设置主题(处理中文)
|
||||||
|
message.setSubject(subject, "UTF-8");
|
||||||
|
|
||||||
|
// 3.5 构建邮件内容(支持附件)
|
||||||
|
Multipart multipart = new MimeMultipart();
|
||||||
|
// 文本部分
|
||||||
|
MimeBodyPart textPart = new MimeBodyPart();
|
||||||
|
textPart.setContent(content, "text/html;charset=UTF-8"); // 支持HTML内容
|
||||||
|
multipart.addBodyPart(textPart);
|
||||||
|
|
||||||
|
// 3.6 添加附件(可选)
|
||||||
|
boolean hasAttachment = attachments != null && attachments.length > 0;
|
||||||
|
if (hasAttachment) {
|
||||||
|
for (MultipartFile file : attachments) {
|
||||||
|
if (file.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
MimeBodyPart attachmentPart = new MimeBodyPart();
|
||||||
|
// 附件流
|
||||||
|
attachmentPart.setDataHandler(new DataHandler(file.getInputStream(), file.getContentType()));
|
||||||
|
// 附件名(处理中文乱码)
|
||||||
|
attachmentPart.setFileName(MimeUtility.encodeText(file.getOriginalFilename()));
|
||||||
|
multipart.addBodyPart(attachmentPart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.7 设置邮件内容
|
||||||
|
message.setContent(multipart);
|
||||||
|
// 设置发送时间
|
||||||
|
message.setSentDate(new java.util.Date());
|
||||||
|
message.saveChanges();
|
||||||
|
|
||||||
|
// -------------------------- 发送邮件 --------------------------
|
||||||
|
transport = session.getTransport("smtp");
|
||||||
|
transport.connect(account.getHost(), account.getUsername(), account.getPassword());
|
||||||
|
transport.sendMessage(message, message.getAllRecipients());
|
||||||
|
|
||||||
|
// -------------------------- 保存发送记录到数据库 --------------------------
|
||||||
|
saveSentRecord(message, account, toAddresses, ccAddresses, subject, content, hasAttachment, null);
|
||||||
|
|
||||||
|
// -------------------------- 保存附件记录到数据库(类型2:发件附件) --------------------------
|
||||||
|
if (hasAttachment) {
|
||||||
|
saveSendAttachments(attachments, getSentIdByMessageId(message.getMessageID()), account);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 发送失败,保存失败记录
|
||||||
|
saveSentRecord(null, account, toAddresses, ccAddresses, subject, content,
|
||||||
|
attachments != null && attachments.length > 0, e.getMessage());
|
||||||
|
throw new RuntimeException("发送邮件失败", e);
|
||||||
|
} finally {
|
||||||
|
// 关闭资源
|
||||||
|
try {
|
||||||
|
if (transport != null && transport.isConnected()) {
|
||||||
|
transport.close();
|
||||||
|
}
|
||||||
|
} catch (MessagingException e) {
|
||||||
|
log.error("关闭SMTP连接失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存发送记录到biz_mail_sent
|
||||||
|
*/
|
||||||
|
private void saveSentRecord(MimeMessage message, MailAccount account, String[] toAddresses,
|
||||||
|
String[] ccAddresses, String subject, String content,
|
||||||
|
boolean hasAttachment, String errorMsg) {
|
||||||
|
try {
|
||||||
|
MailSent mailSent = new MailSent();
|
||||||
|
// 基础字段
|
||||||
|
mailSent.setMessageId(message != null ? message.getMessageID() : null);
|
||||||
|
mailSent.setAccountId(account.getId());
|
||||||
|
mailSent.setFromAddress(account.getFromAddress());
|
||||||
|
mailSent.setToAddresses(String.join(",", toAddresses));
|
||||||
|
mailSent.setCcAddresses(ccAddresses != null ? String.join(",", ccAddresses) : "");
|
||||||
|
mailSent.setSubject(subject);
|
||||||
|
mailSent.setContent(content);
|
||||||
|
mailSent.setSendTime(LocalDateTime.now());
|
||||||
|
// 状态字段
|
||||||
|
mailSent.setSendStatus(message != null ? Boolean.TRUE : Boolean.FALSE); // 1-成功,0-失败
|
||||||
|
mailSent.setErrorMsg(errorMsg); // 失败时记录错误信息
|
||||||
|
mailSent.setHasAttachment(hasAttachment ? Boolean.TRUE : Boolean.FALSE);
|
||||||
|
// 公共字段
|
||||||
|
mailSent.setCreateTime(LocalDateTime.now());
|
||||||
|
mailSent.setUpdateTime(LocalDateTime.now());
|
||||||
|
mailSent.setFTenantId(account.getFTenantId());
|
||||||
|
mailSent.setFFlowId(account.getFFlowId());
|
||||||
|
mailSent.setFFlowTaskId(account.getFFlowTaskId());
|
||||||
|
mailSent.setFFlowState(account.getFFlowState());
|
||||||
|
save(mailSent);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存发件附件到本地和数据库(类型2:发件附件)
|
||||||
|
*/
|
||||||
|
private void saveSendAttachments(MultipartFile[] multipartFiles, Long sentId, MailAccount account) {
|
||||||
|
for (MultipartFile file : multipartFiles) {
|
||||||
|
if (file.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 1. 生成附件存储信息
|
||||||
|
String originalFileName = file.getOriginalFilename();
|
||||||
|
String randomFileName = FileUtils.generate32RandomFileName(originalFileName);
|
||||||
|
String storagePath = FileUtils.ATTACHMENT_ROOT_DIR + "/" + randomFileName;
|
||||||
|
String fileNo = randomFileName.substring(0, 16);
|
||||||
|
String directory = FileUtils.ATTACHMENT_ROOT_DIR;
|
||||||
|
|
||||||
|
// 2. 保存附件到本地
|
||||||
|
FileUtils.saveFile(file.getInputStream(), storagePath);
|
||||||
|
Long fileSize = file.getSize();
|
||||||
|
|
||||||
|
// 3. 保存到附件表(类型2:发件附件)
|
||||||
|
MailAttachment mailAttachment = new MailAttachment();
|
||||||
|
mailAttachment.setFileNo(fileNo);
|
||||||
|
mailAttachment.setDirectory(directory);
|
||||||
|
mailAttachment.setOriginalFileName(originalFileName);
|
||||||
|
mailAttachment.setStoragePath(storagePath);
|
||||||
|
mailAttachment.setFileSize(fileSize);
|
||||||
|
mailAttachment.setType(Boolean.FALSE); // 2-发件附件(注意:实体类type是Boolean,用false对应2)
|
||||||
|
mailAttachment.setRefId(sentId); // 关联发送邮件ID
|
||||||
|
mailAttachment.setContentType(file.getContentType());
|
||||||
|
mailAttachment.setDownloadCount(0);
|
||||||
|
// 公共字段
|
||||||
|
mailAttachment.setCreateTime(LocalDateTime.now());
|
||||||
|
mailAttachment.setUpdateTime(LocalDateTime.now());
|
||||||
|
mailAttachment.setFTenantId(account.getFTenantId());
|
||||||
|
mailAttachment.setFFlowId(account.getFFlowId());
|
||||||
|
mailAttachment.setFFlowTaskId(account.getFFlowTaskId());
|
||||||
|
mailAttachment.setFFlowState(account.getFFlowState());
|
||||||
|
|
||||||
|
mailAttachmentService.save(mailAttachment);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据messageId查询发送记录ID(用于附件关联)
|
||||||
|
*/
|
||||||
|
private Long getSentIdByMessageId(String messageId) {
|
||||||
|
MailSent mailSent = getOne(new LambdaQueryWrapper<MailSent>()
|
||||||
|
.eq(MailSent::getMessageId, messageId)
|
||||||
|
.last("limit 1"));
|
||||||
|
return mailSent != null ? mailSent.getId() : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
69
src/main/java/com/mini/capi/config/JavaMailConfig.java
Normal file
69
src/main/java/com/mini/capi/config/JavaMailConfig.java
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package com.mini.capi.config;
|
||||||
|
|
||||||
|
import com.mini.capi.biz.domain.MailAccount;
|
||||||
|
import javax.mail.Session;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
public class JavaMailConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建IMAP Session(用于接收邮件)
|
||||||
|
*/
|
||||||
|
public static Session createImapSession(MailAccount account) {
|
||||||
|
Properties props = new Properties();
|
||||||
|
// IMAP基础配置
|
||||||
|
props.setProperty("mail.store.protocol", "imap");
|
||||||
|
props.setProperty("mail.imap.host", account.getHost());
|
||||||
|
props.setProperty("mail.imap.port", account.getImapPort().toString());
|
||||||
|
// SSL配置
|
||||||
|
if (Boolean.TRUE.equals(account.getSslEnable())) {
|
||||||
|
props.setProperty("mail.imap.ssl.enable", "true");
|
||||||
|
props.setProperty("mail.imap.ssl.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
|
||||||
|
}
|
||||||
|
// 超时配置
|
||||||
|
props.setProperty("mail.imap.connectiontimeout", "5000");
|
||||||
|
props.setProperty("mail.imap.timeout", "5000");
|
||||||
|
|
||||||
|
// 创建Session(带认证)
|
||||||
|
return Session.getInstance(props, new javax.mail.Authenticator() {
|
||||||
|
@Override
|
||||||
|
protected javax.mail.PasswordAuthentication getPasswordAuthentication() {
|
||||||
|
return new javax.mail.PasswordAuthentication(
|
||||||
|
account.getUsername(),
|
||||||
|
account.getPassword()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建SMTP Session(用于发送邮件)
|
||||||
|
*/
|
||||||
|
public static Session createSmtpSession(MailAccount account) {
|
||||||
|
Properties props = new Properties();
|
||||||
|
// SMTP基础配置
|
||||||
|
props.setProperty("mail.transport.protocol", "smtp");
|
||||||
|
props.setProperty("mail.smtp.host", account.getHost());
|
||||||
|
props.setProperty("mail.smtp.port", account.getSmtpPort().toString());
|
||||||
|
// 认证和SSL配置
|
||||||
|
props.setProperty("mail.smtp.auth", "true"); // 必须开启认证
|
||||||
|
if (Boolean.TRUE.equals(account.getSslEnable())) {
|
||||||
|
props.setProperty("mail.smtp.ssl.enable", "true");
|
||||||
|
props.setProperty("mail.smtp.ssl.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
|
||||||
|
}
|
||||||
|
// 超时配置
|
||||||
|
props.setProperty("mail.smtp.connectiontimeout", "5000");
|
||||||
|
props.setProperty("mail.smtp.timeout", "5000");
|
||||||
|
|
||||||
|
// 创建Session(带认证)
|
||||||
|
return Session.getInstance(props, new javax.mail.Authenticator() {
|
||||||
|
@Override
|
||||||
|
protected javax.mail.PasswordAuthentication getPasswordAuthentication() {
|
||||||
|
return new javax.mail.PasswordAuthentication(
|
||||||
|
account.getUsername(),
|
||||||
|
account.getPassword()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/main/java/com/mini/capi/config/ThreadPoolConfig.java
Normal file
25
src/main/java/com/mini/capi/config/ThreadPoolConfig.java
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package com.mini.capi.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class ThreadPoolConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附件处理线程池:核心线程2,最大线程5,队列100,空闲60s回收
|
||||||
|
*/
|
||||||
|
@Bean("attachmentThreadPool")
|
||||||
|
public ExecutorService attachmentThreadPool() {
|
||||||
|
return new ThreadPoolExecutor(
|
||||||
|
2,
|
||||||
|
5,
|
||||||
|
60L,
|
||||||
|
TimeUnit.SECONDS,
|
||||||
|
new LinkedBlockingQueue<>(100),
|
||||||
|
Executors.defaultThreadFactory(),
|
||||||
|
new ThreadPoolExecutor.AbortPolicy() // 队列满时拒绝策略(可根据需求调整)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.mini.capi.exception;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理业务异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(RuntimeException.class)
|
||||||
|
public ResponseEntity<Map<String, String>> handleRuntimeException(RuntimeException e) {
|
||||||
|
log.error("业务异常:", e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(Map.of("code", "500", "msg", e.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理参数异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
|
public ResponseEntity<Map<String, String>> handleIllegalArgumentException(IllegalArgumentException e) {
|
||||||
|
log.error("参数异常:", e);
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(Map.of("code", "400", "msg", e.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理附件过大异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||||
|
public ResponseEntity<Map<String, String>> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e) {
|
||||||
|
log.error("附件过大:", e);
|
||||||
|
return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
|
||||||
|
.body(Map.of("code", "413", "msg", "附件大小超过限制"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理通用异常
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<Map<String, String>> handleException(Exception e) {
|
||||||
|
log.error("系统异常:", e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(Map.of("code", "500", "msg", "系统内部错误,请联系管理员"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package com.mini.capi.mail.config;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class MailConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public JavaMailSenderImpl javaMailSender() {
|
|
||||||
return new JavaMailSenderImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 配置SMTP属性
|
|
||||||
*/
|
|
||||||
public Properties getSmtpProperties(String host, int port, boolean ssl, boolean auth) {
|
|
||||||
Properties props = new Properties();
|
|
||||||
props.put("mail.smtp.host", host);
|
|
||||||
props.put("mail.smtp.port", port);
|
|
||||||
props.put("mail.smtp.auth", auth);
|
|
||||||
|
|
||||||
if (ssl) {
|
|
||||||
props.put("mail.smtp.ssl.enable", "true");
|
|
||||||
} else {
|
|
||||||
props.put("mail.smtp.starttls.enable", "true");
|
|
||||||
}
|
|
||||||
|
|
||||||
props.put("mail.smtp.connectiontimeout", 5000);
|
|
||||||
props.put("mail.smtp.timeout", 5000);
|
|
||||||
props.put("mail.smtp.writetimeout", 5000);
|
|
||||||
|
|
||||||
return props;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 配置IMAP属性
|
|
||||||
*/
|
|
||||||
public Properties getImapProperties(String host, int port, boolean ssl) {
|
|
||||||
Properties props = new Properties();
|
|
||||||
props.put("mail.imap.host", host);
|
|
||||||
props.put("mail.imap.port", port);
|
|
||||||
|
|
||||||
if (ssl) {
|
|
||||||
props.put("mail.imap.ssl.enable", "true");
|
|
||||||
}
|
|
||||||
|
|
||||||
props.put("mail.imap.connectiontimeout", 5000);
|
|
||||||
props.put("mail.imap.timeout", 5000);
|
|
||||||
|
|
||||||
return props;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package com.mini.capi.mail.config;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
|
||||||
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
import java.util.concurrent.ThreadPoolExecutor;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class ThreadPoolConfig {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 附件处理线程池
|
|
||||||
*/
|
|
||||||
@Bean(name = "attachmentExecutor")
|
|
||||||
public Executor attachmentExecutor() {
|
|
||||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
|
||||||
// 核心线程数
|
|
||||||
executor.setCorePoolSize(5);
|
|
||||||
// 最大线程数
|
|
||||||
executor.setMaxPoolSize(10);
|
|
||||||
// 队列容量
|
|
||||||
executor.setQueueCapacity(50);
|
|
||||||
// 线程名称前缀
|
|
||||||
executor.setThreadNamePrefix("attachment-");
|
|
||||||
// 拒绝策略:由调用线程处理
|
|
||||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
|
||||||
// 初始化
|
|
||||||
executor.initialize();
|
|
||||||
return executor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
package com.mini.capi.mail.controller;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/mail")
|
|
||||||
public class MailController {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MailReceiveService mailReceiveService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MailSendService mailSendService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MailAccountMapper mailAccountMapper;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MailReceivedMapper mailReceivedMapper;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MailSentMapper mailSentMapper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 接收邮件
|
|
||||||
*/
|
|
||||||
@PostMapping("/receive/{accountId}")
|
|
||||||
public ResponseVO<Integer> receiveEmails(@PathVariable Long accountId) {
|
|
||||||
try {
|
|
||||||
int count = mailReceiveService.syncNewEmails(accountId);
|
|
||||||
return ResponseVO.success(count, "接收邮件成功");
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ResponseVO.error("接收邮件失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送邮件
|
|
||||||
*/
|
|
||||||
@PostMapping("/send/{accountId}")
|
|
||||||
public ResponseVO<Long> sendEmail(
|
|
||||||
@PathVariable Long accountId,
|
|
||||||
@ModelAttribute MailSendVO mailSendVO) {
|
|
||||||
try {
|
|
||||||
MailAccount account = mailAccountMapper.selectById(accountId);
|
|
||||||
if (account == null) {
|
|
||||||
return ResponseVO.error("未找到邮件账户");
|
|
||||||
}
|
|
||||||
|
|
||||||
Long mailId = mailSendService.sendEmail(account, mailSendVO);
|
|
||||||
return ResponseVO.success(mailId, "发送邮件成功");
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ResponseVO.error("发送邮件失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取收件箱邮件
|
|
||||||
*/
|
|
||||||
@GetMapping("/received/{accountId}")
|
|
||||||
public ResponseVO<List<MailReceived>> getReceivedEmails(
|
|
||||||
@PathVariable Long accountId,
|
|
||||||
@RequestParam(defaultValue = "0") int page,
|
|
||||||
@RequestParam(defaultValue = "20") int size) {
|
|
||||||
try {
|
|
||||||
QueryWrapper<MailReceived> query = new QueryWrapper<>();
|
|
||||||
query.eq("account_id", accountId)
|
|
||||||
.orderByDesc("received_date")
|
|
||||||
.last("LIMIT " + page * size + "," + size);
|
|
||||||
|
|
||||||
List<MailReceived> emails = mailReceivedMapper.selectList(query);
|
|
||||||
return ResponseVO.success(emails);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ResponseVO.error("获取收件箱邮件失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取已发送邮件
|
|
||||||
*/
|
|
||||||
@GetMapping("/sent/{accountId}")
|
|
||||||
public ResponseVO<List<MailSent>> getSentEmails(
|
|
||||||
@PathVariable Long accountId,
|
|
||||||
@RequestParam(defaultValue = "0") int page,
|
|
||||||
@RequestParam(defaultValue = "20") int size) {
|
|
||||||
try {
|
|
||||||
QueryWrapper<MailSent> query = new QueryWrapper<>();
|
|
||||||
query.eq("account_id", accountId)
|
|
||||||
.orderByDesc("send_date")
|
|
||||||
.last("LIMIT " + page * size + "," + size);
|
|
||||||
|
|
||||||
List<MailSent> emails = mailSentMapper.selectList(query);
|
|
||||||
return ResponseVO.success(emails);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ResponseVO.error("获取已发送邮件失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package com.mini.capi.mail;
|
|
||||||
|
|
||||||
public class dc {
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.mini.capi.mail.service;
|
|
||||||
|
|
||||||
public interface MailReceiveService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 接收邮件
|
|
||||||
* @param account 邮件账户
|
|
||||||
* @return 接收成功的邮件数量
|
|
||||||
*/
|
|
||||||
int receiveEmails(MailAccount account);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 同步接收最新的未读邮件
|
|
||||||
* @param accountId 邮件账户ID
|
|
||||||
* @return 接收成功的邮件数量
|
|
||||||
*/
|
|
||||||
int syncNewEmails(Long accountId);
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.mini.capi.mail.service;
|
|
||||||
|
|
||||||
public interface MailSendService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送邮件
|
|
||||||
* @param account 邮件账户
|
|
||||||
* @param mailSendVO 邮件发送参数
|
|
||||||
* @return 发送成功的邮件ID
|
|
||||||
*/
|
|
||||||
Long sendEmail(MailAccount account, MailSendVO mailSendVO);
|
|
||||||
}
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
package com.mini.capi.mail.service.impl;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.scheduling.annotation.Async;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import javax.mail.*;
|
|
||||||
import javax.mail.internet.InternetAddress;
|
|
||||||
import javax.mail.internet.MimeMessage;
|
|
||||||
import javax.mail.internet.MimeMultipart;
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class MailReceiveServiceImpl implements MailReceiveService {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(MailReceiveServiceImpl.class);
|
|
||||||
|
|
||||||
// 邮件存储目录
|
|
||||||
private static final String MAIL_FILES_DIR = "/ogsapp/mailfiles";
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MailAccountMapper mailAccountMapper;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MailReceivedMapper mailReceivedMapper;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MailAttachmentMapper mailAttachmentMapper;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MailConfig mailConfig;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private Executor attachmentExecutor;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public int receiveEmails(MailAccount account) {
|
|
||||||
if (account == null) {
|
|
||||||
logger.error("邮件账户不能为空");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Store store = null;
|
|
||||||
Folder inbox = null;
|
|
||||||
int receivedCount = 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 初始化IMAP连接
|
|
||||||
Properties props = mailConfig.getImapProperties(
|
|
||||||
account.getImapHost(),
|
|
||||||
account.getImapPort(),
|
|
||||||
account.isImapSsl()
|
|
||||||
);
|
|
||||||
|
|
||||||
Session session = Session.getInstance(props);
|
|
||||||
store = session.getStore("imap");
|
|
||||||
store.connect(
|
|
||||||
account.getImapHost(),
|
|
||||||
account.getUsername(),
|
|
||||||
account.getPassword()
|
|
||||||
);
|
|
||||||
|
|
||||||
// 打开收件箱,只获取未读邮件
|
|
||||||
inbox = store.getFolder("INBOX");
|
|
||||||
inbox.open(Folder.READ_WRITE);
|
|
||||||
|
|
||||||
// 搜索未读邮件
|
|
||||||
Message[] messages = inbox.search(
|
|
||||||
new FlagTerm(new Flags(Flags.Flag.SEEN), false)
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info("找到 {} 封未读邮件", messages.length);
|
|
||||||
|
|
||||||
// 处理每封邮件
|
|
||||||
for (Message message : messages) {
|
|
||||||
if (message instanceof MimeMessage) {
|
|
||||||
// 保存邮件基本信息
|
|
||||||
MailReceived mailReceived = saveMailInfo((MimeMessage) message, account);
|
|
||||||
if (mailReceived != null) {
|
|
||||||
receivedCount++;
|
|
||||||
|
|
||||||
// 异步处理附件
|
|
||||||
attachmentExecutor.execute(() -> {
|
|
||||||
try {
|
|
||||||
handleAttachments((MimeMessage) message, mailReceived.getId());
|
|
||||||
// 标记为已读
|
|
||||||
message.setFlag(Flags.Flag.SEEN, true);
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("处理邮件附件失败", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("接收邮件失败", e);
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
if (inbox != null && inbox.isOpen()) {
|
|
||||||
inbox.close(false);
|
|
||||||
}
|
|
||||||
if (store != null && store.isConnected()) {
|
|
||||||
store.close();
|
|
||||||
}
|
|
||||||
} catch (MessagingException e) {
|
|
||||||
logger.error("关闭邮件连接失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return receivedCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int syncNewEmails(Long accountId) {
|
|
||||||
if (accountId == null) {
|
|
||||||
logger.error("账户ID不能为空");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
MailAccount account = mailAccountMapper.selectById(accountId);
|
|
||||||
if (account == null) {
|
|
||||||
logger.error("未找到ID为 {} 的邮件账户", accountId);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return receiveEmails(account);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存邮件基本信息到数据库
|
|
||||||
*/
|
|
||||||
private MailReceived saveMailInfo(MimeMessage message, MailAccount account) throws MessagingException {
|
|
||||||
try {
|
|
||||||
MailReceived mailReceived = new MailReceived();
|
|
||||||
|
|
||||||
// 设置邮件基本信息
|
|
||||||
mailReceived.setAccountId(account.getId());
|
|
||||||
mailReceived.setSubject(message.getSubject());
|
|
||||||
mailReceived.setSentDate(message.getSentDate());
|
|
||||||
mailReceived.setReceivedDate(new Date());
|
|
||||||
|
|
||||||
// 设置发件人
|
|
||||||
Address[] fromAddresses = message.getFrom();
|
|
||||||
if (fromAddresses != null && fromAddresses.length > 0) {
|
|
||||||
mailReceived.setFromAddress(((InternetAddress) fromAddresses[0]).getAddress());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置收件人
|
|
||||||
Address[] toAddresses = message.getRecipients(Message.RecipientType.TO);
|
|
||||||
if (toAddresses != null && toAddresses.length > 0) {
|
|
||||||
List<String> toList = new ArrayList<>();
|
|
||||||
for (Address addr : toAddresses) {
|
|
||||||
toList.add(((InternetAddress) addr).getAddress());
|
|
||||||
}
|
|
||||||
mailReceived.setToAddresses(String.join(",", toList));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置抄送人
|
|
||||||
Address[] ccAddresses = message.getRecipients(Message.RecipientType.CC);
|
|
||||||
if (ccAddresses != null && ccAddresses.length > 0) {
|
|
||||||
List<String> ccList = new ArrayList<>();
|
|
||||||
for (Address addr : ccAddresses) {
|
|
||||||
ccList.add(((InternetAddress) addr).getAddress());
|
|
||||||
}
|
|
||||||
mailReceived.setCcAddresses(String.join(",", ccList));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取邮件内容
|
|
||||||
String content = MailUtil.getEmailContent(message);
|
|
||||||
mailReceived.setContent(content);
|
|
||||||
|
|
||||||
// 保存到数据库
|
|
||||||
mailReceivedMapper.insert(mailReceived);
|
|
||||||
return mailReceived;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("保存邮件信息失败", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理并保存邮件附件
|
|
||||||
*/
|
|
||||||
@Async("attachmentExecutor")
|
|
||||||
@Transactional
|
|
||||||
public void handleAttachments(MimeMessage message, Long mailReceivedId) throws MessagingException {
|
|
||||||
try {
|
|
||||||
Object content = message.getContent();
|
|
||||||
|
|
||||||
if (content instanceof MimeMultipart) {
|
|
||||||
MimeMultipart multipart = (MimeMultipart) content;
|
|
||||||
|
|
||||||
// 遍历所有部分寻找附件
|
|
||||||
for (int i = 0; i < multipart.getCount(); i++) {
|
|
||||||
BodyPart bodyPart = multipart.getBodyPart(i);
|
|
||||||
|
|
||||||
// 判断是否为附件
|
|
||||||
if (Part.ATTACHMENT.equalsIgnoreCase(bodyPart.getDisposition()) ||
|
|
||||||
bodyPart.getFileName() != null) {
|
|
||||||
|
|
||||||
// 处理附件
|
|
||||||
String originalFileName = bodyPart.getFileName();
|
|
||||||
if (originalFileName != null) {
|
|
||||||
// 生成存储文件名(32位随机字符+扩展名)
|
|
||||||
String fileExt = originalFileName.contains(".") ?
|
|
||||||
originalFileName.substring(originalFileName.lastIndexOf(".")) : "";
|
|
||||||
String storedFileName = UUID.randomUUID().toString().replaceAll("-", "") + fileExt;
|
|
||||||
|
|
||||||
// 确保存储目录存在
|
|
||||||
File dir = new File(MAIL_FILES_DIR);
|
|
||||||
if (!dir.exists()) {
|
|
||||||
dir.mkdirs();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存附件
|
|
||||||
String filePath = MAIL_FILES_DIR + File.separator + storedFileName;
|
|
||||||
FileUtil.saveFile(bodyPart.getInputStream(), filePath);
|
|
||||||
|
|
||||||
// 获取文件大小
|
|
||||||
File savedFile = new File(filePath);
|
|
||||||
long fileSize = savedFile.length();
|
|
||||||
|
|
||||||
// 保存附件信息到数据库
|
|
||||||
MailAttachment attachment = new MailAttachment();
|
|
||||||
attachment.setFileNo(UUID.randomUUID().toString());
|
|
||||||
attachment.setDirectory(MAIL_FILES_DIR);
|
|
||||||
attachment.setFileName(originalFileName);
|
|
||||||
attachment.setStoredPath(filePath);
|
|
||||||
attachment.setFileSize(fileSize);
|
|
||||||
attachment.setType(1); // 1表示收件
|
|
||||||
attachment.setRelatedId(mailReceivedId);
|
|
||||||
attachment.setCreateTime(new Date());
|
|
||||||
|
|
||||||
mailAttachmentMapper.insert(attachment);
|
|
||||||
|
|
||||||
logger.info("保存附件成功: {} -> {}", originalFileName, storedFileName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("处理邮件附件失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
package com.mini.capi.mail.service.impl;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.mail.javamail.JavaMailSenderImpl;
|
|
||||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import javax.mail.internet.MimeMessage;
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class MailSendServiceImpl implements MailSendService {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(MailSendServiceImpl.class);
|
|
||||||
|
|
||||||
// 邮件存储目录
|
|
||||||
private static final String MAIL_FILES_DIR = "/ogsapp/mailfiles";
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private JavaMailSenderImpl mailSender;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MailConfig mailConfig;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MailSentMapper mailSentMapper;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MailAttachmentMapper mailAttachmentMapper;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private Executor attachmentExecutor;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public Long sendEmail(MailAccount account, MailSendVO mailSendVO) {
|
|
||||||
if (account == null || mailSendVO == null) {
|
|
||||||
logger.error("邮件账户或发送参数不能为空");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mailSendVO.getTo() == null || mailSendVO.getTo().isEmpty()) {
|
|
||||||
logger.error("收件人不能为空");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 配置邮件发送器
|
|
||||||
mailSender.setHost(account.getSmtpHost());
|
|
||||||
mailSender.setPort(account.getSmtpPort());
|
|
||||||
mailSender.setUsername(account.getUsername());
|
|
||||||
mailSender.setPassword(account.getPassword());
|
|
||||||
mailSender.setJavaMailProperties(mailConfig.getSmtpProperties(
|
|
||||||
account.getSmtpHost(),
|
|
||||||
account.getSmtpPort(),
|
|
||||||
account.isSmtpSsl(),
|
|
||||||
true
|
|
||||||
));
|
|
||||||
|
|
||||||
// 创建邮件消息
|
|
||||||
MimeMessage message = mailSender.createMimeMessage();
|
|
||||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
|
||||||
|
|
||||||
// 设置发件人
|
|
||||||
helper.setFrom(account.getUsername());
|
|
||||||
|
|
||||||
// 设置收件人
|
|
||||||
helper.setTo(mailSendVO.getTo().toArray(new String[0]));
|
|
||||||
|
|
||||||
// 设置抄送
|
|
||||||
if (!CollectionUtils.isEmpty(mailSendVO.getCc())) {
|
|
||||||
helper.setCc(mailSendVO.getCc().toArray(new String[0]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置邮件主题和内容
|
|
||||||
helper.setSubject(mailSendVO.getSubject());
|
|
||||||
helper.setText(mailSendVO.getContent(), mailSendVO.isHtml());
|
|
||||||
|
|
||||||
// 设置发送时间
|
|
||||||
Date sendDate = new Date();
|
|
||||||
helper.setSentDate(sendDate);
|
|
||||||
|
|
||||||
// 处理附件
|
|
||||||
List<MailAttachment> attachments = new ArrayList<>();
|
|
||||||
if (mailSendVO.getAttachments() != null && !mailSendVO.getAttachments().isEmpty()) {
|
|
||||||
for (MultipartFile file : mailSendVO.getAttachments()) {
|
|
||||||
if (!file.isEmpty()) {
|
|
||||||
// 保存附件到服务器
|
|
||||||
String originalFileName = file.getOriginalFilename();
|
|
||||||
String fileExt = originalFileName.contains(".") ?
|
|
||||||
originalFileName.substring(originalFileName.lastIndexOf(".")) : "";
|
|
||||||
String storedFileName = UUID.randomUUID().toString().replaceAll("-", "") + fileExt;
|
|
||||||
|
|
||||||
// 确保存储目录存在
|
|
||||||
File dir = new File(MAIL_FILES_DIR);
|
|
||||||
if (!dir.exists()) {
|
|
||||||
dir.mkdirs();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存文件
|
|
||||||
String filePath = MAIL_FILES_DIR + File.separator + storedFileName;
|
|
||||||
file.transferTo(new File(filePath));
|
|
||||||
|
|
||||||
// 添加到邮件
|
|
||||||
helper.addAttachment(originalFileName, new File(filePath));
|
|
||||||
|
|
||||||
// 记录附件信息
|
|
||||||
MailAttachment attachment = new MailAttachment();
|
|
||||||
attachment.setFileNo(UUID.randomUUID().toString());
|
|
||||||
attachment.setDirectory(MAIL_FILES_DIR);
|
|
||||||
attachment.setFileName(originalFileName);
|
|
||||||
attachment.setStoredPath(filePath);
|
|
||||||
attachment.setFileSize(file.getSize());
|
|
||||||
attachment.setType(2); // 2表示发件
|
|
||||||
attachment.setCreateTime(new Date());
|
|
||||||
|
|
||||||
attachments.add(attachment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送邮件
|
|
||||||
mailSender.send(message);
|
|
||||||
logger.info("邮件发送成功,主题: {}", mailSendVO.getSubject());
|
|
||||||
|
|
||||||
// 保存发送记录
|
|
||||||
MailSent mailSent = new MailSent();
|
|
||||||
mailSent.setAccountId(account.getId());
|
|
||||||
mailSent.setSubject(mailSendVO.getSubject());
|
|
||||||
mailSent.setContent(mailSendVO.getContent());
|
|
||||||
mailSent.setFromAddress(account.getUsername());
|
|
||||||
mailSent.setToAddresses(String.join(",", mailSendVO.getTo()));
|
|
||||||
|
|
||||||
if (!CollectionUtils.isEmpty(mailSendVO.getCc())) {
|
|
||||||
mailSent.setCcAddresses(String.join(",", mailSendVO.getCc()));
|
|
||||||
}
|
|
||||||
|
|
||||||
mailSent.setSendDate(sendDate);
|
|
||||||
mailSent.setIsHtml(mailSendVO.isHtml() ? 1 : 0);
|
|
||||||
|
|
||||||
mailSentMapper.insert(mailSent);
|
|
||||||
|
|
||||||
// 保存附件关联信息
|
|
||||||
for (MailAttachment attachment : attachments) {
|
|
||||||
attachment.setRelatedId(mailSent.getId());
|
|
||||||
mailAttachmentMapper.insert(attachment);
|
|
||||||
}
|
|
||||||
|
|
||||||
return mailSent.getId();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("发送邮件失败", e);
|
|
||||||
throw new RuntimeException("发送邮件失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.mini.capi.mail.vo;
|
|
||||||
|
|
||||||
public class MailSendVO {
|
|
||||||
|
|
||||||
// 收件人列表
|
|
||||||
private List<String> to;
|
|
||||||
|
|
||||||
// 抄送列表
|
|
||||||
private List<String> cc;
|
|
||||||
|
|
||||||
// 邮件主题
|
|
||||||
private String subject;
|
|
||||||
|
|
||||||
// 邮件内容
|
|
||||||
private String content;
|
|
||||||
|
|
||||||
// 是否为HTML内容
|
|
||||||
private boolean isHtml = false;
|
|
||||||
|
|
||||||
// 附件列表
|
|
||||||
private List<MultipartFile> attachments;
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package com.mini.capi.mail.vo;
|
|
||||||
|
|
||||||
|
|
||||||
public class ResponseVO<T> {
|
|
||||||
|
|
||||||
// 状态码:0表示成功,其他表示失败
|
|
||||||
private int code;
|
|
||||||
|
|
||||||
// 消息
|
|
||||||
private String message;
|
|
||||||
|
|
||||||
// 数据
|
|
||||||
private T data;
|
|
||||||
|
|
||||||
// 成功响应
|
|
||||||
public static <T> ResponseVO<T> success(T data, String message) {
|
|
||||||
ResponseVO<T> response = new ResponseVO<>();
|
|
||||||
response.code = 0;
|
|
||||||
response.message = message;
|
|
||||||
response.data = data;
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 成功响应(默认消息)
|
|
||||||
public static <T> ResponseVO<T> success(T data) {
|
|
||||||
return success(data, "操作成功");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 错误响应
|
|
||||||
public static <T> ResponseVO<T> error(String message) {
|
|
||||||
ResponseVO<T> response = new ResponseVO<>();
|
|
||||||
response.code = 1;
|
|
||||||
response.message = message;
|
|
||||||
response.data = null;
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.mini.capi.sys.controller;
|
||||||
|
|
||||||
|
import com.mini.capi.biz.service.MailReceivedService;
|
||||||
|
import com.mini.capi.biz.service.MailSentService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/mail")
|
||||||
|
public class MailController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MailReceivedService mailReceiveService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MailSentService mailSendService;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发接收INBOX未读邮件
|
||||||
|
*/
|
||||||
|
@PostMapping("/receive")
|
||||||
|
public ResponseEntity<Map<String, String>> receiveMail() {
|
||||||
|
mailReceiveService.receiveUnreadMail();
|
||||||
|
return ResponseEntity.ok(Map.of("code", "200", "msg", "接收邮件任务已触发"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送邮件(支持多收件人、多抄送、多附件)
|
||||||
|
* @param to 收件人(多个用逗号分隔,如:a@xxx.com,b@xxx.com)
|
||||||
|
* @param cc 抄送(多个用逗号分隔,可选)
|
||||||
|
* @param subject 主题
|
||||||
|
* @param content 内容(支持HTML)
|
||||||
|
* @param files 附件(可选)
|
||||||
|
*/
|
||||||
|
@PostMapping("/send")
|
||||||
|
public ResponseEntity<Map<String, String>> sendMail(
|
||||||
|
@RequestParam("to") String to,
|
||||||
|
@RequestParam(value = "cc", required = false) String cc,
|
||||||
|
@RequestParam("subject") String subject,
|
||||||
|
@RequestParam("content") String content,
|
||||||
|
@RequestParam(value = "files", required = false) MultipartFile[] files) {
|
||||||
|
|
||||||
|
// 解析收件人(逗号分隔转数组)
|
||||||
|
String[] toAddresses = to.split(",");
|
||||||
|
// 解析抄送(可选,空则为null)
|
||||||
|
String[] ccAddresses = cc != null && !cc.trim().isEmpty() ? cc.split(",") : null;
|
||||||
|
|
||||||
|
// 调用发送服务
|
||||||
|
mailSendService.sendMail(toAddresses, ccAddresses, subject, content, files);
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED)
|
||||||
|
.body(Map.of("code", "201", "msg", "邮件发送成功"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,61 +1,78 @@
|
|||||||
package com.mini.capi.utils;
|
package com.mini.capi.utils;
|
||||||
|
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.Paths;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public class FileUtil {
|
public class FileUtils {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(FileUtil.class);
|
// 附件根目录(需求指定)
|
||||||
|
public static final String ATTACHMENT_ROOT_DIR = "/ogsapp/mailFiles";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存输入流到文件
|
* 检查目录是否存在,不存在则创建
|
||||||
*/
|
*/
|
||||||
public static void saveFile(InputStream inputStream, String filePath) {
|
public static void checkDirExists(String dirPath) {
|
||||||
try (OutputStream outputStream = new FileOutputStream(filePath)) {
|
File dir = new File(dirPath);
|
||||||
byte[] buffer = new byte[4096];
|
if (!dir.exists()) {
|
||||||
int bytesRead;
|
boolean mkdirs = dir.mkdirs();
|
||||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
if (!mkdirs) {
|
||||||
outputStream.write(buffer, 0, bytesRead);
|
throw new RuntimeException("创建目录失败:" + dirPath);
|
||||||
}
|
}
|
||||||
logger.info("文件保存成功: {}", filePath);
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("保存文件失败: " + filePath, e);
|
|
||||||
throw new RuntimeException("保存文件失败", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 复制文件
|
* 保存文件到指定路径
|
||||||
|
*
|
||||||
|
* @param inputStream 输入流(附件流)
|
||||||
|
* @param savePath 保存路径(含文件名)
|
||||||
*/
|
*/
|
||||||
public static void copyFile(File source, File dest) {
|
public static void saveFile(InputStream inputStream, String savePath) {
|
||||||
try {
|
checkDirExists(Paths.get(savePath).getParent().toString()); // 检查父目录
|
||||||
Files.copy(source.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
try (OutputStream os = new FileOutputStream(savePath);
|
||||||
|
BufferedInputStream bis = new BufferedInputStream(inputStream);
|
||||||
|
BufferedOutputStream bos = new BufferedOutputStream(os)) {
|
||||||
|
|
||||||
|
byte[] buffer = new byte[1024 * 8];
|
||||||
|
int len;
|
||||||
|
while ((len = bis.read(buffer)) != -1) {
|
||||||
|
bos.write(buffer, 0, len);
|
||||||
|
}
|
||||||
|
bos.flush();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.error("复制文件失败: {} -> {}", source.getPath(), dest.getPath(), e);
|
throw new RuntimeException("保存文件失败:" + savePath, e);
|
||||||
throw new RuntimeException("复制文件失败", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除文件
|
* 生成32位随机文件名(UUID去掉横线)+ 原始文件后缀
|
||||||
|
*
|
||||||
|
* @param originalFileName 原始文件名
|
||||||
|
* @return 32位随机文件名(如:a1b2c3d4...1234.txt)
|
||||||
*/
|
*/
|
||||||
public static boolean deleteFile(String filePath) {
|
public static String generate32RandomFileName(String originalFileName) {
|
||||||
File file = new File(filePath);
|
// 获取文件后缀(如.txt)
|
||||||
if (file.exists() && file.isFile()) {
|
String suffix = "";
|
||||||
boolean deleted = file.delete();
|
if (StringUtils.hasText(originalFileName) && originalFileName.contains(".")) {
|
||||||
if (deleted) {
|
suffix = originalFileName.substring(originalFileName.lastIndexOf("."));
|
||||||
logger.info("文件删除成功: {}", filePath);
|
|
||||||
} else {
|
|
||||||
logger.warn("文件删除失败: {}", filePath);
|
|
||||||
}
|
|
||||||
return deleted;
|
|
||||||
}
|
}
|
||||||
return false;
|
// 生成32位UUID(去掉横线)
|
||||||
|
String random32Str = UUID.randomUUID().toString().replace("-", "");
|
||||||
|
return random32Str + suffix;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件大小(字节)
|
||||||
|
*/
|
||||||
|
public static long getFileSize(File file) {
|
||||||
|
try {
|
||||||
|
return Files.size(file.toPath());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("获取文件大小失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
132
src/main/java/com/mini/capi/utils/MailParseUtils.java
Normal file
132
src/main/java/com/mini/capi/utils/MailParseUtils.java
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package com.mini.capi.utils;
|
||||||
|
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import javax.mail.*;
|
||||||
|
import javax.mail.internet.InternetAddress;
|
||||||
|
import javax.mail.internet.MimeMultipart;
|
||||||
|
import javax.mail.internet.MimeUtility;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MailParseUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析邮件发件人(名称+地址)
|
||||||
|
*/
|
||||||
|
public static String parseFrom(Message message) throws MessagingException {
|
||||||
|
Address[] froms = message.getFrom();
|
||||||
|
if (froms == null || froms.length == 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
InternetAddress from = (InternetAddress) froms[0];
|
||||||
|
String fromName = from.getPersonal(); // 发件人名称(可能为null)
|
||||||
|
String fromAddr = from.getAddress(); // 发件人地址
|
||||||
|
return StringUtils.hasText(fromName) ? fromName + "<" + fromAddr + ">" : fromAddr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析邮件收件人(多个用逗号分隔)
|
||||||
|
*/
|
||||||
|
public static String parseTo(Message message) throws MessagingException {
|
||||||
|
return parseRecipients(message, Message.RecipientType.TO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析邮件抄送人(多个用逗号分隔)
|
||||||
|
*/
|
||||||
|
public static String parseCc(Message message) throws MessagingException {
|
||||||
|
return parseRecipients(message, Message.RecipientType.CC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用解析收件人/抄送人
|
||||||
|
*/
|
||||||
|
private static String parseRecipients(Message message, Message.RecipientType type) throws MessagingException {
|
||||||
|
Address[] recipients = message.getRecipients(type);
|
||||||
|
if (recipients == null || recipients.length == 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
List<String> addrList = new ArrayList<>();
|
||||||
|
for (Address addr : recipients) {
|
||||||
|
InternetAddress internetAddr = (InternetAddress) addr;
|
||||||
|
String name = internetAddr.getPersonal();
|
||||||
|
String address = internetAddr.getAddress();
|
||||||
|
addrList.add(StringUtils.hasText(name) ? name + "<" + address + ">" : address);
|
||||||
|
}
|
||||||
|
return String.join(",", addrList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析邮件内容(支持文本/HTML)
|
||||||
|
*/
|
||||||
|
public static String parseContent(Part part) throws MessagingException, IOException {
|
||||||
|
if (part.isMimeType("text/plain") || part.isMimeType("text/html")) {
|
||||||
|
// 文本/HTML直接读取
|
||||||
|
return (String) part.getContent();
|
||||||
|
}
|
||||||
|
// 多部分内容(含附件),递归解析文本部分
|
||||||
|
if (part.isMimeType("multipart/*")) {
|
||||||
|
MimeMultipart multipart = (MimeMultipart) part.getContent();
|
||||||
|
for (int i = 0; i < multipart.getCount(); i++) {
|
||||||
|
BodyPart bodyPart = multipart.getBodyPart(i);
|
||||||
|
String content = parseContent(bodyPart);
|
||||||
|
if (StringUtils.hasText(content)) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取邮件附件(返回附件流+原始文件名)
|
||||||
|
*/
|
||||||
|
public static List<AttachmentInfo> extractAttachments(Part part) throws MessagingException, IOException {
|
||||||
|
List<AttachmentInfo> attachments = new ArrayList<>();
|
||||||
|
// 多部分内容才可能有附件
|
||||||
|
if (part.isMimeType("multipart/*")) {
|
||||||
|
MimeMultipart multipart = (MimeMultipart) part.getContent();
|
||||||
|
for (int i = 0; i < multipart.getCount(); i++) {
|
||||||
|
BodyPart bodyPart = multipart.getBodyPart(i);
|
||||||
|
// 判断是否为附件(Disposition为ATTACHMENT或INLINE)
|
||||||
|
String disposition = bodyPart.getDisposition();
|
||||||
|
if (disposition != null && (disposition.equals(Part.ATTACHMENT) || disposition.equals(Part.INLINE))) {
|
||||||
|
// 获取原始文件名(处理中文乱码)
|
||||||
|
String originalFileName = MimeUtility.decodeText(bodyPart.getFileName());
|
||||||
|
// 获取附件输入流
|
||||||
|
InputStream inputStream = bodyPart.getInputStream();
|
||||||
|
attachments.add(new AttachmentInfo(originalFileName, inputStream));
|
||||||
|
} else {
|
||||||
|
// 递归处理嵌套的多部分内容
|
||||||
|
attachments.addAll(extractAttachments(bodyPart));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附件信息封装(原始文件名+输入流)
|
||||||
|
*/
|
||||||
|
public static class AttachmentInfo {
|
||||||
|
private String originalFileName;
|
||||||
|
private InputStream inputStream;
|
||||||
|
|
||||||
|
public AttachmentInfo(String originalFileName, InputStream inputStream) {
|
||||||
|
this.originalFileName = originalFileName;
|
||||||
|
this.inputStream = inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getter
|
||||||
|
public String getOriginalFileName() {
|
||||||
|
return originalFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getInputStream() {
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user