初始化前端目录

This commit is contained in:
2025-09-22 22:16:27 +08:00
parent 52e94288df
commit 69d559a920
23 changed files with 854 additions and 741 deletions

View File

@@ -13,4 +13,8 @@ import com.baomidou.mybatisplus.extension.service.IService;
*/
public interface MailAccountService extends IService<MailAccount> {
/**
* 获取启用的邮件账户(默认取第一个,可扩展为多账户轮询)
*/
MailAccount getEnabledAccount();
}

View File

@@ -13,4 +13,5 @@ import com.baomidou.mybatisplus.extension.service.IService;
*/
public interface MailReceivedService extends IService<MailReceived> {
void receiveUnreadMail();
}

View File

@@ -2,6 +2,7 @@ package com.mini.capi.biz.service;
import com.mini.capi.biz.domain.MailSent;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springframework.web.multipart.MultipartFile;
/**
* <p>
@@ -13,4 +14,5 @@ import com.baomidou.mybatisplus.extension.service.IService;
*/
public interface MailSentService extends IService<MailSent> {
void sendMail(String[] toAddresses, String[] ccAddresses, String subject, String content, MultipartFile[] attachments);
}

View File

@@ -1,5 +1,6 @@
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.mapper.MailAccountMapper;
import com.mini.capi.biz.service.MailAccountService;
@@ -17,4 +18,14 @@ import org.springframework.stereotype.Service;
@Service
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"));
}
}

View File

@@ -1,11 +1,30 @@
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.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.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 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>
* 接收邮件表 服务实现类
@@ -17,4 +36,187 @@ import org.springframework.stereotype.Service;
@Service
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"; // 默认二进制流
}
}

View File

@@ -1,10 +1,25 @@
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.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.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.web.multipart.MultipartFile;
import javax.activation.DataHandler;
import javax.mail.*;
import javax.mail.internet.*;
import java.time.LocalDateTime;
import java.util.Arrays;
/**
* <p>
@@ -17,4 +32,221 @@ import org.springframework.stereotype.Service;
@Service
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;
}
}

View 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()
);
}
});
}
}

View 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() // 队列满时拒绝策略(可根据需求调整)
);
}
}

View File

@@ -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", "系统内部错误,请联系管理员"));
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}
}

View File

@@ -1,4 +0,0 @@
package com.mini.capi.mail;
public class dc {
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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", "邮件发送成功"));
}
}

View File

@@ -1,61 +1,78 @@
package com.mini.capi.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import java.io.*;
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) {
try (OutputStream outputStream = new FileOutputStream(filePath)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
public static void checkDirExists(String dirPath) {
File dir = new File(dirPath);
if (!dir.exists()) {
boolean mkdirs = dir.mkdirs();
if (!mkdirs) {
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) {
try {
Files.copy(source.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
public static void saveFile(InputStream inputStream, String savePath) {
checkDirExists(Paths.get(savePath).getParent().toString()); // 检查父目录
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) {
logger.error("复制文件失败: {} -> {}", source.getPath(), dest.getPath(), e);
throw new RuntimeException("复制文件失败", e);
throw new RuntimeException("保存文件失败" + savePath, e);
}
}
/**
* 删除文件
* 生成32位随机文件名UUID去掉横线+ 原始文件后缀
*
* @param originalFileName 原始文件名
* @return 32位随机文件名a1b2c3d4...1234.txt
*/
public static boolean deleteFile(String filePath) {
File file = new File(filePath);
if (file.exists() && file.isFile()) {
boolean deleted = file.delete();
if (deleted) {
logger.info("文件删除成功: {}", filePath);
} else {
logger.warn("文件删除失败: {}", filePath);
}
return deleted;
public static String generate32RandomFileName(String originalFileName) {
// 获取文件后缀(如.txt
String suffix = "";
if (StringUtils.hasText(originalFileName) && originalFileName.contains(".")) {
suffix = originalFileName.substring(originalFileName.lastIndexOf("."));
}
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);
}
}
}

View 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;
}
}
}