This commit is contained in:
2025-09-22 18:14:20 +08:00
parent 4e00a3a6e9
commit 52e94288df
41 changed files with 2493 additions and 1 deletions

View File

@@ -0,0 +1,18 @@
package com.mini.capi.biz.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 邮件账户配置表 前端控制器
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
@RestController
@RequestMapping("/biz/mailAccount")
public class MailAccountController {
}

View File

@@ -0,0 +1,18 @@
package com.mini.capi.biz.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 附件表 前端控制器
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
@RestController
@RequestMapping("/biz/mailAttachment")
public class MailAttachmentController {
}

View File

@@ -0,0 +1,18 @@
package com.mini.capi.biz.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 接收邮件表 前端控制器
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
@RestController
@RequestMapping("/biz/mailReceived")
public class MailReceivedController {
}

View File

@@ -0,0 +1,18 @@
package com.mini.capi.biz.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 发送邮件表 前端控制器
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
@RestController
@RequestMapping("/biz/mailSent")
public class MailSentController {
}

View File

@@ -0,0 +1,125 @@
package com.mini.capi.biz.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.Setter;
/**
* <p>
* 邮件账户配置表
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
@Getter
@Setter
@TableName("biz_mail_account")
public class MailAccount implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 创建时间
*/
@TableField("create_time")
private LocalDateTime createTime;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 邮件服务器地址
*/
@TableField("host")
private String host;
/**
* SMTP端口
*/
@TableField("smtp_port")
private Integer smtpPort;
/**
* IMAP端口
*/
@TableField("imap_port")
private Integer imapPort;
/**
* 用户名
*/
@TableField("username")
private String username;
/**
* 密码
*/
@TableField("password")
private String password;
/**
* 发件人地址
*/
@TableField("from_address")
private String fromAddress;
/**
* 是否启用SSL
*/
@TableField("ssl_enable")
private Boolean sslEnable;
/**
* 状态0-禁用1-启用
*/
@TableField("status")
private Boolean status;
/**
* 备注
*/
@TableField("remark")
private String remark;
/**
* 更新时间
*/
@TableField("update_time")
private LocalDateTime updateTime;
/**
* 租户id
*/
@TableField("f_tenant_id")
private String fTenantId;
/**
* 流程id
*/
@TableField("f_flow_id")
private String fFlowId;
/**
* 流程任务主键
*/
@TableField("f_flow_task_id")
private String fFlowTaskId;
/**
* 流程任务状态
*/
@TableField("f_flow_state")
private Integer fFlowState;
}

View File

@@ -0,0 +1,122 @@
package com.mini.capi.biz.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.Setter;
/**
* <p>
* 附件表
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
@Getter
@Setter
@TableName("biz_mail_attachment")
public class MailAttachment implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 创建时间
*/
@TableField("create_time")
private LocalDateTime createTime;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 文件编号
*/
@TableField("file_no")
private String fileNo;
/**
* 目录
*/
@TableField("directory")
private String directory;
/**
* 原始文件名
*/
@TableField("original_file_name")
private String originalFileName;
/**
* 存储地址(目录+32位随机字符+拓展名)
*/
@TableField("storage_path")
private String storagePath;
/**
* 文件大小(字节)
*/
@TableField("file_size")
private Long fileSize;
/**
* 类型1-收件附件2-发件附件
*/
@TableField("type")
private Boolean type;
/**
* 关联的收件或发件ID
*/
@TableField("ref_id")
private Long refId;
/**
* 文件类型
*/
@TableField("content_type")
private String contentType;
/**
* 下载次数
*/
@TableField("download_count")
private Integer downloadCount;
/**
* 更新时间
*/
@TableField("update_time")
private LocalDateTime updateTime;
/**
* 租户id
*/
@TableField("f_tenant_id")
private String fTenantId;
/**
* 流程id
*/
@TableField("f_flow_id")
private String fFlowId;
/**
* 流程任务主键
*/
@TableField("f_flow_task_id")
private String fFlowTaskId;
/**
* 流程任务状态
*/
@TableField("f_flow_state")
private Integer fFlowState;
}

View File

@@ -0,0 +1,146 @@
package com.mini.capi.biz.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.Setter;
/**
* <p>
* 接收邮件表
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
@Getter
@Setter
@TableName("biz_mail_received")
public class MailReceived implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 创建时间
*/
@TableField("create_time")
private LocalDateTime createTime;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 邮件服务器消息ID
*/
@TableField("message_id")
private String messageId;
/**
* 关联的邮件账户ID
*/
@TableField("account_id")
private Long accountId;
/**
* 发件人地址
*/
@TableField("from_address")
private String fromAddress;
/**
* 发件人名称
*/
@TableField("from_name")
private String fromName;
/**
* 收件人地址,多个用逗号分隔
*/
@TableField("to_addresses")
private String toAddresses;
/**
* 抄送地址,多个用逗号分隔
*/
@TableField("cc_addresses")
private String ccAddresses;
/**
* 邮件主题
*/
@TableField("subject")
private String subject;
/**
* 邮件内容
*/
@TableField("content")
private String content;
/**
* 发送时间
*/
@TableField("send_time")
private LocalDateTime sendTime;
/**
* 接收时间
*/
@TableField("receive_time")
private LocalDateTime receiveTime;
/**
* 是否已读0-未读1-已读
*/
@TableField("is_read")
private Boolean isRead;
/**
* 是否有附件0-无1-有
*/
@TableField("has_attachment")
private Boolean hasAttachment;
/**
* 邮件文件夹
*/
@TableField("folder")
private String folder;
/**
* 更新时间
*/
@TableField("update_time")
private LocalDateTime updateTime;
/**
* 租户id
*/
@TableField("f_tenant_id")
private String fTenantId;
/**
* 流程id
*/
@TableField("f_flow_id")
private String fFlowId;
/**
* 流程任务主键
*/
@TableField("f_flow_task_id")
private String fFlowTaskId;
/**
* 流程任务状态
*/
@TableField("f_flow_state")
private Integer fFlowState;
}

View File

@@ -0,0 +1,134 @@
package com.mini.capi.biz.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.Setter;
/**
* <p>
* 发送邮件表
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
@Getter
@Setter
@TableName("biz_mail_sent")
public class MailSent implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 创建时间
*/
@TableField("create_time")
private LocalDateTime createTime;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 邮件服务器消息ID
*/
@TableField("message_id")
private String messageId;
/**
* 关联的邮件账户ID
*/
@TableField("account_id")
private Long accountId;
/**
* 发件人地址
*/
@TableField("from_address")
private String fromAddress;
/**
* 收件人地址,多个用逗号分隔
*/
@TableField("to_addresses")
private String toAddresses;
/**
* 抄送地址,多个用逗号分隔
*/
@TableField("cc_addresses")
private String ccAddresses;
/**
* 邮件主题
*/
@TableField("subject")
private String subject;
/**
* 邮件内容
*/
@TableField("content")
private String content;
/**
* 发送时间
*/
@TableField("send_time")
private LocalDateTime sendTime;
/**
* 发送状态0-待发送1-发送成功2-发送失败
*/
@TableField("send_status")
private Boolean sendStatus;
/**
* 错误信息
*/
@TableField("error_msg")
private String errorMsg;
/**
* 是否有附件0-无1-有
*/
@TableField("has_attachment")
private Boolean hasAttachment;
/**
* 更新时间
*/
@TableField("update_time")
private LocalDateTime updateTime;
/**
* 租户id
*/
@TableField("f_tenant_id")
private String fTenantId;
/**
* 流程id
*/
@TableField("f_flow_id")
private String fFlowId;
/**
* 流程任务主键
*/
@TableField("f_flow_task_id")
private String fFlowTaskId;
/**
* 流程任务状态
*/
@TableField("f_flow_state")
private Integer fFlowState;
}

View File

@@ -0,0 +1,16 @@
package com.mini.capi.biz.mapper;
import com.mini.capi.biz.domain.MailAccount;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 邮件账户配置表 Mapper 接口
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
public interface MailAccountMapper extends BaseMapper<MailAccount> {
}

View File

@@ -0,0 +1,16 @@
package com.mini.capi.biz.mapper;
import com.mini.capi.biz.domain.MailAttachment;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 附件表 Mapper 接口
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
public interface MailAttachmentMapper extends BaseMapper<MailAttachment> {
}

View File

@@ -0,0 +1,16 @@
package com.mini.capi.biz.mapper;
import com.mini.capi.biz.domain.MailReceived;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 接收邮件表 Mapper 接口
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
public interface MailReceivedMapper extends BaseMapper<MailReceived> {
}

View File

@@ -0,0 +1,16 @@
package com.mini.capi.biz.mapper;
import com.mini.capi.biz.domain.MailSent;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 发送邮件表 Mapper 接口
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
public interface MailSentMapper extends BaseMapper<MailSent> {
}

View File

@@ -0,0 +1,16 @@
package com.mini.capi.biz.service;
import com.mini.capi.biz.domain.MailAccount;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 邮件账户配置表 服务类
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
public interface MailAccountService extends IService<MailAccount> {
}

View File

@@ -0,0 +1,16 @@
package com.mini.capi.biz.service;
import com.mini.capi.biz.domain.MailAttachment;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 附件表 服务类
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
public interface MailAttachmentService extends IService<MailAttachment> {
}

View File

@@ -0,0 +1,16 @@
package com.mini.capi.biz.service;
import com.mini.capi.biz.domain.MailReceived;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 接收邮件表 服务类
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
public interface MailReceivedService extends IService<MailReceived> {
}

View File

@@ -0,0 +1,16 @@
package com.mini.capi.biz.service;
import com.mini.capi.biz.domain.MailSent;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 发送邮件表 服务类
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
public interface MailSentService extends IService<MailSent> {
}

View File

@@ -0,0 +1,20 @@
package com.mini.capi.biz.service.impl;
import com.mini.capi.biz.domain.MailAccount;
import com.mini.capi.biz.mapper.MailAccountMapper;
import com.mini.capi.biz.service.MailAccountService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 邮件账户配置表 服务实现类
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
@Service
public class MailAccountServiceImpl extends ServiceImpl<MailAccountMapper, MailAccount> implements MailAccountService {
}

View File

@@ -0,0 +1,20 @@
package com.mini.capi.biz.service.impl;
import com.mini.capi.biz.domain.MailAttachment;
import com.mini.capi.biz.mapper.MailAttachmentMapper;
import com.mini.capi.biz.service.MailAttachmentService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 附件表 服务实现类
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
@Service
public class MailAttachmentServiceImpl extends ServiceImpl<MailAttachmentMapper, MailAttachment> implements MailAttachmentService {
}

View File

@@ -0,0 +1,20 @@
package com.mini.capi.biz.service.impl;
import com.mini.capi.biz.domain.MailReceived;
import com.mini.capi.biz.mapper.MailReceivedMapper;
import com.mini.capi.biz.service.MailReceivedService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 接收邮件表 服务实现类
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
@Service
public class MailReceivedServiceImpl extends ServiceImpl<MailReceivedMapper, MailReceived> implements MailReceivedService {
}

View File

@@ -0,0 +1,20 @@
package com.mini.capi.biz.service.impl;
import com.mini.capi.biz.domain.MailSent;
import com.mini.capi.biz.mapper.MailSentMapper;
import com.mini.capi.biz.service.MailSentService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 发送邮件表 服务实现类
* </p>
*
* @author gaoxq
* @since 2025-09-22
*/
@Service
public class MailSentServiceImpl extends ServiceImpl<MailSentMapper, MailSent> implements MailSentService {
}

View File

@@ -0,0 +1,57 @@
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

@@ -0,0 +1,33 @@
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

@@ -0,0 +1,102 @@
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

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

View File

@@ -0,0 +1,18 @@
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

@@ -0,0 +1,12 @@
package com.mini.capi.mail.service;
public interface MailSendService {
/**
* 发送邮件
* @param account 邮件账户
* @param mailSendVO 邮件发送参数
* @return 发送成功的邮件ID
*/
Long sendEmail(MailAccount account, MailSendVO mailSendVO);
}

View File

@@ -0,0 +1,253 @@
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

@@ -0,0 +1,163 @@
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

@@ -0,0 +1,22 @@
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

@@ -0,0 +1,38 @@
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

@@ -29,7 +29,7 @@ public class demo {
.pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + "/src/main/resources/mapper"));
})
.strategyConfig(builder -> {
builder.addInclude("biz_ssh_servers")
builder.addInclude("biz_mail_account,biz_mail_received,biz_mail_sent,biz_mail_attachment")
.addTablePrefix("biz_")
.entityBuilder()
.enableLombok()

View File

@@ -0,0 +1,17 @@
package com.mini.capi.sys.domain;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
import java.io.Serializable;
import java.util.List;
@Data
public class SendMailDTO implements Serializable {
private Long accountId; // 邮件账户ID
private List<String> toAddresses; // 收件人地址列表
private List<String> ccAddresses; // 抄送地址列表
private String subject; // 邮件主题
private String content; // 邮件内容
private List<MultipartFile> attachments; // 附件列表
}

View File

@@ -0,0 +1,29 @@
package com.mini.capi.utils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class DateUtils {
/**
* 格式化当前日期
* @param pattern 格式模式
* @return 格式化后的日期字符串
*/
public static String formatCurrentDate(String pattern) {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(pattern));
}
/**
* 格式化指定日期
* @param date 日期
* @param pattern 格式模式
* @return 格式化后的日期字符串
*/
public static String formatDate(LocalDateTime date, String pattern) {
if (date == null) {
return "";
}
return date.format(DateTimeFormatter.ofPattern(pattern));
}
}

View File

@@ -0,0 +1,61 @@
package com.mini.capi.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
public class FileUtil {
private static final Logger logger = LoggerFactory.getLogger(FileUtil.class);
/**
* 保存输入流到文件
*/
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);
}
logger.info("文件保存成功: {}", filePath);
} catch (Exception e) {
logger.error("保存文件失败: " + filePath, e);
throw new RuntimeException("保存文件失败", e);
}
}
/**
* 复制文件
*/
public static void copyFile(File source, File dest) {
try {
Files.copy(source.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
logger.error("复制文件失败: {} -> {}", source.getPath(), dest.getPath(), e);
throw new RuntimeException("复制文件失败", e);
}
}
/**
* 删除文件
*/
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;
}
return false;
}
}

View File

@@ -0,0 +1,88 @@
package com.mini.capi.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.internet.MimeMessage;
import java.io.IOException;
public class MailUtil {
private static final Logger logger = LoggerFactory.getLogger(MailUtil.class);
/**
* 获取邮件内容
*/
public static String getEmailContent(Message message) {
try {
Object content = message.getContent();
// 简单文本内容
if (content instanceof String) {
return (String) content;
}
// 复杂内容(多部分)
else if (content instanceof Multipart) {
Multipart multipart = (Multipart) content;
StringBuilder contentBuilder = new StringBuilder();
// 遍历所有部分
for (int i = 0; i < multipart.getCount(); i++) {
BodyPart bodyPart = multipart.getBodyPart(i);
// 忽略附件
if (BodyPart.ATTACHMENT.equalsIgnoreCase(bodyPart.getDisposition()) ||
bodyPart.getFileName() != null) {
continue;
}
// 获取内容
Object partContent = bodyPart.getContent();
if (partContent instanceof String) {
contentBuilder.append(partContent);
} else if (partContent instanceof Multipart) {
// 处理嵌套的多部分内容
contentBuilder.append(getMultipartContent((Multipart) partContent));
}
}
return contentBuilder.toString();
}
} catch (Exception e) {
logger.error("获取邮件内容失败", e);
}
return "";
}
/**
* 处理嵌套的多部分内容
*/
private static String getMultipartContent(Multipart multipart) throws MessagingException, IOException {
StringBuilder contentBuilder = new StringBuilder();
for (int i = 0; i < multipart.getCount(); i++) {
BodyPart bodyPart = multipart.getBodyPart(i);
// 忽略附件
if (BodyPart.ATTACHMENT.equalsIgnoreCase(bodyPart.getDisposition()) ||
bodyPart.getFileName() != null) {
continue;
}
Object partContent = bodyPart.getContent();
if (partContent instanceof String) {
contentBuilder.append(partContent);
} else if (partContent instanceof Multipart) {
contentBuilder.append(getMultipartContent((Multipart) partContent));
}
}
return contentBuilder.toString();
}
}