From 69d559a9205b54e7a6fa1db0ce111f0acce91507 Mon Sep 17 00:00:00 2001 From: gaoxq <376340421@qq.com> Date: Mon, 22 Sep 2025 22:16:27 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- .../capi/biz/service/MailAccountService.java | 4 + .../capi/biz/service/MailReceivedService.java | 1 + .../capi/biz/service/MailSentService.java | 2 + .../service/impl/MailAccountServiceImpl.java | 11 + .../service/impl/MailReceivedServiceImpl.java | 204 +++++++++++++- .../biz/service/impl/MailSentServiceImpl.java | 234 +++++++++++++++- .../com/mini/capi/config/JavaMailConfig.java | 69 +++++ .../mini/capi/config/ThreadPoolConfig.java | 25 ++ .../exception/GlobalExceptionHandler.java | 57 ++++ .../com/mini/capi/mail/config/MailConfig.java | 57 ---- .../capi/mail/config/ThreadPoolConfig.java | 33 --- .../capi/mail/controller/MailController.java | 102 ------- src/main/java/com/mini/capi/mail/dc.java | 4 - .../capi/mail/service/MailReceiveService.java | 18 -- .../capi/mail/service/MailSendService.java | 12 - .../service/impl/MailReceiveServiceImpl.java | 253 ------------------ .../service/impl/MailSendServiceImpl.java | 163 ----------- .../com/mini/capi/mail/vo/MailSendVO.java | 22 -- .../com/mini/capi/mail/vo/ResponseVO.java | 38 --- .../capi/sys/controller/MailController.java | 63 +++++ .../java/com/mini/capi/utils/FileUtils.java | 89 +++--- .../com/mini/capi/utils/MailParseUtils.java | 132 +++++++++ 23 files changed, 854 insertions(+), 741 deletions(-) create mode 100644 src/main/java/com/mini/capi/config/JavaMailConfig.java create mode 100644 src/main/java/com/mini/capi/config/ThreadPoolConfig.java create mode 100644 src/main/java/com/mini/capi/exception/GlobalExceptionHandler.java delete mode 100644 src/main/java/com/mini/capi/mail/config/MailConfig.java delete mode 100644 src/main/java/com/mini/capi/mail/config/ThreadPoolConfig.java delete mode 100644 src/main/java/com/mini/capi/mail/controller/MailController.java delete mode 100644 src/main/java/com/mini/capi/mail/dc.java delete mode 100644 src/main/java/com/mini/capi/mail/service/MailReceiveService.java delete mode 100644 src/main/java/com/mini/capi/mail/service/MailSendService.java delete mode 100644 src/main/java/com/mini/capi/mail/service/impl/MailReceiveServiceImpl.java delete mode 100644 src/main/java/com/mini/capi/mail/service/impl/MailSendServiceImpl.java delete mode 100644 src/main/java/com/mini/capi/mail/vo/MailSendVO.java delete mode 100644 src/main/java/com/mini/capi/mail/vo/ResponseVO.java create mode 100644 src/main/java/com/mini/capi/sys/controller/MailController.java create mode 100644 src/main/java/com/mini/capi/utils/MailParseUtils.java diff --git a/pom.xml b/pom.xml index d1433a5..3a3a24f 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,7 @@ org.springdoc springdoc-openapi-starter-webmvc-ui - 2.2.0 + 2.7.0 diff --git a/src/main/java/com/mini/capi/biz/service/MailAccountService.java b/src/main/java/com/mini/capi/biz/service/MailAccountService.java index c3e3c1a..a4f2522 100644 --- a/src/main/java/com/mini/capi/biz/service/MailAccountService.java +++ b/src/main/java/com/mini/capi/biz/service/MailAccountService.java @@ -13,4 +13,8 @@ import com.baomidou.mybatisplus.extension.service.IService; */ public interface MailAccountService extends IService { + /** + * 获取启用的邮件账户(默认取第一个,可扩展为多账户轮询) + */ + MailAccount getEnabledAccount(); } diff --git a/src/main/java/com/mini/capi/biz/service/MailReceivedService.java b/src/main/java/com/mini/capi/biz/service/MailReceivedService.java index 737163a..2c0421d 100644 --- a/src/main/java/com/mini/capi/biz/service/MailReceivedService.java +++ b/src/main/java/com/mini/capi/biz/service/MailReceivedService.java @@ -13,4 +13,5 @@ import com.baomidou.mybatisplus.extension.service.IService; */ public interface MailReceivedService extends IService { + void receiveUnreadMail(); } diff --git a/src/main/java/com/mini/capi/biz/service/MailSentService.java b/src/main/java/com/mini/capi/biz/service/MailSentService.java index 9433bfd..309674f 100644 --- a/src/main/java/com/mini/capi/biz/service/MailSentService.java +++ b/src/main/java/com/mini/capi/biz/service/MailSentService.java @@ -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; /** *

@@ -13,4 +14,5 @@ import com.baomidou.mybatisplus.extension.service.IService; */ public interface MailSentService extends IService { + void sendMail(String[] toAddresses, String[] ccAddresses, String subject, String content, MultipartFile[] attachments); } diff --git a/src/main/java/com/mini/capi/biz/service/impl/MailAccountServiceImpl.java b/src/main/java/com/mini/capi/biz/service/impl/MailAccountServiceImpl.java index 643d837..db6ea1a 100644 --- a/src/main/java/com/mini/capi/biz/service/impl/MailAccountServiceImpl.java +++ b/src/main/java/com/mini/capi/biz/service/impl/MailAccountServiceImpl.java @@ -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 implements MailAccountService { + /** + * 获取启用的账户(status=1,按创建时间倒序取第一个) + */ + @Override + public MailAccount getEnabledAccount() { + return getOne(new LambdaQueryWrapper() + .eq(MailAccount::getStatus, Boolean.TRUE) // 1-启用 + .orderByDesc(MailAccount::getCreateTime) + .last("limit 1")); + } } diff --git a/src/main/java/com/mini/capi/biz/service/impl/MailReceivedServiceImpl.java b/src/main/java/com/mini/capi/biz/service/impl/MailReceivedServiceImpl.java index d0c21aa..d530f8b 100644 --- a/src/main/java/com/mini/capi/biz/service/impl/MailReceivedServiceImpl.java +++ b/src/main/java/com/mini/capi/biz/service/impl/MailReceivedServiceImpl.java @@ -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; + /** *

* 接收邮件表 服务实现类 @@ -17,4 +36,187 @@ import org.springframework.stereotype.Service; @Service public class MailReceivedServiceImpl extends ServiceImpl 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 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 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"; // 默认二进制流 + } } diff --git a/src/main/java/com/mini/capi/biz/service/impl/MailSentServiceImpl.java b/src/main/java/com/mini/capi/biz/service/impl/MailSentServiceImpl.java index 42ee154..ea142d4 100644 --- a/src/main/java/com/mini/capi/biz/service/impl/MailSentServiceImpl.java +++ b/src/main/java/com/mini/capi/biz/service/impl/MailSentServiceImpl.java @@ -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; /** *

@@ -17,4 +32,221 @@ import org.springframework.stereotype.Service; @Service public class MailSentServiceImpl extends ServiceImpl 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() + .eq(MailSent::getMessageId, messageId) + .last("limit 1")); + return mailSent != null ? mailSent.getId() : null; + } } diff --git a/src/main/java/com/mini/capi/config/JavaMailConfig.java b/src/main/java/com/mini/capi/config/JavaMailConfig.java new file mode 100644 index 0000000..a37145b --- /dev/null +++ b/src/main/java/com/mini/capi/config/JavaMailConfig.java @@ -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() + ); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/mini/capi/config/ThreadPoolConfig.java b/src/main/java/com/mini/capi/config/ThreadPoolConfig.java new file mode 100644 index 0000000..10e0fae --- /dev/null +++ b/src/main/java/com/mini/capi/config/ThreadPoolConfig.java @@ -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() // 队列满时拒绝策略(可根据需求调整) + ); + } +} diff --git a/src/main/java/com/mini/capi/exception/GlobalExceptionHandler.java b/src/main/java/com/mini/capi/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..22e4f8b --- /dev/null +++ b/src/main/java/com/mini/capi/exception/GlobalExceptionHandler.java @@ -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> 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> 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> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e) { + log.error("附件过大:", e); + return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE) + .body(Map.of("code", "413", "msg", "附件大小超过限制")); + } + + /** + * 处理通用异常 + */ + @ExceptionHandler(Exception.class) + public ResponseEntity> handleException(Exception e) { + log.error("系统异常:", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("code", "500", "msg", "系统内部错误,请联系管理员")); + } +} diff --git a/src/main/java/com/mini/capi/mail/config/MailConfig.java b/src/main/java/com/mini/capi/mail/config/MailConfig.java deleted file mode 100644 index fd0602e..0000000 --- a/src/main/java/com/mini/capi/mail/config/MailConfig.java +++ /dev/null @@ -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; - } - -} diff --git a/src/main/java/com/mini/capi/mail/config/ThreadPoolConfig.java b/src/main/java/com/mini/capi/mail/config/ThreadPoolConfig.java deleted file mode 100644 index 63e1fd6..0000000 --- a/src/main/java/com/mini/capi/mail/config/ThreadPoolConfig.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/com/mini/capi/mail/controller/MailController.java b/src/main/java/com/mini/capi/mail/controller/MailController.java deleted file mode 100644 index ce0d710..0000000 --- a/src/main/java/com/mini/capi/mail/controller/MailController.java +++ /dev/null @@ -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 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 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> getReceivedEmails( - @PathVariable Long accountId, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size) { - try { - QueryWrapper query = new QueryWrapper<>(); - query.eq("account_id", accountId) - .orderByDesc("received_date") - .last("LIMIT " + page * size + "," + size); - - List emails = mailReceivedMapper.selectList(query); - return ResponseVO.success(emails); - } catch (Exception e) { - return ResponseVO.error("获取收件箱邮件失败: " + e.getMessage()); - } - } - - /** - * 获取已发送邮件 - */ - @GetMapping("/sent/{accountId}") - public ResponseVO> getSentEmails( - @PathVariable Long accountId, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size) { - try { - QueryWrapper query = new QueryWrapper<>(); - query.eq("account_id", accountId) - .orderByDesc("send_date") - .last("LIMIT " + page * size + "," + size); - - List emails = mailSentMapper.selectList(query); - return ResponseVO.success(emails); - } catch (Exception e) { - return ResponseVO.error("获取已发送邮件失败: " + e.getMessage()); - } - } -} - diff --git a/src/main/java/com/mini/capi/mail/dc.java b/src/main/java/com/mini/capi/mail/dc.java deleted file mode 100644 index d1b6779..0000000 --- a/src/main/java/com/mini/capi/mail/dc.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.mini.capi.mail; - -public class dc { -} diff --git a/src/main/java/com/mini/capi/mail/service/MailReceiveService.java b/src/main/java/com/mini/capi/mail/service/MailReceiveService.java deleted file mode 100644 index 0b6e309..0000000 --- a/src/main/java/com/mini/capi/mail/service/MailReceiveService.java +++ /dev/null @@ -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); -} diff --git a/src/main/java/com/mini/capi/mail/service/MailSendService.java b/src/main/java/com/mini/capi/mail/service/MailSendService.java deleted file mode 100644 index 336f5df..0000000 --- a/src/main/java/com/mini/capi/mail/service/MailSendService.java +++ /dev/null @@ -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); -} diff --git a/src/main/java/com/mini/capi/mail/service/impl/MailReceiveServiceImpl.java b/src/main/java/com/mini/capi/mail/service/impl/MailReceiveServiceImpl.java deleted file mode 100644 index b938240..0000000 --- a/src/main/java/com/mini/capi/mail/service/impl/MailReceiveServiceImpl.java +++ /dev/null @@ -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 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 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); - } - } -} diff --git a/src/main/java/com/mini/capi/mail/service/impl/MailSendServiceImpl.java b/src/main/java/com/mini/capi/mail/service/impl/MailSendServiceImpl.java deleted file mode 100644 index 6725d70..0000000 --- a/src/main/java/com/mini/capi/mail/service/impl/MailSendServiceImpl.java +++ /dev/null @@ -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 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); - } - } -} diff --git a/src/main/java/com/mini/capi/mail/vo/MailSendVO.java b/src/main/java/com/mini/capi/mail/vo/MailSendVO.java deleted file mode 100644 index 943d87d..0000000 --- a/src/main/java/com/mini/capi/mail/vo/MailSendVO.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.mini.capi.mail.vo; - -public class MailSendVO { - - // 收件人列表 - private List to; - - // 抄送列表 - private List cc; - - // 邮件主题 - private String subject; - - // 邮件内容 - private String content; - - // 是否为HTML内容 - private boolean isHtml = false; - - // 附件列表 - private List attachments; -} diff --git a/src/main/java/com/mini/capi/mail/vo/ResponseVO.java b/src/main/java/com/mini/capi/mail/vo/ResponseVO.java deleted file mode 100644 index 6bc11bd..0000000 --- a/src/main/java/com/mini/capi/mail/vo/ResponseVO.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.mini.capi.mail.vo; - - -public class ResponseVO { - - // 状态码:0表示成功,其他表示失败 - private int code; - - // 消息 - private String message; - - // 数据 - private T data; - - // 成功响应 - public static ResponseVO success(T data, String message) { - ResponseVO response = new ResponseVO<>(); - response.code = 0; - response.message = message; - response.data = data; - return response; - } - - // 成功响应(默认消息) - public static ResponseVO success(T data) { - return success(data, "操作成功"); - } - - // 错误响应 - public static ResponseVO error(String message) { - ResponseVO response = new ResponseVO<>(); - response.code = 1; - response.message = message; - response.data = null; - return response; - } - -} diff --git a/src/main/java/com/mini/capi/sys/controller/MailController.java b/src/main/java/com/mini/capi/sys/controller/MailController.java new file mode 100644 index 0000000..2c90cd2 --- /dev/null +++ b/src/main/java/com/mini/capi/sys/controller/MailController.java @@ -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> 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> 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", "邮件发送成功")); + } +} diff --git a/src/main/java/com/mini/capi/utils/FileUtils.java b/src/main/java/com/mini/capi/utils/FileUtils.java index ab2b067..d0a4b4c 100644 --- a/src/main/java/com/mini/capi/utils/FileUtils.java +++ b/src/main/java/com/mini/capi/utils/FileUtils.java @@ -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); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mini/capi/utils/MailParseUtils.java b/src/main/java/com/mini/capi/utils/MailParseUtils.java new file mode 100644 index 0000000..8b01b35 --- /dev/null +++ b/src/main/java/com/mini/capi/utils/MailParseUtils.java @@ -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 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 extractAttachments(Part part) throws MessagingException, IOException { + List 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; + } + } +} \ No newline at end of file