Quellcode durchsuchen

邮件服务接口增加和调整,业务拆分

1811872455@163.com vor 2 Wochen
Ursprung
Commit
1ae641982a
18 geänderte Dateien mit 532 neuen und 204 gelöschten Zeilen
  1. 14 5
      storlead-api/src/main/java/com/storlead/mail/EmailFolderRuleApiController.java
  2. 31 9
      storlead-api/src/main/java/com/storlead/mail/MaiAttachmentApiController.java
  3. 97 38
      storlead-api/src/main/java/com/storlead/mail/MailApiController.java
  4. 13 5
      storlead-api/src/main/java/com/storlead/mail/MailBlacklistRecordApiController.java
  5. 16 6
      storlead-api/src/main/java/com/storlead/mail/MailTemplatesApiController.java
  6. 18 5
      storlead-api/src/main/java/com/storlead/mail/MailboxAutoReplySetController.java
  7. 28 6
      storlead-api/src/main/java/com/storlead/mail/SmtpPopSettingsApiController.java
  8. 8 7
      storlead-api/src/main/java/com/storlead/mail/ThumbnailPreviewController.java
  9. 13 5
      storlead-api/src/main/java/com/storlead/mail/UserEmailFolderApiController.java
  10. 19 10
      storlead-api/src/main/resources/application-dev.yml
  11. 30 17
      storlead-api/src/main/resources/application-test.yml
  12. 1 1
      storlead-mail/src/main/java/com/storlead/sales/mail/dispatch/AutoSendDelayEmailTaskJob.java
  13. 1 1
      storlead-mail/src/main/java/com/storlead/sales/mail/dispatch/DelayDeleteServerEmailTaskJob.java
  14. 1 1
      storlead-mail/src/main/java/com/storlead/sales/mail/dispatch/EmailPullHeadContentTaskJob.java
  15. 0 85
      storlead-mail/src/main/java/com/storlead/sales/mail/integration/scheduler/MailIntegrationTaskScheduler.java
  16. 156 0
      storlead-mail/src/main/java/com/storlead/sales/mail/support/MailOwnershipGuard.java
  17. 74 0
      storlead-mail/src/main/java/com/storlead/sales/mail/util/SafeFilePathResolver.java
  18. 12 3
      storlead-mail/src/main/java/com/storlead/sales/mail/util/ZipUtility.java

+ 14 - 5
storlead-api/src/main/java/com/storlead/mail/EmailFolderRuleApiController.java

@@ -16,6 +16,7 @@ import com.storlead.sales.mail.pojo.dto.EmailTemplatesDTO;
 import com.storlead.sales.mail.pojo.dto.FolderRuleDTO;
 import com.storlead.sales.mail.service.EmailFolderRuleService;
 import com.storlead.sales.mail.service.EmailsService;
+import com.storlead.sales.mail.support.MailOwnershipGuard;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiResponse;
@@ -50,6 +51,9 @@ public class EmailFolderRuleApiController {
     @Resource
     private EmailsService emailsService;
 
+    @Resource
+    private MailOwnershipGuard mailOwnershipGuard;
+
     @PostMapping(value = "/get_folder_rule")
     @ApiOperation(value = "获取规则" )
     public Result<?> getFolderRule() {
@@ -111,6 +115,9 @@ public class EmailFolderRuleApiController {
             }
         } else {
 
+            if (mailOwnershipGuard.getOwnedEntity(folderRuleService, entity.getId(), currentUserId) == null) {
+                return Result.error("无权操作");
+            }
             EmailFolderRuleEntity oldFolder = folderRuleService.getOne(w);
             if (Objects.nonNull(oldFolder)) {
                 if (!oldFolder.getId().equals(entity.getId())) {
@@ -119,6 +126,7 @@ public class EmailFolderRuleApiController {
             }
         }
 
+        entity.setOwnerBy(currentUserId);
         folderRuleService.saveOrUpdate(entity);
         return Result.ok();
     }
@@ -136,6 +144,7 @@ public class EmailFolderRuleApiController {
         LambdaQueryWrapper<EmailFolderRuleEntity> deleteW = new LambdaQueryWrapper<>();
         deleteW.eq(EmailFolderRuleEntity::getOwnerBy,currentUserId);
         folderRuleService.remove(deleteW);
+        dto.getFolderRules().forEach(rule -> rule.setOwnerBy(currentUserId));
         folderRuleService.saveOrUpdateBatch(dto.getFolderRules());
         return Result.ok();
     }
@@ -146,10 +155,12 @@ public class EmailFolderRuleApiController {
         if(Objects.isNull(ids)) {
             return Result.error("参数错误");
         }
-        folderRuleService.lgDelete(Arrays.asList(ids));
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
+        mailOwnershipGuard.softDeleteOwned(folderRuleService, Arrays.asList(ids), currentUserId);
 
         LambdaUpdateWrapper<EmailsEntity> updateWrapper = new LambdaUpdateWrapper<>();
         updateWrapper.in(EmailsEntity::getCustomFolderId,Arrays.asList(ids));
+        updateWrapper.eq(EmailsEntity::getOwnerBy, currentUserId);
         updateWrapper.set(EmailsEntity::getCustomFolderId,null);
         emailsService.update(updateWrapper);
         return Result.ok();
@@ -161,10 +172,8 @@ public class EmailFolderRuleApiController {
     @ApiOperation("禁用或启用")
     @PostMapping("enabled")
     public Result enabled(Long id,Boolean enable) {
-        LambdaUpdateWrapper<EmailFolderRuleEntity> updateWrapper = new LambdaUpdateWrapper<>();
-        updateWrapper.set(EmailFolderRuleEntity::getEnabled,enable);
-        updateWrapper.eq(EmailFolderRuleEntity::getId,id);
-        folderRuleService.update(updateWrapper);
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
+        mailOwnershipGuard.updateEnabledOwned(folderRuleService, id, enable, currentUserId);
         return Result.ok();
     }
 }

+ 31 - 9
storlead-api/src/main/java/com/storlead/mail/MaiAttachmentApiController.java

@@ -9,7 +9,9 @@ import com.storlead.sales.mail.entity.SmtpPopSettingsEntity;
 import com.storlead.sales.mail.properties.MailFileProperties;
 import com.storlead.sales.mail.service.MailTempAttachmentService;
 import com.storlead.sales.mail.service.SmtpPopSettingsService;
+import com.storlead.sales.mail.support.MailOwnershipGuard;
 import com.storlead.sales.mail.util.FileHelper;
+import com.storlead.sales.mail.util.SafeFilePathResolver;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.extern.log4j.Log4j2;
@@ -28,6 +30,8 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.InputStream;
 import java.net.URLEncoder;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.*;
 
 /**
@@ -49,6 +53,9 @@ public class MaiAttachmentApiController {
     @Resource
     private MailTempAttachmentService tempAttachmentService;
 
+    @Resource
+    private MailOwnershipGuard mailOwnershipGuard;
+
     @PostMapping(value = "/cache_mail_file")
     @ApiOperation(value = "缓存需要上传的附件" )
     public Result<?> cacheMailFile(@RequestParam("files") List<MultipartFile> files) {
@@ -110,9 +117,14 @@ public class MaiAttachmentApiController {
     @ApiOperation(value = "下载临时附件" )
     public void downLoadMailFile(Long fileId, HttpServletResponse response) {
         try {
-            MailTempAttachmentEntity resource = tempAttachmentService.getById(fileId);
-            String downloadDir = mailFileProperties.getPath().getPath() +resource.getFilePath();
-            InputStream inputStream = new BufferedInputStream(new FileInputStream(downloadDir));
+            Long currentUserId = LoginUserUtil.getCurrentUserId();
+            MailTempAttachmentEntity resource = mailOwnershipGuard.getOwnedTempAttachment(fileId, currentUserId);
+            if (resource == null) {
+                return;
+            }
+            Path filePath = SafeFilePathResolver.resolveUnderBase(
+                    mailFileProperties.getPath().getPath(), resource.getFilePath());
+            InputStream inputStream = new BufferedInputStream(new FileInputStream(filePath.toFile()));
             response.reset();
             response.setContentType("application/octet-stream;charset=utf-8");
             /*
@@ -138,6 +150,8 @@ public class MaiAttachmentApiController {
             inputStream.close();
             outputStream.close();
 
+        } catch (IllegalArgumentException e) {
+            log.warn("download_mail_file rejected unsafe path, fileId={}", fileId);
         } catch (Exception e) {
             log.error("--upload -- error =", e);
         }
@@ -145,11 +159,19 @@ public class MaiAttachmentApiController {
 
     @GetMapping("/images/{imageName}")
     public ResponseEntity<FileSystemResource> getImage(@PathVariable String imageName) {
-
-        String downloadDir = mailFileProperties.getPath().getJpeg();
-        FileSystemResource resource = new FileSystemResource(downloadDir + imageName);
-        return ResponseEntity.ok()
-                .contentType(MediaType.IMAGE_JPEG)  // 根据图片类型调整
-                .body(resource);
+        try {
+            String jpegRoot = mailFileProperties.getPath().getJpeg();
+            Path filePath = SafeFilePathResolver.resolveFileNameUnderBase(jpegRoot, imageName);
+            if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
+                return ResponseEntity.notFound().build();
+            }
+            FileSystemResource resource = new FileSystemResource(filePath.toFile());
+            return ResponseEntity.ok()
+                    .contentType(MediaType.IMAGE_JPEG)
+                    .body(resource);
+        } catch (IllegalArgumentException e) {
+            log.warn("getImage rejected unsafe name: {}", imageName);
+            return ResponseEntity.badRequest().build();
+        }
     }
 }

+ 97 - 38
storlead-api/src/main/java/com/storlead/mail/MailApiController.java

@@ -18,7 +18,9 @@ import com.storlead.sales.mail.enums.SendStatus;
 import com.storlead.sales.mail.pojo.*;
 import com.storlead.sales.mail.properties.MailFileProperties;
 import com.storlead.sales.mail.service.*;
+import com.storlead.sales.mail.support.MailOwnershipGuard;
 import com.storlead.sales.mail.util.FileHelper;
+import com.storlead.sales.mail.util.SafeFilePathResolver;
 import com.storlead.sales.mail.util.ZipUtility;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
@@ -85,6 +87,9 @@ public class MailApiController {
     @Resource
     private Environment environment;
 
+    @Resource
+    private MailOwnershipGuard mailOwnershipGuard;
+
     @PostMapping(value = "/send_mail")
     @ApiOperation(value = "发送邮件")
     @ApiResponses({
@@ -149,6 +154,9 @@ public class MailApiController {
             entity.setRecipientBcc(org.apache.commons.lang3.StringUtils.join(dto.getRecipientBccls(), ","));
         }
         if (Objects.nonNull(dto.getId())) {
+            if (mailOwnershipGuard.getOwnedEmail(dto.getId(), currentUserId) == null) {
+                return Result.error("无权操作该邮件");
+            }
             entity.setId(dto.getId());
         }
 
@@ -162,12 +170,22 @@ public class MailApiController {
 
         emailsService.saveOrUpdate(entity);
         if (!CollectionUtils.isEmpty(dto.getFileIds())) {
+            for (Long fileId : dto.getFileIds()) {
+                if (mailOwnershipGuard.getOwnedTempAttachment(fileId, currentUserId) == null) {
+                    return Result.error("无权操作附件");
+                }
+            }
             LambdaUpdateWrapper<MailTempAttachmentEntity> fileWrapper = new LambdaUpdateWrapper<>();
             fileWrapper.set(MailTempAttachmentEntity::getEmailId, entity.getId());
             fileWrapper.in(MailTempAttachmentEntity::getId, dto.getFileIds());
             tempAttachmentService.update(fileWrapper);
         }
         if (!CollectionUtils.isEmpty(dto.getOldfileIds())) {
+            for (Long fileId : dto.getOldfileIds()) {
+                if (mailOwnershipGuard.getOwnedAttachment(fileId, currentUserId) == null) {
+                    return Result.error("无权操作附件");
+                }
+            }
             LambdaUpdateWrapper<MailAttachmentEntity> fileWrapper = new LambdaUpdateWrapper<>();
             fileWrapper.in(MailAttachmentEntity::getId, dto.getOldfileIds());
             List<MailAttachmentEntity> mailAttachments = mailAttachmentService.list(fileWrapper);
@@ -217,7 +235,7 @@ public class MailApiController {
         if (CollectionUtils.isEmpty(dto.getMailIds())) {
             return;
         }
-        List<EmailsEntity> entities = emailsService.list(new LambdaQueryWrapper<EmailsEntity>().in(EmailsEntity::getId, dto.getMailIds()));
+        List<EmailsEntity> entities = mailOwnershipGuard.listOwnedEmails(dto.getMailIds(), currentUserId);
         if (CollectionUtils.isEmpty(entities)) {
             return;
         }
@@ -285,7 +303,8 @@ public class MailApiController {
         if (Objects.isNull(dto.getMailId())) {
             return;
         }
-        EmailsEntity entitie = emailsService.getById(dto.getMailId());
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
+        EmailsEntity entitie = mailOwnershipGuard.getOwnedEmail(dto.getMailId(), currentUserId);
         if (Objects.isNull(entitie)) {
             return;
         }
@@ -402,7 +421,7 @@ public class MailApiController {
                 // 设置文本消息部分
                 for (MailTempAttachmentEntity attachment : tempAttachment) {
                     BodyPart messageBodyPart = new MimeBodyPart();
-                    String filePath = mailFileProperties.getPath().getPath() + attachment.getFilePath();
+                    String filePath = resolveMailFile(attachment.getFilePath()).toString();
                     messageBodyPart = new MimeBodyPart();
                     String fileName = attachment.getFileName() + "." + attachment.getFileExt();
                     DataSource fileSource = new FileDataSource(filePath);
@@ -479,9 +498,11 @@ public class MailApiController {
         if (Objects.isNull(ids)) {
             return Result.error("参数错误");
         }
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
         LambdaUpdateWrapper<EmailsEntity> uWrapper = new LambdaUpdateWrapper<>();
         uWrapper.set(EmailsEntity::getIsDelete, Integer.valueOf(1));
         uWrapper.in(EmailsEntity::getId, Arrays.asList(ids));
+        uWrapper.eq(EmailsEntity::getOwnerBy, currentUserId);
         emailsService.update(uWrapper);
         return Result.ok();
     }
@@ -495,8 +516,10 @@ public class MailApiController {
         if (Objects.isNull(ids)) {
             return Result.error("删除草稿");
         }
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
         LambdaUpdateWrapper<EmailsEntity> uWrapper = new LambdaUpdateWrapper<>();
         uWrapper.in(EmailsEntity::getId, Arrays.asList(ids));
+        uWrapper.eq(EmailsEntity::getOwnerBy, currentUserId);
         emailsService.remove(uWrapper);
         return Result.ok();
     }
@@ -510,8 +533,12 @@ public class MailApiController {
         if (Objects.isNull(mailId)) {
             return Result.error("参数错误!");
         }
-        EmailsEntity email = emailsService.getById(mailId);
-        if (Objects.nonNull(email) && EmailBoxEnum.DRAFT.code.equals(email.getFolder())) {
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
+        EmailsEntity email = mailOwnershipGuard.getOwnedEmail(mailId, currentUserId);
+        if (email == null) {
+            return Result.error("邮件不存在或无权访问");
+        }
+        if (EmailBoxEnum.DRAFT.code.equals(email.getFolder())) {
             List<MailTempAttachmentEntity> atts = tempAttachmentService.list(new LambdaQueryWrapper<MailTempAttachmentEntity>().eq(MailTempAttachmentEntity::getEmailId, mailId));
             if (!CollectionUtils.isEmpty(atts)) {
                 atts.forEach(e -> {
@@ -538,9 +565,17 @@ public class MailApiController {
         if (Objects.isNull(mailIds) || Objects.isNull(folderId)) {
             return Result.error("参数错误");
         }
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
+        if (mailOwnershipGuard.getOwnedFolder(folderId, currentUserId) == null) {
+            return Result.error("文件夹不存在或无权访问");
+        }
+        if (!mailOwnershipGuard.ownsAllEmails(Arrays.asList(mailIds), currentUserId)) {
+            return Result.error("无权操作邮件");
+        }
         LambdaUpdateWrapper<EmailsEntity> updateWrapper = new LambdaUpdateWrapper<>();
         updateWrapper.set(EmailsEntity::getCustomFolderId, folderId);
         updateWrapper.in(EmailsEntity::getId, Arrays.asList(mailIds));
+        updateWrapper.eq(EmailsEntity::getOwnerBy, currentUserId);
         emailsService.update(updateWrapper);
         return Result.ok();
     }
@@ -551,8 +586,10 @@ public class MailApiController {
         if (Objects.isNull(mailIds)) {
             return Result.error("参数错误");
         }
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
         LambdaUpdateWrapper<EmailsEntity> updateWrapper = new LambdaUpdateWrapper<>();
         updateWrapper.in(EmailsEntity::getId, Arrays.asList(mailIds));
+        updateWrapper.eq(EmailsEntity::getOwnerBy, currentUserId);
         updateWrapper.set(EmailsEntity::getCustomFolderId, null);
         emailsService.update(updateWrapper);
         return Result.ok();
@@ -589,8 +626,10 @@ public class MailApiController {
         if (Objects.isNull(dto.getEmailId()) && StringUtils.isEmpty(dto.getEmailIds())) {
             return Result.error("参数错误!");
         }
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
         LambdaUpdateWrapper<EmailsEntity> uWrapper = new LambdaUpdateWrapper<>();
         uWrapper.set(EmailsEntity::getIsRead, dto.getIsRead());
+        uWrapper.eq(EmailsEntity::getOwnerBy, currentUserId);
         if (Objects.nonNull(dto.getEmailId())) {
             uWrapper.eq(EmailsEntity::getId, dto.getEmailId());
         } else {
@@ -609,8 +648,10 @@ public class MailApiController {
         if (Objects.isNull(dto.getEmailId()) && StringUtils.isEmpty(dto.getEmailIds())) {
             return Result.error("参数错误!");
         }
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
         LambdaUpdateWrapper<EmailsEntity> uWrapper = new LambdaUpdateWrapper<>();
         uWrapper.set(EmailsEntity::getReplyMsgId, dto.getReplyMsgId());
+        uWrapper.eq(EmailsEntity::getOwnerBy, currentUserId);
         if (Objects.nonNull(dto.getEmailId())) {
             uWrapper.eq(EmailsEntity::getId, dto.getEmailId());
         } else {
@@ -704,12 +745,19 @@ public class MailApiController {
         if (Objects.isNull(dto.getMailId()) && CollectionUtils.isEmpty(dto.getAttachmentIds())) {
             return;
         }
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
         String zipFilePath = mailFileProperties.getPath().getPath() + System.currentTimeMillis() + RandomGenerateHelper.generateRandomString(6) + ".zip";
         List<MailAttachmentEntity> attachments;
         if (Objects.nonNull(dto.getMailId())) {
+            if (mailOwnershipGuard.getOwnedEmail(dto.getMailId(), currentUserId) == null) {
+                return;
+            }
             attachments = mailAttachmentService.list(new LambdaQueryWrapper<MailAttachmentEntity>().eq(MailAttachmentEntity::getEmailId, dto.getMailId()).eq(MailAttachmentEntity::getDownload, Integer.valueOf(1)));
         } else {
             attachments = mailAttachmentService.list(new LambdaQueryWrapper<MailAttachmentEntity>().in(MailAttachmentEntity::getId, dto.getAttachmentIds()).eq(MailAttachmentEntity::getDownload, Integer.valueOf(1)));
+            attachments = attachments.stream()
+                    .filter(a -> mailOwnershipGuard.getOwnedAttachment(a.getId(), currentUserId) != null)
+                    .collect(Collectors.toList());
         }
         if (CollectionUtils.isEmpty(attachments)) {
             return;
@@ -754,36 +802,33 @@ public class MailApiController {
             if (attachmentId == null) {
                 return Result.error("附未找到");
             }
-            MailAttachmentEntity attachment = mailAttachmentService.getById(attachmentId);
+            Long currentUserId = LoginUserUtil.getCurrentUserId();
+            MailAttachmentEntity attachment = mailOwnershipGuard.getOwnedAttachment(attachmentId, currentUserId);
             if (attachment == null) {
                 return Result.error("附未找到");
             }
 
-            String url =  mailFileProperties.getPath().getPath() + File.separator + attachment.getFilePath();
-
+            Path filePath = resolveMailFile(attachment.getFilePath());
+            if (StrUtil.isNotBlank(attachment.getFileCode())) {
+                String alt = attachment.getFilePath().replace("." + attachment.getFileCode(), "");
+                if (!alt.equals(attachment.getFilePath())) {
+                    filePath = resolveMailFile(alt);
+                }
+            }
 
-            // String newFileName = attachment.getFilePath().replace("."+attachment.getFileCode(),"");
             File exportDir;
             String active = environment.getProperty("spring.profiles.active");
             String port =  environment.getProperty("server.port");
             if (active.equals("dev")) {
                 exportDir = new File("D:\\mail\\tempfiles");
             } else {
-                url = url.replace("\\", "/");
                 exportDir = new File("/app/tempfiles");
             }
-            Path filePath = Paths.get(url);
             if (!exportDir.exists()) {
                 exportDir.mkdirs();
             }
 
-            Path targetPath1;
-            if (StrUtil.isNotBlank(attachment.getFileCode())) {
-                targetPath1 = Paths.get(url.replace("."+attachment.getFileCode(),""));
-            } else {
-                targetPath1 = Paths.get(url);
-            }
-            String fileName = targetPath1.getFileName().toString();
+            String fileName = filePath.getFileName().toString();
 
             Path target = Paths.get(exportDir.getAbsolutePath());
             // 创建目标目录(如果不存在)
@@ -805,6 +850,9 @@ public class MailApiController {
                 domainUrl = "http://localhost:"+port+fileUrl;
             }
             return Result.result(domainUrl);
+        } catch (IllegalArgumentException e) {
+            log.warn("download_url rejected unsafe path, attachmentId={}", attachmentId);
+            return Result.error("附未找到");
         } catch (Exception e) {
             log.error("download--error", e);
             return Result.error("附未找到");
@@ -818,12 +866,19 @@ public class MailApiController {
             if (attachmentId == null) {
                 return Result.error("附未找到");
             }
-            MailTempAttachmentEntity attachment = tempAttachmentService.getById(attachmentId);
+            Long currentUserId = LoginUserUtil.getCurrentUserId();
+            MailTempAttachmentEntity attachment = mailOwnershipGuard.getOwnedTempAttachment(attachmentId, currentUserId);
             if (attachment == null) {
                 return Result.error("附未找到");
             }
 
-            String url =  mailFileProperties.getPath().getPath() + File.separator + attachment.getFilePath();
+            Path filePath = resolveMailFile(attachment.getFilePath());
+            if (StrUtil.isNotBlank(attachment.getFileCode())) {
+                String alt = attachment.getFilePath().replace("." + attachment.getFileCode(), "");
+                if (!alt.equals(attachment.getFilePath())) {
+                    filePath = resolveMailFile(alt);
+                }
+            }
 
             File exportDir;
             String active = environment.getProperty("spring.profiles.active");
@@ -831,20 +886,12 @@ public class MailApiController {
             if (active.equals("dev")) {
                 exportDir = new File("D:\\mail\\tempfiles");
             } else {
-                url = url.replace("\\", "/");
                 exportDir = new File("/app/tempfiles");
             }
-            Path filePath = Paths.get(url);
             if (!exportDir.exists()) {
                 exportDir.mkdirs();
             }
-            Path targetPath1;
-            if (StrUtil.isNotBlank(attachment.getFileCode())) {
-                targetPath1 = Paths.get(url.replace("."+attachment.getFileCode(),""));
-            } else {
-                targetPath1 = Paths.get(url);
-            }
-            String fileName = targetPath1.getFileName().toString();
+            String fileName = filePath.getFileName().toString();
 
             Path target = Paths.get(exportDir.getAbsolutePath());
             // 创建目标目录(如果不存在)
@@ -866,6 +913,9 @@ public class MailApiController {
                 domainUrl = "http://localhost:"+port+fileUrl;
             }
             return Result.result(domainUrl);
+        } catch (IllegalArgumentException e) {
+            log.warn("draft_download_url rejected unsafe path, attachmentId={}", attachmentId);
+            return Result.error("附未找到");
         } catch (Exception e) {
             log.error("download--error", e);
             return Result.error("附未找到");
@@ -879,13 +929,13 @@ public class MailApiController {
             if (attachmentId == null) {
                 return;
             }
-            MailTempAttachmentEntity attachment = tempAttachmentService.getById(attachmentId);
+            Long currentUserId = LoginUserUtil.getCurrentUserId();
+            MailTempAttachmentEntity attachment = mailOwnershipGuard.getOwnedTempAttachment(attachmentId, currentUserId);
             if (attachment == null) {
                 return;
             }
-            InputStream inputStream;
-            String url = mailFileProperties.getPath().getPath() + attachment.getFilePath();
-            inputStream = new BufferedInputStream(new FileInputStream(url));
+            Path filePath = resolveMailFile(attachment.getFilePath());
+            InputStream inputStream = new BufferedInputStream(new FileInputStream(filePath.toFile()));
             response.reset();
             response.setContentType("application/octet-stream;charset=utf-8");
             /*
@@ -909,6 +959,8 @@ public class MailApiController {
             }
             inputStream.close();
             outputStream.close();
+        } catch (IllegalArgumentException e) {
+            log.warn("draft_download rejected unsafe path, attachmentId={}", attachmentId);
         } catch (Exception e) {
             log.error("download--error", e);
         }
@@ -921,14 +973,16 @@ public class MailApiController {
             if (attachmentId == null) {
                 return;
             }
-            MailAttachmentEntity attachment = mailAttachmentService.getById(attachmentId);
+            Long currentUserId = LoginUserUtil.getCurrentUserId();
+            MailAttachmentEntity attachment = mailOwnershipGuard.getOwnedAttachment(attachmentId, currentUserId);
             if (attachment == null) {
                 return;
             }
             InputStream inputStream;
-            String url = mailFileProperties.getPath().getPath() + attachment.getFilePath();
+            Path filePath = resolveMailFile(attachment.getFilePath());
+            String url = filePath.toString();
             if (Integer.valueOf(0).equals(attachment.getDownload())) {
-                if (new File(url).exists()) {
+                if (Files.exists(filePath)) {
                     LambdaUpdateWrapper<MailAttachmentEntity> attachmentUpdate = new LambdaUpdateWrapper<>();
                     attachmentUpdate.set(MailAttachmentEntity::getDownload, Integer.valueOf(1));
                     attachmentUpdate.eq(MailAttachmentEntity::getId, attachment.getId());
@@ -941,8 +995,7 @@ public class MailApiController {
                 if (Objects.isNull(inputStream)) {
                     return;
                 }
-                String path = mailFileProperties.getPath().getPath() + attachment.getFilePath();
-                File file = new File(path);
+                File file = filePath.toFile();
                 try (FileOutputStream output = new FileOutputStream(file)) {
                     inputStream.transferTo(output);
                 }
@@ -998,6 +1051,8 @@ public class MailApiController {
             }
             inputStream.close();
             outputStream.close();
+        } catch (IllegalArgumentException e) {
+            log.warn("download rejected unsafe path, attachmentId={}", attachmentId);
         } catch (Exception e) {
             log.error("download--error", e);
         }
@@ -1064,4 +1119,8 @@ public class MailApiController {
         return tempFilePath;
     }
 
+    private Path resolveMailFile(String relativePath) {
+        return SafeFilePathResolver.resolveUnderBase(mailFileProperties.getPath().getPath(), relativePath);
+    }
+
 }

+ 13 - 5
storlead-api/src/main/java/com/storlead/mail/MailBlacklistRecordApiController.java

@@ -15,6 +15,7 @@ import com.storlead.sales.mail.enums.EmailBoxEnum;
 import com.storlead.sales.mail.pojo.dto.EmailTemplatesDTO;
 import com.storlead.sales.mail.service.EmailBlacklistRecordService;
 import com.storlead.sales.mail.service.EmailsService;
+import com.storlead.sales.mail.support.MailOwnershipGuard;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiResponse;
@@ -46,6 +47,9 @@ public class MailBlacklistRecordApiController {
     @Resource
     private EmailsService emailsService;
 
+    @Resource
+    private MailOwnershipGuard mailOwnershipGuard;
+
     @PostMapping(value = "/page_list")
     @ApiOperation(value = "获取邮件模板" )
     @ApiResponses({
@@ -97,6 +101,11 @@ public class MailBlacklistRecordApiController {
         if (record != null){
             return Result.error("该邮箱已在黑名单中已存在");
         }
+        if (Objects.nonNull(entity.getId())
+                && mailOwnershipGuard.getOwnedEntity(blacklistRecordService, entity.getId(), currentUserId) == null) {
+            return Result.error("无权操作");
+        }
+        entity.setOwnerBy(currentUserId);
         Boolean b = blacklistRecordService.saveOrUpdate(entity);
         if (b) {
             /** 将黑名单邮箱的邮件移到垃圾箱中 **/
@@ -116,7 +125,8 @@ public class MailBlacklistRecordApiController {
         if(Objects.isNull(ids)) {
             return Result.error("参数错误");
         }
-        blacklistRecordService.lgDelete(Arrays.asList(ids));
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
+        mailOwnershipGuard.softDeleteOwned(blacklistRecordService, Arrays.asList(ids), currentUserId);
         return Result.ok();
     }
 
@@ -124,10 +134,8 @@ public class MailBlacklistRecordApiController {
     @ApiOperation("禁用或启用")
     @PostMapping("enabled")
     public Result enabled(Long id,Boolean enable) {
-        LambdaUpdateWrapper<EmailBlacklistRecordEntity> updateWrapper = new LambdaUpdateWrapper<>();
-        updateWrapper.set(EmailBlacklistRecordEntity::getEnabled,enable);
-        updateWrapper.eq(EmailBlacklistRecordEntity::getId,id);
-        blacklistRecordService.update(updateWrapper);
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
+        mailOwnershipGuard.updateEnabledOwned(blacklistRecordService, id, enable, currentUserId);
         return Result.ok();
     }
 }

+ 16 - 6
storlead-api/src/main/java/com/storlead/mail/MailTemplatesApiController.java

@@ -11,6 +11,7 @@ import com.storlead.sales.mail.entity.EmailTemplatesEntity;
 import com.storlead.sales.mail.entity.SmtpPopSettingsEntity;
 import com.storlead.sales.mail.pojo.dto.EmailTemplatesDTO;
 import com.storlead.sales.mail.service.EmailTemplatesService;
+import com.storlead.sales.mail.support.MailOwnershipGuard;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiResponse;
@@ -39,6 +40,9 @@ public class MailTemplatesApiController {
     @Resource
     private EmailTemplatesService templatesService;
 
+    @Resource
+    private MailOwnershipGuard mailOwnershipGuard;
+
     @PostMapping(value = "/page_list")
     @ApiOperation(value = "获取邮件模板" )
     @ApiResponses({
@@ -65,7 +69,8 @@ public class MailTemplatesApiController {
             @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
     })
     public Result<?> getById(Long id) {
-        EmailTemplatesEntity templatesEntity = templatesService.getById(id);
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
+        EmailTemplatesEntity templatesEntity = mailOwnershipGuard.getOwnedEntity(templatesService, id, currentUserId);
         return Result.result(templatesEntity);
     }
 
@@ -97,6 +102,12 @@ public class MailTemplatesApiController {
             @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
     })
     public Result<?> save(EmailTemplatesEntity entity) {
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
+        if (Objects.nonNull(entity.getId())
+                && mailOwnershipGuard.getOwnedEntity(templatesService, entity.getId(), currentUserId) == null) {
+            return Result.error("无权操作");
+        }
+        entity.setOwnerBy(currentUserId);
         templatesService.saveOrUpdate(entity);
         return Result.ok();
     }
@@ -107,7 +118,8 @@ public class MailTemplatesApiController {
         if(Objects.isNull(ids)) {
             return Result.error("参数错误");
         }
-        templatesService.lgDelete(Arrays.asList(ids));
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
+        mailOwnershipGuard.softDeleteOwned(templatesService, Arrays.asList(ids), currentUserId);
         return Result.ok();
     }
 
@@ -115,10 +127,8 @@ public class MailTemplatesApiController {
     @ApiOperation("禁用或启用")
     @PostMapping("enabled")
     public Result enabled(Long id,Boolean enable) {
-        LambdaUpdateWrapper<EmailTemplatesEntity> updateWrapper = new LambdaUpdateWrapper<>();
-        updateWrapper.set(EmailTemplatesEntity::getEnabled,enable);
-        updateWrapper.eq(EmailTemplatesEntity::getId,id);
-        templatesService.update(updateWrapper);
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
+        mailOwnershipGuard.updateEnabledOwned(templatesService, id, enable, currentUserId);
         return Result.ok();
     }
 }

+ 18 - 5
storlead-api/src/main/java/com/storlead/mail/MailboxAutoReplySetController.java

@@ -13,6 +13,7 @@ import com.storlead.sales.mail.entity.SmtpPopSettingsEntity;
 import com.storlead.sales.mail.pojo.dto.EmailTemplatesDTO;
 import com.storlead.sales.mail.service.MailboxAutoReplySetService;
 import com.storlead.sales.mail.service.SmtpPopSettingsService;
+import com.storlead.sales.mail.support.MailOwnershipGuard;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiResponse;
@@ -50,6 +51,9 @@ public class MailboxAutoReplySetController {
     @Resource
     private SmtpPopSettingsService popSettingsService;
 
+    @Resource
+    private MailOwnershipGuard mailOwnershipGuard;
+
     @PostMapping(value = "/page_list")
     @ApiOperation(value = "获取邮件自动回复配置" )
     @ApiResponses({
@@ -109,6 +113,16 @@ public class MailboxAutoReplySetController {
             @ApiResponse(code = 200, message = "", response = MailboxAutoReplySetEntity.class)
     })
     public Result<?> save(MailboxAutoReplySetEntity entity) {
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
+        if (Objects.nonNull(entity.getId())
+                && mailOwnershipGuard.getOwnedEntity(replySetService, entity.getId(), currentUserId) == null) {
+            return Result.error("无权操作");
+        }
+        if (Objects.nonNull(entity.getSmtpPopId())
+                && mailOwnershipGuard.getOwnedSmtpPop(entity.getSmtpPopId(), currentUserId) == null) {
+            return Result.error("无权操作");
+        }
+        entity.setOwnerBy(currentUserId);
         replySetService.saveOrUpdate(entity);
         return Result.ok();
     }
@@ -119,7 +133,8 @@ public class MailboxAutoReplySetController {
         if(Objects.isNull(ids)) {
             return Result.error("参数错误");
         }
-        replySetService.lgDelete(Arrays.asList(ids));
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
+        mailOwnershipGuard.softDeleteOwned(replySetService, Arrays.asList(ids), currentUserId);
         return Result.ok();
     }
 
@@ -127,10 +142,8 @@ public class MailboxAutoReplySetController {
     @ApiOperation("禁用或启用")
     @PostMapping("enabled")
     public Result enabled(Long id,Boolean enable) {
-        LambdaUpdateWrapper<MailboxAutoReplySetEntity> updateWrapper = new LambdaUpdateWrapper<>();
-        updateWrapper.set(MailboxAutoReplySetEntity::getEnabled,enable);
-        updateWrapper.eq(MailboxAutoReplySetEntity::getId,id);
-        replySetService.update(updateWrapper);
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
+        mailOwnershipGuard.updateEnabledOwned(replySetService, id, enable, currentUserId);
         return Result.ok();
     }
 

+ 28 - 6
storlead-api/src/main/java/com/storlead/mail/SmtpPopSettingsApiController.java

@@ -23,6 +23,7 @@ import com.storlead.sales.mail.enums.EmailBoxEnum;
 import com.storlead.sales.mail.pojo.vo.MailAcountVO;
 import com.storlead.sales.mail.service.EmailsService;
 import com.storlead.sales.mail.service.SmtpPopSettingsService;
+import com.storlead.sales.mail.support.MailOwnershipGuard;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiResponse;
@@ -61,6 +62,9 @@ public class SmtpPopSettingsApiController {
     @Resource
     private EmailsService emailsService;
 
+    @Resource
+    private MailOwnershipGuard mailOwnershipGuard;
+
 
     @PostMapping(value = "/pageList")
     @ApiOperation(value = "获取我的邮箱配置信息" )
@@ -95,6 +99,10 @@ public class SmtpPopSettingsApiController {
         if (Objects.isNull(id) || Objects.isNull(autoReplyStatus)) {
             return Result.error("参数错误");
         }
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
+        if (mailOwnershipGuard.getOwnedSmtpPop(id, currentUserId) == null) {
+            return Result.error("无权操作");
+        }
         if (Integer.valueOf(1).equals(autoReplyStatus)) {
             if(StrUtil.isBlank(replyContent) || StrUtil.isBlank(replyTitle)) {
                 return Result.error("参数错误");
@@ -106,6 +114,7 @@ public class SmtpPopSettingsApiController {
         updateWrapper.set(SmtpPopSettingsEntity::getReplyOnTime,new Date());
         updateWrapper.set(SmtpPopSettingsEntity::getIsAutoReply,autoReplyStatus);
         updateWrapper.eq(SmtpPopSettingsEntity::getId,id);
+        updateWrapper.eq(SmtpPopSettingsEntity::getOwnerBy, currentUserId);
         smtpPopSettingsService.update(updateWrapper);
         return Result.ok();
     }
@@ -119,10 +128,15 @@ public class SmtpPopSettingsApiController {
         if (Objects.isNull(id) || Objects.isNull(autoReplyStatus)) {
             return Result.error("参数错误");
         }
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
+        if (mailOwnershipGuard.getOwnedSmtpPop(id, currentUserId) == null) {
+            return Result.error("无权操作");
+        }
         LambdaUpdateWrapper<SmtpPopSettingsEntity> updateWrapper = new LambdaUpdateWrapper();
         updateWrapper.set(SmtpPopSettingsEntity::getIsAutoReply,autoReplyStatus);
         updateWrapper.set(SmtpPopSettingsEntity::getReplyOnTime,new Date());
         updateWrapper.eq(SmtpPopSettingsEntity::getId,id);
+        updateWrapper.eq(SmtpPopSettingsEntity::getOwnerBy, currentUserId);
         smtpPopSettingsService.update(updateWrapper);
         return Result.ok();
     }
@@ -138,13 +152,14 @@ public class SmtpPopSettingsApiController {
             return Result.result(null);
         }
         SmtpPopSettingsEntity smtpPop = smtpPopSettingsService.getById(id);
-        if (Objects.isNull(smtpPop)) {
+        if (Objects.isNull(smtpPop) || mailOwnershipGuard.getOwnedSmtpPop(id, currentUserId) == null) {
             return Result.error("参数错误");
         }
 
         LambdaUpdateWrapper<SmtpPopSettingsEntity> updateWrapper = new LambdaUpdateWrapper<>();
         updateWrapper.set(SmtpPopSettingsEntity::getIsDelete,Integer.valueOf(1));
         updateWrapper.eq(SmtpPopSettingsEntity::getId,id);
+        updateWrapper.eq(SmtpPopSettingsEntity::getOwnerBy, currentUserId);
         smtpPopSettingsService.update(updateWrapper);
         return Result.ok();
     }
@@ -207,13 +222,15 @@ public class SmtpPopSettingsApiController {
 
         if (Objects.nonNull(entity.getId())) {
             // 修改
-            SmtpPopSettingsEntity old = smtpPopSettingsService.getById(entity.getId());
-            if (Objects.nonNull(old)) {
-                if(!old.getEmailAddress().equals(entity.getEmailAddress())){
-                    return Result.error("已绑定的邮箱无法修改,可新增邮箱配置!");
-                }
+            SmtpPopSettingsEntity old = mailOwnershipGuard.getOwnedSmtpPop(entity.getId(), currentUserId);
+            if (Objects.isNull(old)) {
+                return Result.error("无权操作");
+            }
+            if(!old.getEmailAddress().equals(entity.getEmailAddress())){
+                return Result.error("已绑定的邮箱无法修改,可新增邮箱配置!");
             }
         }
+        entity.setOwnerBy(currentUserId);
         Boolean b = smtpPopSettingsService.saveOrUpdate(entity);
         if (Integer.valueOf(1).equals(entity.getUseDefault()) && b) {
             // 如果设置了默认邮箱,则将所有默认邮箱改为非默认
@@ -264,9 +281,14 @@ public class SmtpPopSettingsApiController {
         if (Objects.isNull(id) || Objects.isNull(isEnabled)) {
             return Result.error("参数错误");
         }
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
+        if (mailOwnershipGuard.getOwnedSmtpPop(id, currentUserId) == null) {
+            return Result.error("无权操作");
+        }
         LambdaUpdateWrapper<SmtpPopSettingsEntity> updateWrapper = new LambdaUpdateWrapper();
         updateWrapper.set(SmtpPopSettingsEntity::getIsDeleteMail,isEnabled);
         updateWrapper.eq(SmtpPopSettingsEntity::getId,id);
+        updateWrapper.eq(SmtpPopSettingsEntity::getOwnerBy, currentUserId);
         smtpPopSettingsService.update(updateWrapper);
         return Result.ok();
     }

+ 8 - 7
storlead-api/src/main/java/com/storlead/mail/ThumbnailPreviewController.java

@@ -16,9 +16,12 @@ import javax.imageio.ImageWriteParam;
 import javax.imageio.ImageWriter;
 import javax.imageio.stream.ImageOutputStream;
 import java.awt.image.BufferedImage;
+import com.storlead.sales.mail.util.SafeFilePathResolver;
+
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.Iterator;
 
 /**
@@ -40,13 +43,8 @@ public class ThumbnailPreviewController {
     public ResponseEntity<byte[]> thumbnail(@RequestParam String url) throws Exception {
 
         try {
-            // 1. 构建文件路径
-            String fileUrl = fileProperties.getPath().getPath() + url;
-            String normalizedPath = fileUrl.replace('/', File.separatorChar)
-                    .replace('\\', File.separatorChar);
-
-            // 2. 读取原始图片
-            BufferedImage originalImage = ImageIO.read(new File(normalizedPath));
+            Path filePath = SafeFilePathResolver.resolveUnderBase(fileProperties.getPath().getPath(), url);
+            BufferedImage originalImage = ImageIO.read(filePath.toFile());
             if (originalImage == null) {
                 return ResponseEntity.notFound().build();
             }
@@ -57,6 +55,9 @@ public class ThumbnailPreviewController {
                     .contentType(MediaType.IMAGE_JPEG)
                     .body(compressedImage);
 
+        } catch (IllegalArgumentException e) {
+            log.warn("thumbnail rejected unsafe path: {}", url);
+            return ResponseEntity.badRequest().build();
         } catch (Exception e) {
             log.error("thumbnail error", e);
             return ResponseEntity.notFound().build();

+ 13 - 5
storlead-api/src/main/java/com/storlead/mail/UserEmailFolderApiController.java

@@ -14,6 +14,7 @@ import com.storlead.sales.mail.entity.UserEmailFolderEntity;
 import com.storlead.sales.mail.pojo.dto.EmailTemplatesDTO;
 import com.storlead.sales.mail.service.EmailsService;
 import com.storlead.sales.mail.service.UserEmailFolderService;
+import com.storlead.sales.mail.support.MailOwnershipGuard;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiResponse;
@@ -45,6 +46,9 @@ public class UserEmailFolderApiController {
     @Resource
     private EmailsService emailsService;
 
+    @Resource
+    private MailOwnershipGuard mailOwnershipGuard;
+
     @PostMapping(value = "/page_list")
     @ApiOperation(value = "获取用户文件夹(分页)" )
     @ApiResponses({
@@ -94,6 +98,9 @@ public class UserEmailFolderApiController {
                 return Result.error("文件夹'"+ entity.getFolderName() +"'已存在!");
             }
         } else {
+            if (mailOwnershipGuard.getOwnedEntity(emailFolderService, entity.getId(), currentUserId) == null) {
+                return Result.error("无权操作");
+            }
             UserEmailFolderEntity oldFolder = emailFolderService.getOne(queryWrapper);
             if (Objects.nonNull(oldFolder)) {
                 if (!oldFolder.getId().equals(entity.getId())) {
@@ -101,6 +108,7 @@ public class UserEmailFolderApiController {
                 }
             }
         }
+        entity.setOwnerBy(currentUserId);
         emailFolderService.saveOrUpdate(entity);
         return Result.ok();
     }
@@ -111,13 +119,15 @@ public class UserEmailFolderApiController {
         if(Objects.isNull(ids)) {
             return Result.error("参数错误");
         }
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
         /**
          * 需要修改邮件文件夹标记为''
          */
-        emailFolderService.lgDelete(Arrays.asList(ids));
+        mailOwnershipGuard.softDeleteOwned(emailFolderService, Arrays.asList(ids), currentUserId);
 
         LambdaUpdateWrapper<EmailsEntity> updateWrapper = new LambdaUpdateWrapper<>();
         updateWrapper.in(EmailsEntity::getCustomFolderId,Arrays.asList(ids));
+        updateWrapper.eq(EmailsEntity::getOwnerBy, currentUserId);
         updateWrapper.set(EmailsEntity::getCustomFolderId,null);
         emailsService.update(updateWrapper);
         return Result.ok();
@@ -127,10 +137,8 @@ public class UserEmailFolderApiController {
     @ApiOperation("禁用或启用")
     @PostMapping("enabled")
     public Result enabled(Long id,Boolean enable) {
-        LambdaUpdateWrapper<UserEmailFolderEntity> updateWrapper = new LambdaUpdateWrapper<>();
-        updateWrapper.set(UserEmailFolderEntity::getEnabled,enable);
-        updateWrapper.eq(UserEmailFolderEntity::getId,id);
-        emailFolderService.update(updateWrapper);
+        Long currentUserId = LoginUserUtil.getCurrentUserId();
+        mailOwnershipGuard.updateEnabledOwned(emailFolderService, id, enable, currentUserId);
         return Result.ok();
     }
 }

+ 19 - 10
storlead-api/src/main/resources/application-dev.yml

@@ -1,10 +1,10 @@
 server:
-  port: 10020
+  port: 10080
   tomcat:
     max-swallow-size: -1
     basedir: /app/temp
   servlet:
-    context-path: /router/rest
+    context-path: /router/rest/
   compression:
     enabled: true
     min-response-size: 1024
@@ -147,11 +147,20 @@ environment: test
 # 站内消息、邮件模板中的前端站点根地址(覆盖 application.yml 默认值)
 domainname: https://test1.storlead.com
 
-storlead:
-  es:
-    enabled: false
-    host: 39.108.252.62
-    port: 9200
-    scheme: http
-    username: elastic
-    password: storlead123456
+file:
+  mode: 1
+  filePath: /files
+  active: test
+  mac:
+    path: ~/file/
+    avatar: ~/avatar/
+  linux:
+    path: /mnt/vdb/storlead/sales/file/email/attachments/
+    avatar: /mnt/vdb/storlead/sales/file/avatar/
+    jpeg: /mnt/vdb/storlead/sales/file/email/image/
+  windows:
+    path: D:\images\file\
+    avatar: D:\images\
+  # 文件大小 /M
+  maxSize: 100
+  avatarMaxSize: 5

+ 30 - 17
storlead-api/src/main/resources/application-test.yml

@@ -1,5 +1,5 @@
 server:
-  port: 10020
+  port: 10080
   tomcat:
     max-swallow-size: -1
     basedir: /app/sp/smarttrade/temp/${spring.profiles.active}
@@ -42,9 +42,9 @@ spring:
       - classpath:/templates
   # 设置静态文件路径,js,css等  #redis 配置
   redis:
-    host: test1.storlead.com
+    host: test1.storlead.com #test1.storlead.com
     port: 59394
-    database: 14
+    database: 10
     lettuce:
       pool:
         max-wait: 100000
@@ -52,6 +52,7 @@ spring:
         max-active: 100
     timeout: 5000
     password: bnoCWkyDqYA*ecT7FoL7
+
   mvc:
     static-path-pattern: /**
   resource:
@@ -90,24 +91,14 @@ spring:
           username: root
           password: rCgRgLjH99Xvg5BN
           driver-class-name: com.mysql.jdbc.Driver
-        sales:
-          driver-class-name: com.mysql.jdbc.Driver
-          url: jdbc:mysql://mysql.test.storlead.com:39091/sp_sales_test?useSSL=false&useUnicode=true&characterEncoding=utf8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true
-          username: root
-          password: rCgRgLjH99Xvg5BN
-        management:
-          driver-class-name: com.mysql.jdbc.Driver
-          url: jdbc:mysql://mysql.test.storlead.com:39091/storlead_test?useSSL=false&autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&noDatetimeStringSync=true&serverTimezone=Asia/Shanghai
-          username: root
-          password: rCgRgLjH99Xvg5BN
 
 #mybatis plus 设置
 mybatis-plus:
   mapper-locations: classpath*:/mapper/*Mapper.xml,classpath*:/mapper/*/*Mapper.xml
   # 实体扫描,多个 package 用逗号或者分号分隔
-  type-aliases-package: com.storlead.tems.modules.*.entity
-  type-enums-package:
-    #  configuration:
+  type-aliases-package:
+  typeEnumsPackage:
+  #  configuration:
   #配置显示查询SQL
   #    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
   #    default-enum-type-handler: org.apache.ibatis.type.EnumOrdinalTypeHandler
@@ -127,7 +118,7 @@ mybatis-plus:
 logging:
   file:
     # 日志存放目录
-    path: /app/sp/okr/${spring.profiles.active}/log
+    path: /app/sp/${spring.application.name}/log
   level:
     root: info
     io:
@@ -150,4 +141,26 @@ logging:
     com:
       storlead: debug
 
+
+environment: test
+
+# 站内消息、邮件模板中的前端站点根地址(覆盖 application.yml 默认值)
 domainname: https://test1.storlead.com
+
+file:
+  mode: 1
+  filePath: /files
+  active: test
+  mac:
+    path: ~/file/
+    avatar: ~/avatar/
+  linux:
+    path: /mnt/vdb/storlead/sales/file/email/attachments/
+    avatar: /mnt/vdb/storlead/sales/file/avatar/
+    jpeg: /mnt/vdb/storlead/sales/file/email/image/
+  windows:
+    path: D:\images\file\
+    avatar: D:\images\
+  # 文件大小 /M
+  maxSize: 100
+  avatarMaxSize: 5

+ 1 - 1
storlead-mail/src/main/java/com/storlead/sales/mail/dispatch/AutoSendDelayEmailTaskJob.java

@@ -36,7 +36,7 @@ public class AutoSendDelayEmailTaskJob {
     @Resource
     private SmtpPopSettingsService smtpPopSettingsService;
 
-    @Scheduled(cron = "${storlead.mail.scheduler.delay-send-cron:0 * * * * ?}")
+    // @Scheduled(cron = "${storlead.mail.scheduler.delay-send-cron:0 * * * * ?}")
     public void sendDelayEmailTask() {
         String active = environment.getProperty("spring.profiles.active");
         if ("dev".equals(active)) {

+ 1 - 1
storlead-mail/src/main/java/com/storlead/sales/mail/dispatch/DelayDeleteServerEmailTaskJob.java

@@ -46,7 +46,7 @@ public class DelayDeleteServerEmailTaskJob {
     /**
      * 定时执行:从邮件服务器延迟删除已标记过期的邮件。
      */
-    @Scheduled(cron = "${storlead.mail.scheduler.delay-delete-cron:0 0 2 * * ?}")
+    // @Scheduled(cron = "${storlead.mail.scheduler.delay-delete-cron:0 0 2 * * ?}")
     public void delayDeleteServerEmailTask() {
         List<EmailsEntity> taskEntities = emailsService.getDelayDeleteMail();
         if (CollectionUtil.isEmpty(taskEntities)) {

+ 1 - 1
storlead-mail/src/main/java/com/storlead/sales/mail/dispatch/EmailPullHeadContentTaskJob.java

@@ -24,7 +24,7 @@ public class EmailPullHeadContentTaskJob {
     @Resource
     private EmailsService emailsService;
 
-    @Scheduled(cron = "${storlead.mail.scheduler.pull-email-cron:0 */5 * * * ?}")
+    //@Scheduled(cron = "${storlead.mail.scheduler.pull-email-cron:0 */5 * * * ?}")
     public void pullHeadMailTask() {
         LambdaQueryWrapper<SmtpPopSettingsEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
         lambdaQueryWrapper.eq(SmtpPopSettingsEntity::getIsDelete, 0);

+ 0 - 85
storlead-mail/src/main/java/com/storlead/sales/mail/integration/scheduler/MailIntegrationTaskScheduler.java

@@ -1,85 +0,0 @@
-package com.storlead.sales.mail.integration.scheduler;
-
-import cn.hutool.core.util.StrUtil;
-import com.storlead.sales.mail.integration.config.MailIntegrationProperties;
-import com.storlead.sales.mail.integration.entity.MailIntegrationTaskEntity;
-import com.storlead.sales.mail.integration.enums.MailIntegrationTaskStatus;
-import com.storlead.sales.mail.integration.enums.MailIntegrationTaskType;
-import com.storlead.sales.mail.integration.handler.CrmMailSyncHandler;
-import com.storlead.sales.mail.integration.handler.NewMailRemindHandler;
-import com.storlead.sales.mail.integration.mapper.MailIntegrationTaskMapper;
-import lombok.extern.log4j.Log4j2;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-import org.springframework.util.CollectionUtils;
-
-import javax.annotation.Resource;
-import java.util.Date;
-import java.util.List;
-
-@Log4j2
-@Component
-public class MailIntegrationTaskScheduler {
-
-    @Resource
-    private MailIntegrationProperties properties;
-    @Resource
-    private MailIntegrationTaskMapper mailIntegrationTaskMapper;
-    @Resource
-    private CrmMailSyncHandler crmMailSyncHandler;
-    @Resource
-    private NewMailRemindHandler newMailRemindHandler;
-
-    @Scheduled(fixedDelayString = "${storlead.mail.integration.poll-interval-ms:30000}")
-    public void processPendingTasks() {
-        if (!properties.isEnabled() || !properties.isInvokeExternalBeans()) {
-            return;
-        }
-        List<MailIntegrationTaskEntity> tasks =
-                mailIntegrationTaskMapper.selectPendingTasks(properties.getBatchSize());
-        if (CollectionUtils.isEmpty(tasks)) {
-            return;
-        }
-        for (MailIntegrationTaskEntity task : tasks) {
-            processOne(task);
-        }
-    }
-
-    private void processOne(MailIntegrationTaskEntity task) {
-        int claimed = mailIntegrationTaskMapper.claimTask(
-                task.getId(),
-                MailIntegrationTaskStatus.PENDING.getCode(),
-                MailIntegrationTaskStatus.PROCESSING.getCode());
-        if (claimed == 0) {
-            return;
-        }
-        try {
-            MailIntegrationTaskType type = MailIntegrationTaskType.fromCode(task.getTaskType());
-            switch (type) {
-                case CRM_SYNC:
-                    crmMailSyncHandler.execute(task.getMailIds(), task.getOwnerBy());
-                    break;
-                case NEW_REMIND:
-                    newMailRemindHandler.execute(task.getMailIds(), task.getOwnerBy());
-                    break;
-                default:
-                    throw new IllegalStateException("Unsupported task type: " + task.getTaskType());
-            }
-            markFinished(task.getId(), MailIntegrationTaskStatus.SUCCESS, null);
-        } catch (Exception e) {
-            log.error("mail integration task failed, id={}, type={}", task.getId(), task.getTaskType(), e);
-            String msg = StrUtil.sub(e.getMessage(), 0, 1900);
-            markFinished(task.getId(), MailIntegrationTaskStatus.FAILED, msg);
-        }
-    }
-
-    private void markFinished(Long id, MailIntegrationTaskStatus status, String errorMsg) {
-        MailIntegrationTaskEntity update = new MailIntegrationTaskEntity();
-        update.setId(id);
-        update.setTaskStatus(status.getCode());
-        update.setErrorMsg(errorMsg);
-        update.setProcessedAt(new Date());
-        update.setUpdateTime(new Date());
-        mailIntegrationTaskMapper.updateById(update);
-    }
-}

+ 156 - 0
storlead-mail/src/main/java/com/storlead/sales/mail/support/MailOwnershipGuard.java

@@ -0,0 +1,156 @@
+package com.storlead.sales.mail.support;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.storlead.framework.common.constant.CommonConstant;
+import com.storlead.framework.mybatis.entity.SysBaseField;
+import com.storlead.sales.mail.entity.EmailsEntity;
+import com.storlead.sales.mail.entity.MailAttachmentEntity;
+import com.storlead.sales.mail.entity.MailTempAttachmentEntity;
+import com.storlead.sales.mail.entity.SmtpPopSettingsEntity;
+import com.storlead.sales.mail.entity.UserEmailFolderEntity;
+import com.storlead.sales.mail.service.EmailsService;
+import com.storlead.sales.mail.service.MailAttachmentService;
+import com.storlead.sales.mail.service.MailTempAttachmentService;
+import com.storlead.sales.mail.service.SmtpPopSettingsService;
+import com.storlead.sales.mail.service.UserEmailFolderService;
+import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+
+import javax.annotation.Resource;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 邮件模块资源归属校验,防止水平越权(IDOR)。
+ */
+@Component
+public class MailOwnershipGuard {
+
+    @Resource
+    private EmailsService emailsService;
+    @Resource
+    private SmtpPopSettingsService smtpPopSettingsService;
+    @Resource
+    private MailAttachmentService mailAttachmentService;
+    @Resource
+    private MailTempAttachmentService tempAttachmentService;
+    @Resource
+    private UserEmailFolderService userEmailFolderService;
+
+    public boolean isOwnedBy(Long ownerBy, Long userId) {
+        return ownerBy != null && userId != null && ownerBy.equals(userId);
+    }
+
+    public EmailsEntity getOwnedEmail(Long mailId, Long userId) {
+        if (mailId == null || userId == null) {
+            return null;
+        }
+        EmailsEntity email = emailsService.getById(mailId);
+        return isOwnedBy(email != null ? email.getOwnerBy() : null, userId) ? email : null;
+    }
+
+    public List<EmailsEntity> listOwnedEmails(Collection<Long> mailIds, Long userId) {
+        if (CollectionUtils.isEmpty(mailIds) || userId == null) {
+            return Collections.emptyList();
+        }
+        return emailsService.list(new LambdaQueryWrapper<EmailsEntity>()
+                .in(EmailsEntity::getId, mailIds)
+                .eq(EmailsEntity::getOwnerBy, userId));
+    }
+
+    public boolean ownsAllEmails(Collection<Long> mailIds, Long userId) {
+        if (CollectionUtils.isEmpty(mailIds) || userId == null) {
+            return false;
+        }
+        long owned = emailsService.count(new LambdaQueryWrapper<EmailsEntity>()
+                .in(EmailsEntity::getId, mailIds)
+                .eq(EmailsEntity::getOwnerBy, userId));
+        return owned == mailIds.size();
+    }
+
+    public SmtpPopSettingsEntity getOwnedSmtpPop(Long smtpPopId, Long userId) {
+        if (smtpPopId == null || userId == null) {
+            return null;
+        }
+        SmtpPopSettingsEntity smtpPop = smtpPopSettingsService.getById(smtpPopId);
+        if (smtpPop == null || !Integer.valueOf(0).equals(smtpPop.getIsDelete())) {
+            return null;
+        }
+        return isOwnedBy(smtpPop.getOwnerBy(), userId) ? smtpPop : null;
+    }
+
+    public UserEmailFolderEntity getOwnedFolder(Long folderId, Long userId) {
+        if (folderId == null || userId == null) {
+            return null;
+        }
+        UserEmailFolderEntity folder = userEmailFolderService.getById(folderId);
+        if (folder == null || !Integer.valueOf(0).equals(folder.getIsDelete())) {
+            return null;
+        }
+        return isOwnedBy(folder.getOwnerBy(), userId) ? folder : null;
+    }
+
+    public MailAttachmentEntity getOwnedAttachment(Long attachmentId, Long userId) {
+        if (attachmentId == null || userId == null) {
+            return null;
+        }
+        MailAttachmentEntity attachment = mailAttachmentService.getById(attachmentId);
+        if (attachment == null) {
+            return null;
+        }
+        EmailsEntity email = emailsService.getById(attachment.getEmailId());
+        return email != null && isOwnedBy(email.getOwnerBy(), userId) ? attachment : null;
+    }
+
+    public MailTempAttachmentEntity getOwnedTempAttachment(Long attachmentId, Long userId) {
+        if (attachmentId == null || userId == null) {
+            return null;
+        }
+        MailTempAttachmentEntity attachment = tempAttachmentService.getById(attachmentId);
+        if (attachment == null) {
+            return null;
+        }
+        if (attachment.getEmailId() != null) {
+            EmailsEntity email = emailsService.getById(attachment.getEmailId());
+            return email != null && isOwnedBy(email.getOwnerBy(), userId) ? attachment : null;
+        }
+        return isOwnedBy(attachment.getOwnerBy(), userId) ? attachment : null;
+    }
+
+    public <T extends SysBaseField> T getOwnedEntity(IService<T> service, Long id, Long userId) {
+        if (id == null || userId == null) {
+            return null;
+        }
+        T entity = service.getById(id);
+        if (entity == null || !Objects.equals(entity.getIsDelete(), CommonConstant.DEL_FLAG_0)) {
+            return null;
+        }
+        return isOwnedBy(entity.getOwnerBy(), userId) ? entity : null;
+    }
+
+    public <T extends SysBaseField> boolean softDeleteOwned(IService<T> service, Collection<Long> ids, Long userId) {
+        if (CollectionUtils.isEmpty(ids) || userId == null) {
+            return false;
+        }
+        UpdateWrapper<T> wrapper = new UpdateWrapper<>();
+        wrapper.in("id", ids);
+        wrapper.eq("owner_by", userId);
+        wrapper.set("is_delete", CommonConstant.DEL_FLAG_1);
+        return service.update(wrapper);
+    }
+
+    public <T extends SysBaseField> boolean updateEnabledOwned(IService<T> service, Long id, Boolean enabled, Long userId) {
+        if (id == null || userId == null) {
+            return false;
+        }
+        UpdateWrapper<T> wrapper = new UpdateWrapper<>();
+        wrapper.eq("id", id);
+        wrapper.eq("owner_by", userId);
+        wrapper.set("enabled", enabled);
+        return service.update(wrapper);
+    }
+}

+ 74 - 0
storlead-mail/src/main/java/com/storlead/sales/mail/util/SafeFilePathResolver.java

@@ -0,0 +1,74 @@
+package com.storlead.sales.mail.util;
+
+import cn.hutool.core.util.StrUtil;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+/**
+ * 安全文件路径解析:规范化后必须落在指定根目录内,禁止路径穿越。
+ */
+public final class SafeFilePathResolver {
+
+    private SafeFilePathResolver() {
+    }
+
+    /**
+     * 将相对路径解析为根目录下的绝对路径。
+     */
+    public static Path resolveUnderBase(String baseDir, String relativePath) {
+        if (StrUtil.isBlank(baseDir)) {
+            throw new IllegalArgumentException("文件根目录未配置");
+        }
+        if (StrUtil.isBlank(relativePath)) {
+            throw new IllegalArgumentException("文件路径不能为空");
+        }
+        String cleaned = sanitizeRelativePath(relativePath);
+        Path base = Paths.get(baseDir).toAbsolutePath().normalize();
+        Path resolved = base.resolve(cleaned).normalize();
+        if (!resolved.startsWith(base)) {
+            throw new IllegalArgumentException("非法文件路径");
+        }
+        return resolved;
+    }
+
+    /**
+     * 解析单文件名(不允许目录分隔符与 ..)。
+     */
+    public static Path resolveFileNameUnderBase(String baseDir, String fileName) {
+        if (StrUtil.isBlank(fileName)) {
+            throw new IllegalArgumentException("文件名不能为空");
+        }
+        String name = fileName.replace('\\', '/').trim();
+        if (name.contains("/") || name.contains("..")) {
+            throw new IllegalArgumentException("非法文件名");
+        }
+        return resolveUnderBase(baseDir, name);
+    }
+
+    private static String sanitizeRelativePath(String relativePath) {
+        String cleaned = relativePath.replace('\\', '/').trim();
+        if (cleaned.contains("\0")) {
+            throw new IllegalArgumentException("非法文件路径");
+        }
+        if (isAbsolutePath(cleaned)) {
+            throw new IllegalArgumentException("非法文件路径");
+        }
+        while (cleaned.startsWith("/")) {
+            cleaned = cleaned.substring(1);
+        }
+        for (String segment : cleaned.split("/")) {
+            if ("..".equals(segment)) {
+                throw new IllegalArgumentException("非法文件路径");
+            }
+        }
+        return cleaned;
+    }
+
+    private static boolean isAbsolutePath(String path) {
+        if (path.startsWith("/")) {
+            return true;
+        }
+        return path.length() >= 2 && Character.isLetter(path.charAt(0)) && path.charAt(1) == ':';
+    }
+}

+ 12 - 3
storlead-mail/src/main/java/com/storlead/sales/mail/util/ZipUtility.java

@@ -8,10 +8,12 @@ package com.storlead.sales.mail.util;
  */
 import com.storlead.sales.mail.entity.MailAttachmentEntity;
 import com.storlead.sales.mail.properties.MailFileProperties;
+import com.storlead.sales.mail.util.SafeFilePathResolver;
 import lombok.extern.log4j.Log4j2;
 
 import javax.mail.Message;
 import java.io.*;
+import java.nio.file.Path;
 import java.util.List;
 import java.util.zip.*;
 
@@ -23,8 +25,15 @@ public class ZipUtility {
              ZipOutputStream zos = new ZipOutputStream(fos)) {
 
             for (MailAttachmentEntity attachment : attachments) {
-                String filePath = mailFileProperties.getPath().getPath() + File.separator + attachment.getFilePath();
-                File file = new File(filePath);
+                File file;
+                try {
+                    Path filePath = SafeFilePathResolver.resolveUnderBase(
+                            mailFileProperties.getPath().getPath(), attachment.getFilePath());
+                    file = filePath.toFile();
+                } catch (IllegalArgumentException e) {
+                    log.warn("zipFiles skipped unsafe path: {}", attachment.getFilePath());
+                    continue;
+                }
                 if (file.exists() && !file.isDirectory()) {
                     try (FileInputStream fis = new FileInputStream(file)) {
                         ZipEntry zipEntry = new ZipEntry(attachment.getFileName()+"."+attachment.getFileExt());
@@ -38,7 +47,7 @@ public class ZipUtility {
                         zos.closeEntry();
                     }
                 } else {
-                    System.err.println("File not found: " + filePath);
+                    System.err.println("File not found: " + file.getAbsolutePath());
                 }
             }
         }