2 Commit-ok c241f5a0db ... c2bf222b52

Szerző SHA1 Üzenet Dátum
  chenkq c2bf222b52 Merge remote-tracking branch 'origin/master' 1 hete
  chenkq 4f3602a35e 多数据库配置 1 hete
42 módosított fájl, 2092 hozzáadás és 2427 törlés
  1. 16 0
      java/storlead-api/src/main/resources/application-dev.yml
  2. 11 0
      java/storlead-api/src/main/resources/application-prod.yml
  3. 21 11
      java/storlead-api/src/main/resources/application-test.yml
  4. 5 0
      java/storlead-api/src/main/resources/application.yml
  5. 19 6
      java/storlead-mail/storlead-mail-api/pom.xml
  6. 0 19
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/ClientSentEmailsController.java
  7. 25 127
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/EmailFolderRuleApiController.java
  8. 0 94
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/EmailServiceChecker.java
  9. 17 123
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/MaiAttachmentApiController.java
  10. 18 97
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/MailBlacklistRecordApiController.java
  11. 12 100
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/MailSignatureApiController.java
  12. 21 84
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/MailTemplatesApiController.java
  13. 18 100
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/MailboxAutoReplySetController.java
  14. 0 442
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/SmtpPopMailApiController.java
  15. 38 413
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/SmtpPopSettingsApiController.java
  16. 17 100
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/UserEmailFolderApiController.java
  17. 26 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/MailRemoteClientFacade.java
  18. 56 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmAccountRemoteClient.java
  19. 46 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmEmailFolderRuleRemoteClient.java
  20. 37 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmMailBlacklistRemoteClient.java
  21. 36 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmMailFileRemoteClient.java
  22. 116 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmMailRemoteClient.java
  23. 49 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmMailSignatureRemoteClient.java
  24. 41 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmMailTemplatesRemoteClient.java
  25. 37 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmMailboxAutoReplyRemoteClient.java
  26. 20 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmThumbnailRemoteClient.java
  27. 37 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmUserEmailFolderRemoteClient.java
  28. 41 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/MktAccountRemoteClient.java
  29. 71 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/MktMailRemoteClient.java
  30. 27 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/config/MailRemoteClientCondition.java
  31. 150 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/config/MailRemoteProperties.java
  32. 148 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/config/MailWebClientAutoConfiguration.java
  33. 315 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/constant/MailRemoteApiConstants.java
  34. 29 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/dto/MktMailQueryRemoteDTO.java
  35. 63 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/dto/MktSendMailRemoteDTO.java
  36. 32 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/support/MailRemoteException.java
  37. 381 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/support/MailRemoteHttpSupport.java
  38. 53 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/support/MailRemoteResponseWriter.java
  39. 2 0
      java/storlead-mail/storlead-mail-api/src/main/resources/META-INF/spring.factories
  40. 2 2
      java/storlead-sasa/storlead-sales/pom.xml
  41. 37 707
      java/storlead-sasa/storlead-sales/src/main/java/com/storlead/sales/controller/mail/MailApiController.java
  42. 2 2
      java/storlead-sasa/storlead-trade/pom.xml

+ 16 - 0
java/storlead-api/src/main/resources/application-dev.yml

@@ -202,6 +202,22 @@ dify:
 domainname: https://test1.storlead.com
 
 storlead:
+  # 独立邮件服务(B)远程调用:按环境配置 IP/域名 + context-path,请求自动透传 token
+  mail:
+    remote:
+      enabled: true
+      scheme: http
+      host: 192.168.1.181
+      port: 10080
+      context-path: /router/email
+      # 也可直接写完整地址(优先于上面分项):
+      # base-url: http://127.0.0.1:18090/router/rest
+      connect-timeout-ms: 5000
+      read-timeout-ms: 60000
+      write-timeout-ms: 60000
+      token-header: token
+      forward-token: true
+      forward-tenant: true
   es:
     enabled: false
     host: 39.108.252.62

+ 11 - 0
java/storlead-api/src/main/resources/application-prod.yml

@@ -155,6 +155,17 @@ sp:
 domainname: https://project.storlead.com
 
 storlead:
+  mail:
+    remote:
+      enabled: true
+      scheme: https
+      # 生产请改为邮件服务域名或内网 IP
+      host: mail.storlead.com
+      # port: 443
+      context-path: /router/rest
+      token-header: token
+      forward-token: true
+      forward-tenant: true
   es:
     enabled: true
     host: 39.108.252.62

+ 21 - 11
java/storlead-api/src/main/resources/application-test.yml

@@ -21,7 +21,7 @@ spring:
     multipart:
       max-file-size: 20MB
       max-request-size: 20MB
-  ## quartz摰𡁏𧒄隞餃𦛚,��鍂�唳旿摨𤘪䲮撘
+  ## quartz摰𡁏𧒄隞餃𦛚,��鍂�唳旿摨𤘪䲮撘?
   #  quartz:
   #    job-store-type: jdbc
   #json �園𡢿�喟�銝�頧祆揢
@@ -32,7 +32,7 @@ spring:
     proxy-target-class: true
   #�滨蔭freemarker
   freemarker:
-    # 霈曄蔭璅⊥踎�𡒊��
+    # 霈曄蔭璅⊥踎�𡒊��?
     suffix: .ftl
     # 霈曄蔭��﹝蝐餃�
     content-type: text/html
@@ -44,7 +44,7 @@ spring:
     # 霈曄蔭ftl��辣頝臬�
     template-loader-path:
       - classpath:/templates
-  # 霈曄蔭�蹱���隞嗉楝敺��js,css蝑 #redis �滨蔭
+  # 霈曄蔭�蹱���隞嗉楝敺��js,css蝑? #redis �滨蔭
   redis:
     host: test1.storlead.com
     port: 59394
@@ -64,13 +64,13 @@ spring:
     exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
   datasource:
     dynamic:
-      druid: # �典�druid��㺭嚗𣬚�憭折����澆�暺䁅恕靽脲�銝��氬��(�啣歇�舀�����啣�銝�,銝齿�璆𡁜鉄銋劐�閬�僚霈曄蔭)
+      druid: # �典�druid��㺭嚗𣬚�憭折����澆�暺䁅恕靽脲�銝��氬�?�啣歇�舀�����啣�銝?銝齿�璆𡁜鉄銋劐�閬�僚霈曄蔭)
         # 餈墧𦻖瘙删��滨蔭靽⊥�
-        # �嘥��硋之撠𧶏���撠𧶏���憭
+        # �嘥��硋之撠𧶏���撠𧶏���憭?
         initial-size: 5
         min-idle: 5
         maxActive: 20
-        # �滨蔭�瑕�餈墧𦻖蝑匧�頞�𧒄��𧒄�
+        # �滨蔭�瑕�餈墧𦻖蝑匧�頞�𧒄��𧒄�?
         maxWait: 60000
         # �滨蔭�湧�憭帋��滩�銵䔶�甈⊥�瘚页�璉�瘚钅�閬���剔�蝛粹𤦭餈墧𦻖嚗��雿齿糓瘥怎�
         timeBetweenEvictionRunsMillis: 60000
@@ -80,10 +80,10 @@ spring:
         testWhileIdle: true
         testOnBorrow: false
         testOnReturn: false
-        # �枏�PSCache嚗�僎銝娍�摰𡁏�銝芾��乩�PSCache��之撠
+        # �枏�PSCache嚗�僎銝娍�摰𡁏�銝芾��乩�PSCache��之撠?
         poolPreparedStatements: true
         maxPoolPreparedStatementPerConnectionSize: 20
-        # �滨蔭�烐綉蝏蠘恣�行⏛��ilters嚗�縧�匧��烐綉�屸𢒰sql�䭾�蝏蠘恣嚗�'wall'�其��脩�憓�
+        # �滨蔭�烐綉蝏蠘恣�行⏛��ilters嚗�縧�匧��烐綉�屸𢒰sql�䭾�蝏蠘恣嚗?wall'�其��脩�憓?
         filters: stat,wall,slf4j
         # �朞�connectProperties撅墧�扳䔉�枏�mergeSql�蠘�嚗𥟇�SQL霈啣�
         connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
@@ -127,7 +127,7 @@ spring:
 #mybatis plus 霈曄蔭
 mybatis-plus:
   mapper-locations: classpath*:/mapper/*Mapper.xml,classpath*:/mapper/*/*Mapper.xml
-  # 摰硺��急�嚗��銝� package �券�堒噡�𤥁����瑕���
+  # 摰硺��急�嚗��銝?package �券�堒噡�𤥁����瑕��?
   type-aliases-package: com.storlead.tems.modules.*.entity
   type-enums-package:
     #  configuration:
@@ -138,9 +138,9 @@ mybatis-plus:
     # �喲𡡒MP3.0�芸蒂��anner
     banner: false
     db-config:
-      #銝駁睸蝐餃�  0:"�唳旿摨𨧻D�芸�",1:"霂亦掩�衤蛹�芾挽蝵桐蜓�桃掩��", 2:"�冽�颲枏�ID",3:"�典��臭�ID (�啣�蝐餃��臭�ID)", 4:"�典��臭�ID UUID",5:"摮㛖泵銝脣�撅��臭�ID (idWorker ���蝚虫葡銵函內)";
+      #銝駁睸蝐餃�  0:"�唳旿摨𨧻D�芸�",1:"霂亦掩�衤蛹�芾挽蝵桐蜓�桃掩�?, 2:"�冽�颲枏�ID",3:"�典��臭�ID (�啣�蝐餃��臭�ID)", 4:"�典��臭�ID UUID",5:"摮㛖泵銝脣�撅��臭�ID (idWorker ���蝚虫葡銵函內)";
       id-type: 4
-      # 暺䁅恕�唳旿摨栞”銝见�蝥踹𦶢�
+      # 暺䁅恕�唳旿摨栞”銝见�蝥踹𦶢�?
       table-underline: true
     #configuration:
     # 餈嗘葵�滨蔭隡𡁜��扯���ql�枏㫲�箸䔉嚗�銁撘��烐�瘚贝���𧒄�坔虾隞亦鍂
@@ -187,6 +187,16 @@ domainname: https://test1.storlead.com
 
 
 storlead:
+  mail:
+    remote:
+      enabled: true
+      scheme: http
+      host: 127.0.0.1
+      port: 18090
+      context-path: /router/rest
+      token-header: token
+      forward-token: true
+      forward-tenant: true
   es:
     enabled: false
     host: 39.108.252.62

+ 5 - 0
java/storlead-api/src/main/resources/application.yml

@@ -125,3 +125,8 @@ knife4j:
     enable-document-manage: true
     enable-search: true
     enable-footer: false
+
+# 独立邮件服务远程调用(各环境在 application-*.yml 配置 host / context-path)
+# storlead.mail.remote.host: mail.example.com
+# storlead.mail.remote.context-path: /router/rest
+# storlead.mail.remote.token-header: token

+ 19 - 6
java/storlead-mail/storlead-mail-api/pom.xml

@@ -14,7 +14,7 @@
     <artifactId>storlead-mail-api</artifactId>
     <packaging>jar</packaging>
     <name>storlead-mail-api</name>
-    <description>邮件域 Web:Controller 等。</description>
+    <description>邮件域远程调用客户端(SaaS → HTTP → 独立邮件服务)。</description>
 
     <dependencies>
         <dependency>
@@ -23,19 +23,32 @@
         </dependency>
         <dependency>
             <groupId>com.storlead.boot</groupId>
-            <artifactId>storlead-mail-spi</artifactId>
+            <artifactId>storlead-common</artifactId>
         </dependency>
         <dependency>
             <groupId>com.storlead.boot</groupId>
-            <artifactId>storlead-mail-biz</artifactId>
+            <artifactId>storlead-auth</artifactId>
         </dependency>
         <dependency>
             <groupId>com.storlead.boot</groupId>
-            <artifactId>storlead-web</artifactId>
+            <artifactId>storlead-core</artifactId>
         </dependency>
         <dependency>
-            <groupId>com.storlead.boot</groupId>
-            <artifactId>storlead-common</artifactId>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webflux</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>io.swagger</groupId>
+            <artifactId>swagger-annotations</artifactId>
         </dependency>
     </dependencies>
 </project>

+ 0 - 19
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/ClientSentEmailsController.java

@@ -1,19 +0,0 @@
-package com.storlead.mail.controller;
-
-
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-/**
- * <p>
- * 邮件表 前端控制器
- * </p>
- *
- * @author chenkq
- * @since 2024-12-04
- */
-@RestController
-@RequestMapping("/client-sent-emails-entity")
-public class ClientSentEmailsController {
-
-}

+ 25 - 127
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/EmailFolderRuleApiController.java

@@ -1,43 +1,20 @@
 package com.storlead.mail.controller;
 
-
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
-import com.baomidou.mybatisplus.core.metadata.IPage;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.storlead.framework.common.constant.CommonConstant;
-import com.storlead.framework.util.LoginUserUtil;
 import com.storlead.framework.common.result.Result;
-import com.storlead.mail.pojo.entity.EmailFolderRuleEntity;
-import com.storlead.mail.pojo.entity.EmailsEntity;
-import com.storlead.mail.pojo.entity.SmtpPopSettingsEntity;
-import com.storlead.mail.enums.EmailFolderlRuleEnum;
 import com.storlead.mail.pojo.dto.EmailTemplatesDTO;
 import com.storlead.mail.pojo.dto.FolderRuleDTO;
-import com.storlead.mail.service.EmailFolderRuleService;
-import com.storlead.mail.service.EmailsService;
+import com.storlead.mail.pojo.entity.EmailFolderRuleEntity;
+import com.storlead.mail.webclient.client.CrmEmailFolderRuleRemoteClient;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
 import lombok.extern.log4j.Log4j2;
-import org.springframework.util.CollectionUtils;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
-import java.util.*;
 
-/**
- * <p>
- * 用户邮件文件夹出入规则 前端控制器
- * </p>
- *
- * @author chenkq
- * @since 2024-11-12
- */
 @RestController
 @Log4j2
 @RequestMapping("/email/folder/rule")
@@ -45,126 +22,47 @@ import java.util.*;
 public class EmailFolderRuleApiController {
 
     @Resource
-    private EmailFolderRuleService folderRuleService;
-
-    @Resource
-    private EmailsService emailsService;
+    private CrmEmailFolderRuleRemoteClient crmEmailFolderRuleRemoteClient;
 
-    @PostMapping(value = "/get_folder_rule")
-    @ApiOperation(value = "获取规则" )
+    @PostMapping("/get_folder_rule")
+    @ApiOperation("获取规则")
     public Result<?> getFolderRule() {
-        List<EmailFolderlRuleEnum>  ls = Arrays.asList(EmailFolderlRuleEnum.values());
-        List<Map>  enumls = new ArrayList<>();
-        for (EmailFolderlRuleEnum e : ls) {
-            Map<String,String> stringMap = new HashMap<>();
-            stringMap.put("code",e.getCode());
-            stringMap.put("desc",e.getDesc());
-            enumls.add(stringMap);
-        }
-        return Result.result(enumls);
+        return crmEmailFolderRuleRemoteClient.getFolderRule();
     }
 
-
-    @PostMapping(value = "/page_list")
-    @ApiOperation(value = "获取文件夹入站规则配置(分页)" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-    })
+    @PostMapping("/page_list")
+    @ApiOperation("获取文件夹入站规则配置(分页)")
     public Result<?> pageList(EmailTemplatesDTO dto) {
-
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        Page<EmailFolderRuleEntity> page = new Page<>(dto.getPageIndex(),dto.getPageSize());
-        LambdaQueryWrapper<EmailFolderRuleEntity> queryWrapper = new LambdaQueryWrapper();
-        queryWrapper.eq(EmailFolderRuleEntity::getOwnerBy,currentUserId);
-        queryWrapper.eq(EmailFolderRuleEntity::getIsDelete,Integer.valueOf(0));
-        IPage<EmailFolderRuleEntity> pageList = folderRuleService.page(page,queryWrapper);
-        return Result.result(pageList);
+        return crmEmailFolderRuleRemoteClient.pageList(dto);
     }
 
-    @PostMapping(value = "/list")
-    @ApiOperation(value = "获取文件夹入站规则" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-    })
+    @PostMapping("/list")
+    @ApiOperation("获取文件夹入站规则")
     public Result<?> list() {
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        List<EmailFolderRuleEntity> list = folderRuleService.getUserFolderRules(currentUserId);
-        return Result.result(list);
+        return crmEmailFolderRuleRemoteClient.list();
     }
 
-    @PostMapping(value = "/save")
-    @ApiOperation(value = "保存用户规则配置" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-    })
+    @PostMapping("/save")
+    @ApiOperation("保存用户规则配置")
     public Result<?> save(EmailFolderRuleEntity entity) {
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        LambdaQueryWrapper<EmailFolderRuleEntity> w = new LambdaQueryWrapper<>();
-        w.eq(EmailFolderRuleEntity::getFolderRuleCode,entity.getFolderRuleCode());
-        w.eq(EmailFolderRuleEntity::getCompareContent,entity.getCompareContent());
-        w.eq(EmailFolderRuleEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
-        w.eq(EmailFolderRuleEntity::getOwnerBy,currentUserId);
-        if (Objects.isNull(entity.getId())) {
-            Integer exit = folderRuleService.count(w);
-            if (exit > 0) {
-                return Result.error("已存在同样的规则,不能新增同样的入站规则!");
-            }
-        } else {
-
-            EmailFolderRuleEntity oldFolder = folderRuleService.getOne(w);
-            if (Objects.nonNull(oldFolder)) {
-                if (!oldFolder.getId().equals(entity.getId())) {
-                    return Result.error("已存在同样的入站规则,无法修改!");
-                }
-            }
-        }
-
-        folderRuleService.saveOrUpdate(entity);
-        return Result.ok();
+        return crmEmailFolderRuleRemoteClient.save(entity);
     }
 
-    @PostMapping(value = "/saveBatch")
-    @ApiOperation(value = "保存用户规则配置" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-    })
-    public Result<?> save(@RequestBody FolderRuleDTO dto) {
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        if (CollectionUtils.isEmpty(dto.getFolderRules())) {
-            return Result.error("参数错误");
-        }
-        LambdaQueryWrapper<EmailFolderRuleEntity> deleteW = new LambdaQueryWrapper<>();
-        deleteW.eq(EmailFolderRuleEntity::getOwnerBy,currentUserId);
-        folderRuleService.remove(deleteW);
-        folderRuleService.saveOrUpdateBatch(dto.getFolderRules());
-        return Result.ok();
+    @PostMapping("/saveBatch")
+    @ApiOperation("批量保存用户规则配置")
+    public Result<?> saveBatch(@RequestBody FolderRuleDTO dto) {
+        return crmEmailFolderRuleRemoteClient.saveBatch(dto);
     }
 
-    @ApiOperation(value = "删除")
     @PostMapping("deleteIds")
-    public Result delete(Long [] ids) {
-        if(Objects.isNull(ids)) {
-            return Result.error("参数错误");
-        }
-        folderRuleService.lgDelete(Arrays.asList(ids));
-
-        LambdaUpdateWrapper<EmailsEntity> updateWrapper = new LambdaUpdateWrapper<>();
-        updateWrapper.in(EmailsEntity::getCustomFolderId,Arrays.asList(ids));
-        updateWrapper.set(EmailsEntity::getCustomFolderId,null);
-        emailsService.update(updateWrapper);
-        return Result.ok();
+    @ApiOperation("删除")
+    public Result<?> delete(Long[] ids) {
+        return crmEmailFolderRuleRemoteClient.deleteIds(ids);
     }
 
-
-
-
-    @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);
-        return Result.ok();
+    @ApiOperation("禁用或启用")
+    public Result<?> enabled(Long id, Boolean enable) {
+        return crmEmailFolderRuleRemoteClient.enabled(id, enable);
     }
 }

+ 0 - 94
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/EmailServiceChecker.java

@@ -1,94 +0,0 @@
-package com.storlead.mail.controller;
-
-/**
- * @program: sp-sales-platform
- * @description:
- * @author: chenkq
- * @create: 2024-08-08 17:46
- */
-
-import javax.naming.Context;
-import javax.naming.NamingException;
-import javax.naming.directory.Attribute;
-import javax.naming.directory.Attributes;
-import javax.naming.directory.DirContext;
-import javax.naming.directory.InitialDirContext;
-import java.util.Hashtable;
-
-public class EmailServiceChecker {
-    public static void main(String[] args) {
-//        String email = "chenkaiqiang@lcjs43.wecom.work";
-        String email = "fan@storlead.com";
-        String emailService = getEmailService(email);
-        System.out.println("邮箱服务提供商: " + emailService);
-    }
-
-    public static String getEmailService(String email) {
-        if (email == null || !email.contains("@")) {
-            return "";
-        }
-
-        String domain = email.substring(email.indexOf("@") + 1);
-
-        // 使用域名匹配常见邮箱服务提供商
-        switch (domain) {
-            case "gmail.com":
-                return "Google Gmail";
-            case "yahoo.com":
-            case "yahoo.co.jp":
-                return "yahoo.com";
-            case "outlook.com":
-                return "outlook.com";
-            case "hotmail.com":
-                return "hotmail.com";
-            case "qq.com":
-                return "qq.com";
-            case "exmail.qq.com":
-                return "exmail.qq.com";
-            case "163.com":
-                return "163.com";
-            case "storlead.com":
-                return "exmail.qq.com";
-            case "renice-tech.com":
-                return "exmail.qq.com";
-            // 添加其他常见的邮箱服务提供商...
-            default:
-                return checkMXRecords(domain);
-        }
-    }
-
-    public static String checkMXRecords(String domain) {
-        try {
-            Hashtable<String, String> env = new Hashtable<>();
-            env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
-            DirContext ctx = new InitialDirContext(env);
-            Attributes attrs = ctx.getAttributes(domain, new String[]{"MX"});
-            Attribute attr = attrs.get("MX");
-
-            if (attr == null) {
-                return "";
-            }
-
-            for (int i = 0; i < attr.size(); i++) {
-                String mxRecord = (String) attr.get(i);
-                if (mxRecord.contains("google.com")) {
-                    return "gmail.com";
-                } else if (mxRecord.contains("outlook.com")) {
-                    return "outlook.com";
-                } else if (mxRecord.contains("hotmail.com")) {
-                    return "hotmail.com";
-                }  else if (mxRecord.contains("qq.com") && (mxRecord.contains("mx1") || mxRecord.contains("mx2"))) {
-                    return "qq.com";
-                } else if (mxRecord.contains("qq.com") && mxRecord.contains("mxbiz")) {
-                    return "exmail.qq.com";
-                } else if (mxRecord.contains("yahoo.com")) {
-                    return "yahoo.com";
-                }
-            }
-        } catch (NamingException e) {
-            e.printStackTrace();
-            return "";
-        }
-        return "";
-    }
-}

+ 17 - 123
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/MaiAttachmentApiController.java

@@ -1,41 +1,20 @@
 package com.storlead.mail.controller;
 
-import com.alibaba.fastjson.JSONObject;
-import com.storlead.framework.common.util.DateUtils;
-import com.storlead.framework.util.LoginUserUtil;
 import com.storlead.framework.common.result.Result;
-import com.storlead.mail.pojo.entity.MailTempAttachmentEntity;
-import com.storlead.mail.pojo.entity.SmtpPopSettingsEntity;
-import com.storlead.mail.properties.MailFileProperties;
-import com.storlead.mail.service.MailTempAttachmentService;
-import com.storlead.mail.service.SmtpPopSettingsService;
-import com.storlead.mail.util.FileHelper;
+import com.storlead.mail.webclient.client.CrmMailFileRemoteClient;
+import com.storlead.mail.webclient.support.MailRemoteResponseWriter;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.extern.log4j.Log4j2;
-import org.apache.commons.io.FilenameUtils;
-import org.springframework.core.io.FileSystemResource;
-import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
 import javax.annotation.Resource;
-import javax.servlet.ServletOutputStream;
 import javax.servlet.http.HttpServletResponse;
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.InputStream;
-import java.net.URLEncoder;
-import java.util.*;
+import java.util.List;
+import java.util.Map;
 
-/**
- * @program: sp-sales-platform
- * @description:
- * @author: chenkq
- * @create: 2024-08-10 09:14
- */
 @RestController
 @RequestMapping("/mail/file")
 @Api(tags = "邮件: 邮件管理")
@@ -43,113 +22,28 @@ import java.util.*;
 public class MaiAttachmentApiController {
 
     @Resource
-    private MailFileProperties mailFileProperties;
-    @Resource
-    private SmtpPopSettingsService smtpPopSettingsService;
-    @Resource
-    private MailTempAttachmentService tempAttachmentService;
+    private CrmMailFileRemoteClient crmMailFileRemoteClient;
 
-    @PostMapping(value = "/cache_mail_file")
-    @ApiOperation(value = "缓存需要上传的附件" )
+    @PostMapping("/cache_mail_file")
+    @ApiOperation("缓存需要上传的附件")
     public Result<?> cacheMailFile(@RequestParam("files") List<MultipartFile> files) {
-        Result result = new Result();
-
-        try {
-            List<JSONObject> res = new ArrayList<>();
-
-            for (MultipartFile file : files) {
-                Long currentUserId = LoginUserUtil.getCurrentUserId();
-                SmtpPopSettingsEntity smtpPop = smtpPopSettingsService.getDefaultSmtpPop(currentUserId);
-                String recipient = DateUtils.date2Str(new Date(), DateUtils.yyyyMMdd);
-                String downloadDir = mailFileProperties.getPath().getPath() + File.separator + smtpPop.getEmailAddress() + File.separator + recipient + File.separator;
-                JSONObject jsonObject = new JSONObject();
-                Long fileId = tempAttachmentService.saveTempAttachment(file, downloadDir, null);
-                if (Objects.isNull(fileId)) {
-                    return Result.error("上传错误");
-                }
-                jsonObject.put("id", fileId);
-                jsonObject.put("fileShowName", file.getOriginalFilename());
-                jsonObject.put("fileName", FilenameUtils.getBaseName(file.getOriginalFilename()));
-                jsonObject.put("fileSize", file.getSize());
-                jsonObject.put("fileSizeName", FileHelper.formatFileSize(file.getSize()));
-                jsonObject.put("fileExt", FilenameUtils.getExtension(file.getOriginalFilename()));
-                res.add(jsonObject);
-            }
-            return Result.result(res);
-
-        } catch (Exception e) {
-            log.error("--upload -- error =", e);
-            return Result.error("上传错误");
-        }
+        return crmMailFileRemoteClient.cacheMailFile(files);
     }
 
-    @PostMapping(value = "/cache_image_file")
-    @ApiOperation(value = "缓存图片文件" )
-    public Map cacheImageFile(@RequestParam("upload") MultipartFile file) {
-        Map jsonObject = new HashMap();
-        try {
-            Long currentUserId = LoginUserUtil.getCurrentUserId();
-            String downloadDir = mailFileProperties.getPath().getJpeg();
-            MailTempAttachmentEntity tempAttachment = tempAttachmentService.saveTempImageAttachment(file, downloadDir);
-            if (Objects.isNull(tempAttachment) || Objects.isNull(tempAttachment.getId())) {
-                return jsonObject;
-            }
-            String fileName =  tempAttachment.getFileName()+"."+tempAttachment.getFileExt();
-            jsonObject.put("id", tempAttachment.getId());
-            jsonObject.put("fileName", fileName);
-            jsonObject.put("uploaded", 1);
-            jsonObject.put("url", "/sales/mail/file/images/"+fileName);
-            return jsonObject;
-        } catch (Exception e) {
-            log.error("--upload -- error =", e);
-            return jsonObject;
-        }
+    @PostMapping("/cache_image_file")
+    @ApiOperation("缓存图片文件")
+    public Map<String, Object> cacheImageFile(@RequestParam("upload") MultipartFile file) {
+        return crmMailFileRemoteClient.cacheImageFile(file);
     }
 
-    @PostMapping(value = "/download_mail_file")
-    @ApiOperation(value = "下载临时附件" )
+    @PostMapping("/download_mail_file")
+    @ApiOperation("下载临时附件")
     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));
-            response.reset();
-            response.setContentType("application/octet-stream;charset=utf-8");
-            /*
-             * 跨域配置
-             * */
-            response.addHeader("Access-Control-Allow-Origin", "*");
-            response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
-            response.addHeader("Access-Control-Allow-Headers", "Content-Type");
-
-            String filename = resource.getFileName()+"."+resource.getFileExt();
-            filename = filename.replace(" ","");
-            response.addHeader("content-disposition",
-                    "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")
-                            + ";filename*=utf-8''" + URLEncoder.encode(filename, "UTF-8"));
-
-            ServletOutputStream outputStream = response.getOutputStream();
-            byte[] b = new byte[1024];
-            int len;
-//从输入流中读取一定数量的字节,并将其存储在缓冲区字节数组中,读到末尾返回-1
-            while ((len = inputStream.read(b)) > 0) {
-                outputStream.write(b, 0, len);
-            }
-            inputStream.close();
-            outputStream.close();
-
-        } catch (Exception e) {
-            log.error("--upload -- error =", e);
-        }
+        MailRemoteResponseWriter.write(response, crmMailFileRemoteClient.downloadMailFile(fileId));
     }
 
     @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);
+    public ResponseEntity<byte[]> getImage(@PathVariable String imageName) {
+        return MailRemoteResponseWriter.image(crmMailFileRemoteClient.getImage(imageName));
     }
 }

+ 18 - 97
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/MailBlacklistRecordApiController.java

@@ -1,39 +1,18 @@
 package com.storlead.mail.controller;
 
-import cn.hutool.core.util.StrUtil;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
-import com.baomidou.mybatisplus.core.metadata.IPage;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.storlead.framework.common.constant.CommonConstant;
-import com.storlead.framework.util.LoginUserUtil;
 import com.storlead.framework.common.result.Result;
-import com.storlead.mail.pojo.entity.EmailBlacklistRecordEntity;
-import com.storlead.mail.pojo.entity.EmailsEntity;
-import com.storlead.mail.pojo.entity.SmtpPopSettingsEntity;
-import com.storlead.mail.enums.EmailBoxEnum;
 import com.storlead.mail.pojo.dto.EmailTemplatesDTO;
-import com.storlead.mail.service.EmailBlacklistRecordService;
-import com.storlead.mail.service.EmailsService;
+import com.storlead.mail.pojo.entity.EmailBlacklistRecordEntity;
+import com.storlead.mail.webclient.client.CrmMailBlacklistRemoteClient;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
 import lombok.extern.log4j.Log4j2;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
-import java.util.Arrays;
-import java.util.Objects;
 
-/**
- * @program: sp-sales-platform
- * @description:
- * @author: chenkq
- * @create: 2024-10-31 10:51
- */
 @RestController
 @RequestMapping("/email/blacklist")
 @Api(tags = "邮件: 黑名单")
@@ -41,93 +20,35 @@ import java.util.Objects;
 public class MailBlacklistRecordApiController {
 
     @Resource
-    private EmailBlacklistRecordService blacklistRecordService;
-
-    @Resource
-    private EmailsService emailsService;
+    private CrmMailBlacklistRemoteClient crmMailBlacklistRemoteClient;
 
-    @PostMapping(value = "/page_list")
-    @ApiOperation(value = "获取邮件模板" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-    })
+    @PostMapping("/page_list")
+    @ApiOperation("黑名单分页")
     public Result<?> pageList(EmailTemplatesDTO dto) {
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        Page<EmailBlacklistRecordEntity> page = new Page<>(dto.getPageIndex(),dto.getPageSize());
-        LambdaQueryWrapper<EmailBlacklistRecordEntity> queryWrapper = new LambdaQueryWrapper();
-        queryWrapper.eq(EmailBlacklistRecordEntity::getOwnerBy,currentUserId);
-        queryWrapper.eq(EmailBlacklistRecordEntity::getIsDelete,Integer.valueOf(0));
-        IPage<EmailBlacklistRecordEntity> pageList = blacklistRecordService.page(page,queryWrapper);
-        return Result.result(pageList);
+        return crmMailBlacklistRemoteClient.pageList(dto);
     }
 
-    @PostMapping(value = "/list")
-    @ApiOperation(value = "获取邮件模板" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-    })
+    @PostMapping("/list")
+    @ApiOperation("黑名单列表")
     public Result<?> list(EmailTemplatesDTO dto) {
-
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        Page<EmailBlacklistRecordEntity> page = new Page<>(dto.getPageIndex(),dto.getPageSize());
-        LambdaQueryWrapper<EmailBlacklistRecordEntity> queryWrapper = new LambdaQueryWrapper();
-        queryWrapper.eq(EmailBlacklistRecordEntity::getOwnerBy,currentUserId);
-        queryWrapper.eq(EmailBlacklistRecordEntity::getIsDelete,Integer.valueOf(0));
-        queryWrapper.eq(EmailBlacklistRecordEntity::getEnabled,dto.getEnabled());
-        IPage<EmailBlacklistRecordEntity> pageList = blacklistRecordService.page(page,queryWrapper);
-        return Result.result(pageList);
+        return crmMailBlacklistRemoteClient.list(dto);
     }
 
-    @PostMapping(value = "/save")
-    @ApiOperation(value = "保存模板" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-    })
+    @PostMapping("/save")
+    @ApiOperation("保存黑名单")
     public Result<?> save(EmailBlacklistRecordEntity entity) {
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        if (StrUtil.isBlank(entity.getEmailAddress())) {
-            return Result.error("邮箱不能为空");
-        }
-        LambdaQueryWrapper<EmailBlacklistRecordEntity> wrapper =new LambdaQueryWrapper<>();
-        wrapper.eq(EmailBlacklistRecordEntity::getEmailAddress,entity.getEmailAddress());
-        wrapper.eq(EmailBlacklistRecordEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
-        wrapper.eq(EmailBlacklistRecordEntity::getOwnerBy,currentUserId);
-        wrapper.last(" limit 1");
-        EmailBlacklistRecordEntity record = blacklistRecordService.getOne(wrapper);
-        if (record != null){
-            return Result.error("该邮箱已在黑名单中已存在");
-        }
-        Boolean b = blacklistRecordService.saveOrUpdate(entity);
-        if (b) {
-            /** 将黑名单邮箱的邮件移到垃圾箱中 **/
-            LambdaUpdateWrapper<EmailsEntity> updateWrapper = new LambdaUpdateWrapper<>();
-            updateWrapper.eq(EmailsEntity::getOwnerBy,currentUserId);
-            updateWrapper.eq(EmailsEntity::getFolder, EmailBoxEnum.INBOX);
-            updateWrapper.eq(EmailsEntity::getFrom,entity.getEmailAddress());
-            updateWrapper.set(EmailsEntity::getIsTrash,1);
-            emailsService.update(updateWrapper);
-        }
-        return Result.ok();
+        return crmMailBlacklistRemoteClient.save(entity);
     }
 
-    @ApiOperation(value = "删除")
     @PostMapping("deleteIds")
-    public Result delete(Long [] ids) {
-        if(Objects.isNull(ids)) {
-            return Result.error("参数错误");
-        }
-        blacklistRecordService.lgDelete(Arrays.asList(ids));
-        return Result.ok();
+    @ApiOperation("删除")
+    public Result<?> delete(Long[] ids) {
+        return crmMailBlacklistRemoteClient.deleteIds(ids);
     }
 
-
-    @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);
-        return Result.ok();
+    @ApiOperation("禁用或启用")
+    public Result<?> enabled(Long id, Boolean enable) {
+        return crmMailBlacklistRemoteClient.enabled(id, enable);
     }
 }

+ 12 - 100
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/MailSignatureApiController.java

@@ -1,15 +1,9 @@
 package com.storlead.mail.controller;
 
-import cn.hutool.core.util.StrUtil;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
-import com.baomidou.mybatisplus.core.metadata.IPage;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.storlead.framework.common.result.Result;
-import com.storlead.framework.util.LoginUserUtil;
 import com.storlead.mail.pojo.dto.EmailSignatureDTO;
 import com.storlead.mail.pojo.entity.EmailSignatureEntity;
-import com.storlead.mail.service.EmailSignatureService;
+import com.storlead.mail.webclient.client.CrmMailSignatureRemoteClient;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.extern.log4j.Log4j2;
@@ -19,12 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
-import java.util.Arrays;
-import java.util.Objects;
 
-/**
- * 邮件签名增删改查
- */
 @RestController
 @RequestMapping("/email/signature")
 @Api(tags = "邮件: 邮件签名")
@@ -32,130 +21,53 @@ import java.util.Objects;
 public class MailSignatureApiController {
 
     @Resource
-    private EmailSignatureService emailSignatureService;
+    private CrmMailSignatureRemoteClient crmMailSignatureRemoteClient;
 
     @PostMapping("/pageLlist")
     @ApiOperation("分页查询邮件签名")
     public Result<?> pageList(EmailSignatureDTO dto) {
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        Page<EmailSignatureEntity> page = new Page<>(dto.getPageIndex(), dto.getPageSize());
-        LambdaQueryWrapper<EmailSignatureEntity> queryWrapper = buildQuery(dto, currentUserId);
-        IPage<EmailSignatureEntity> pageList = emailSignatureService.page(page, queryWrapper);
-        return Result.result(pageList);
+        return crmMailSignatureRemoteClient.pageList(dto);
     }
 
     @PostMapping("/list")
-    @ApiOperation("列表查询(精简字段)")
+    @ApiOperation("列表查询")
     public Result<?> list(EmailSignatureDTO dto) {
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        Page<EmailSignatureEntity> page = new Page<>(dto.getPageIndex(), dto.getPageSize());
-        LambdaQueryWrapper<EmailSignatureEntity> queryWrapper = buildQuery(dto, currentUserId);
-        queryWrapper.select(
-                EmailSignatureEntity::getId,
-                EmailSignatureEntity::getSignatureTitle,
-                EmailSignatureEntity::getIsDefault,
-                EmailSignatureEntity::getSmtpPopId,
-                EmailSignatureEntity::getEnabled,
-                EmailSignatureEntity::getSort,
-                EmailSignatureEntity::getCreateTime,
-                EmailSignatureEntity::getUpdateTime
-        );
-        IPage<EmailSignatureEntity> pageList = emailSignatureService.page(page, queryWrapper);
-        return Result.result(pageList);
+        return crmMailSignatureRemoteClient.list(dto);
     }
 
     @PostMapping("/getById")
     @ApiOperation("签名详情")
     public Result<?> getById(Long id) {
-        EmailSignatureEntity entity = emailSignatureService.getById(id);
-        return Result.result(entity);
+        return crmMailSignatureRemoteClient.getById(id);
     }
 
     @PostMapping("/getDefault")
-    @ApiOperation("获取当前用户默认签名(可选 smtpPopId)")
+    @ApiOperation("获取当前用户默认签名")
     public Result<?> getDefault(Long smtpPopId) {
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        LambdaQueryWrapper<EmailSignatureEntity> queryWrapper = new LambdaQueryWrapper<>();
-        queryWrapper.eq(EmailSignatureEntity::getOwnerBy, currentUserId);
-        queryWrapper.eq(EmailSignatureEntity::getIsDelete, 0);
-        queryWrapper.eq(EmailSignatureEntity::getEnabled, true);
-        queryWrapper.eq(EmailSignatureEntity::getIsDefault, 1);
-        if (Objects.nonNull(smtpPopId)) {
-            queryWrapper.eq(EmailSignatureEntity::getSmtpPopId, smtpPopId);
-        } else {
-            queryWrapper.isNull(EmailSignatureEntity::getSmtpPopId);
-        }
-        queryWrapper.last("limit 1");
-        EmailSignatureEntity entity = emailSignatureService.getOne(queryWrapper);
-        return Result.result(entity);
+        return crmMailSignatureRemoteClient.getDefault(smtpPopId);
     }
 
     @PostMapping("/save")
     @ApiOperation("新增或更新签名")
     public Result<?> save(@RequestBody EmailSignatureEntity entity) {
-        if (StrUtil.isBlank(entity.getSignatureTitle())) {
-            return Result.error("签名名称不能为空");
-        }
-        emailSignatureService.saveSignature(entity);
-        return Result.ok();
+        return crmMailSignatureRemoteClient.save(entity);
     }
 
     @PostMapping("/deleteIds")
     @ApiOperation("逻辑删除")
     public Result<?> delete(Long[] ids) {
-        if (Objects.isNull(ids) || ids.length == 0) {
-            return Result.error("参数错误");
-        }
-        emailSignatureService.lgDelete(Arrays.asList(ids));
-        return Result.ok();
+        return crmMailSignatureRemoteClient.deleteIds(ids);
     }
 
     @PostMapping("/enabled")
     @ApiOperation("启用或禁用")
     public Result<?> enabled(Long id, Boolean enable) {
-        if (Objects.isNull(id)) {
-            return Result.error("参数错误");
-        }
-        LambdaUpdateWrapper<EmailSignatureEntity> updateWrapper = new LambdaUpdateWrapper<>();
-        updateWrapper.set(EmailSignatureEntity::getEnabled, enable);
-        updateWrapper.eq(EmailSignatureEntity::getId, id);
-        emailSignatureService.update(updateWrapper);
-        return Result.ok();
+        return crmMailSignatureRemoteClient.enabled(id, enable);
     }
 
     @PostMapping("/setDefault")
     @ApiOperation("设为默认签名")
     public Result<?> setDefault(Long id) {
-        if (Objects.isNull(id)) {
-            return Result.error("参数错误");
-        }
-        EmailSignatureEntity entity = emailSignatureService.getById(id);
-        if (entity == null || Objects.equals(entity.getIsDelete(), 1)) {
-            return Result.error("签名不存在");
-        }
-        entity.setIsDefault(1);
-        emailSignatureService.saveSignature(entity);
-        return Result.ok();
-    }
-
-    private LambdaQueryWrapper<EmailSignatureEntity> buildQuery(EmailSignatureDTO dto, Long currentUserId) {
-        LambdaQueryWrapper<EmailSignatureEntity> queryWrapper = new LambdaQueryWrapper<>();
-        queryWrapper.eq(EmailSignatureEntity::getOwnerBy, currentUserId);
-        queryWrapper.eq(EmailSignatureEntity::getIsDelete, 0);
-        if (StrUtil.isNotBlank(dto.getSignatureTitle())) {
-            queryWrapper.like(EmailSignatureEntity::getSignatureTitle, dto.getSignatureTitle());
-        }
-        if (Objects.nonNull(dto.getIsDefault())) {
-            queryWrapper.eq(EmailSignatureEntity::getIsDefault, dto.getIsDefault());
-        }
-        if (Objects.nonNull(dto.getSmtpPopId())) {
-            queryWrapper.eq(EmailSignatureEntity::getSmtpPopId, dto.getSmtpPopId());
-        }
-        if (Objects.nonNull(dto.getEnabled())) {
-            queryWrapper.eq(EmailSignatureEntity::getEnabled, dto.getEnabled());
-        }
-        queryWrapper.orderByAsc(EmailSignatureEntity::getSort);
-        queryWrapper.orderByDesc(EmailSignatureEntity::getId);
-        return queryWrapper;
+        return crmMailSignatureRemoteClient.setDefault(id);
     }
 }

+ 21 - 84
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/MailTemplatesApiController.java

@@ -1,34 +1,18 @@
 package com.storlead.mail.controller;
 
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
-import com.baomidou.mybatisplus.core.metadata.IPage;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.storlead.framework.util.LoginUserUtil;
 import com.storlead.framework.common.result.Result;
-import com.storlead.mail.pojo.entity.EmailTemplatesEntity;
-import com.storlead.mail.pojo.entity.SmtpPopSettingsEntity;
 import com.storlead.mail.pojo.dto.EmailTemplatesDTO;
-import com.storlead.mail.service.EmailTemplatesService;
+import com.storlead.mail.pojo.entity.EmailTemplatesEntity;
+import com.storlead.mail.webclient.client.CrmMailTemplatesRemoteClient;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
 import lombok.extern.log4j.Log4j2;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
-import java.util.Arrays;
-import java.util.Objects;
 
-/**
- * @program: sp-sales-platform
- * @description:
- * @author: chenkq
- * @create: 2024-05-29 16:02
- */
 @RestController
 @RequestMapping("/email/templates")
 @Api(tags = "邮件: 邮件模板")
@@ -36,88 +20,41 @@ import java.util.Objects;
 public class MailTemplatesApiController {
 
     @Resource
-    private EmailTemplatesService templatesService;
+    private CrmMailTemplatesRemoteClient crmMailTemplatesRemoteClient;
 
-    @PostMapping(value = "/page_list")
-    @ApiOperation(value = "获取邮件模板" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-    })
+    @PostMapping("/page_list")
+    @ApiOperation("获取邮件模板")
     public Result<?> pageList(EmailTemplatesDTO dto) {
-
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        Page<EmailTemplatesEntity> page = new Page<>(dto.getPageIndex(),dto.getPageSize());
-        LambdaQueryWrapper<EmailTemplatesEntity> queryWrapper = new LambdaQueryWrapper();
-        // queryWrapper.select(EmailTemplatesEntity::getId,EmailTemplatesEntity::getIsDelete,EmailTemplatesEntity::getTemplateTitle,EmailTemplatesEntity::getTemplateType,EmailTemplatesEntity::getCreateTime,EmailTemplatesEntity::getUpdateTime);
-        queryWrapper.eq(EmailTemplatesEntity::getOwnerBy,currentUserId);
-        if (Objects.nonNull(dto.getTemplateType())) {
-            queryWrapper.eq(EmailTemplatesEntity::getTemplateType,dto.getTemplateType());
-        }
-        queryWrapper.eq(EmailTemplatesEntity::getIsDelete,Integer.valueOf(0));
-        IPage<EmailTemplatesEntity> pageList = templatesService.page(page,queryWrapper);
-        return Result.result(pageList);
+        return crmMailTemplatesRemoteClient.pageList(dto);
     }
 
-    @PostMapping(value = "/getById")
-    @ApiOperation(value = "获取邮件模板详情" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-    })
+    @PostMapping("/getById")
+    @ApiOperation("获取邮件模板详情")
     public Result<?> getById(Long id) {
-        EmailTemplatesEntity templatesEntity = templatesService.getById(id);
-        return Result.result(templatesEntity);
+        return crmMailTemplatesRemoteClient.getById(id);
     }
 
-
-    @PostMapping(value = "/list")
-    @ApiOperation(value = "获取邮件模板" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-    })
+    @PostMapping("/list")
+    @ApiOperation("获取邮件模板列表")
     public Result<?> list(EmailTemplatesDTO dto) {
-
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        Page<EmailTemplatesEntity> page = new Page<>(dto.getPageIndex(),dto.getPageSize());
-        LambdaQueryWrapper<EmailTemplatesEntity> queryWrapper = new LambdaQueryWrapper();
-        queryWrapper.select(EmailTemplatesEntity::getId,EmailTemplatesEntity::getIsDelete,EmailTemplatesEntity::getTemplateTitle,EmailTemplatesEntity::getTemplateType,EmailTemplatesEntity::getCreateTime,EmailTemplatesEntity::getUpdateTime);
-        queryWrapper.eq(EmailTemplatesEntity::getOwnerBy,currentUserId);
-        queryWrapper.eq(EmailTemplatesEntity::getIsDelete,Integer.valueOf(0));
-        if (Objects.nonNull(dto.getTemplateType())) {
-            queryWrapper.eq(EmailTemplatesEntity::getTemplateType,dto.getTemplateType());
-        }
-        queryWrapper.eq(EmailTemplatesEntity::getEnabled,dto.getEnabled());
-        IPage<EmailTemplatesEntity> pageList = templatesService.page(page,queryWrapper);
-        return Result.result(pageList);
+        return crmMailTemplatesRemoteClient.list(dto);
     }
 
-    @PostMapping(value = "/save")
-    @ApiOperation(value = "保存模板" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-    })
+    @PostMapping("/save")
+    @ApiOperation("保存模板")
     public Result<?> save(EmailTemplatesEntity entity) {
-        templatesService.saveOrUpdate(entity);
-        return Result.ok();
+        return crmMailTemplatesRemoteClient.save(entity);
     }
 
-    @ApiOperation(value = "删除")
     @PostMapping("deleteIds")
-    public Result delete(Long [] ids) {
-        if(Objects.isNull(ids)) {
-            return Result.error("参数错误");
-        }
-        templatesService.lgDelete(Arrays.asList(ids));
-        return Result.ok();
+    @ApiOperation("删除")
+    public Result<?> delete(Long[] ids) {
+        return crmMailTemplatesRemoteClient.deleteIds(ids);
     }
 
-
-    @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);
-        return Result.ok();
+    @ApiOperation("禁用或启用")
+    public Result<?> enabled(Long id, Boolean enable) {
+        return crmMailTemplatesRemoteClient.enabled(id, enable);
     }
 }

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

@@ -1,42 +1,18 @@
 package com.storlead.mail.controller;
 
-
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
-import com.baomidou.mybatisplus.core.metadata.IPage;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.storlead.framework.util.LoginUserUtil;
 import com.storlead.framework.common.result.Result;
-import com.storlead.mail.pojo.entity.MailboxAutoReplySetEntity;
-import com.storlead.mail.pojo.entity.SmtpPopSettingsEntity;
 import com.storlead.mail.pojo.dto.EmailTemplatesDTO;
-import com.storlead.mail.service.MailboxAutoReplySetService;
-import com.storlead.mail.service.SmtpPopSettingsService;
+import com.storlead.mail.pojo.entity.MailboxAutoReplySetEntity;
+import com.storlead.mail.webclient.client.CrmMailboxAutoReplyRemoteClient;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
 import lombok.extern.log4j.Log4j2;
-import org.springframework.util.CollectionUtils;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.stream.Collectors;
 
-/**
- * <p>
- * 邮箱自动回复内容 前端控制器
- * </p>
- *
- * @author chenkq
- * @since 2025-02-24
- */
 @RestController
 @RequestMapping("/mail/mailbox/reply")
 @Api(tags = "邮件: 邮件管理")
@@ -44,93 +20,35 @@ import java.util.stream.Collectors;
 public class MailboxAutoReplySetController {
 
     @Resource
-    private MailboxAutoReplySetService replySetService;
+    private CrmMailboxAutoReplyRemoteClient crmMailboxAutoReplyRemoteClient;
 
-    @Resource
-    private SmtpPopSettingsService popSettingsService;
-
-    @PostMapping(value = "/page_list")
-    @ApiOperation(value = "获取邮件自动回复配置" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = MailboxAutoReplySetEntity.class)
-    })
+    @PostMapping("/page_list")
+    @ApiOperation("获取邮件自动回复配置")
     public Result<?> pageList(EmailTemplatesDTO dto) {
-
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        Page<MailboxAutoReplySetEntity> page = new Page<>(dto.getPageIndex(),dto.getPageSize());
-        LambdaQueryWrapper<MailboxAutoReplySetEntity> queryWrapper = new LambdaQueryWrapper();
-        queryWrapper.eq(MailboxAutoReplySetEntity::getOwnerBy,currentUserId);
-        queryWrapper.eq(MailboxAutoReplySetEntity::getIsDelete,Integer.valueOf(0));
-        IPage<MailboxAutoReplySetEntity> pageList = replySetService.page(page,queryWrapper);
-        if (!CollectionUtils.isEmpty(pageList.getRecords())) {
-            List<Long> popIds = pageList.getRecords().stream().map(MailboxAutoReplySetEntity::getSmtpPopId).collect(Collectors.toList());
-            List<SmtpPopSettingsEntity> smtpPopSettings = popSettingsService.list(new LambdaQueryWrapper<SmtpPopSettingsEntity>().in(SmtpPopSettingsEntity::getId, popIds));
-            if (!CollectionUtils.isEmpty(smtpPopSettings)) {
-                Map<Long,String> smtpPopMap =  smtpPopSettings.stream().collect(Collectors.toMap(SmtpPopSettingsEntity::getId,SmtpPopSettingsEntity::getEmailAddress, (existing, replacement) -> existing));
-                pageList.getRecords().forEach(e -> {
-                    e.setMailAddress(smtpPopMap.get(e.getSmtpPopId()));
-                });
-            }
-        }
-        return Result.result(pageList);
+        return crmMailboxAutoReplyRemoteClient.pageList(dto);
     }
 
-    @PostMapping(value = "/list")
-    @ApiOperation(value = "获取邮件模板" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = MailboxAutoReplySetEntity.class)
-    })
+    @PostMapping("/list")
+    @ApiOperation("获取邮件自动回复列表")
     public Result<?> list(EmailTemplatesDTO dto) {
-
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        Page<MailboxAutoReplySetEntity> page = new Page<>(dto.getPageIndex(),dto.getPageSize());
-        LambdaQueryWrapper<MailboxAutoReplySetEntity> queryWrapper = new LambdaQueryWrapper();
-        queryWrapper.eq(MailboxAutoReplySetEntity::getOwnerBy,currentUserId);
-        queryWrapper.eq(MailboxAutoReplySetEntity::getIsDelete,Integer.valueOf(0));
-        queryWrapper.eq(MailboxAutoReplySetEntity::getEnabled,dto.getEnabled());
-        IPage<MailboxAutoReplySetEntity> pageList = replySetService.page(page,queryWrapper);
-        if (!CollectionUtils.isEmpty(pageList.getRecords())) {
-           List<Long> popIds = pageList.getRecords().stream().map(MailboxAutoReplySetEntity::getSmtpPopId).collect(Collectors.toList());
-           List<SmtpPopSettingsEntity> smtpPopSettings = popSettingsService.list(new LambdaQueryWrapper<SmtpPopSettingsEntity>().in(SmtpPopSettingsEntity::getId, popIds));
-           if (!CollectionUtils.isEmpty(smtpPopSettings)) {
-              Map<Long,String> smtpPopMap =  smtpPopSettings.stream().collect(Collectors.toMap(SmtpPopSettingsEntity::getId,SmtpPopSettingsEntity::getEmailAddress, (existing, replacement) -> existing));
-               pageList.getRecords().forEach(e -> {
-                   e.setMailAddress(smtpPopMap.get(e.getSmtpPopId()));
-               });
-           }
-        }
-        return Result.result(pageList);
+        return crmMailboxAutoReplyRemoteClient.list(dto);
     }
 
-    @PostMapping(value = "/save")
-    @ApiOperation(value = "保存" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = MailboxAutoReplySetEntity.class)
-    })
+    @PostMapping("/save")
+    @ApiOperation("保存")
     public Result<?> save(MailboxAutoReplySetEntity entity) {
-        replySetService.saveOrUpdate(entity);
-        return Result.ok();
+        return crmMailboxAutoReplyRemoteClient.save(entity);
     }
 
-    @ApiOperation(value = "删除")
     @PostMapping("deleteIds")
-    public Result delete(Long [] ids) {
-        if(Objects.isNull(ids)) {
-            return Result.error("参数错误");
-        }
-        replySetService.lgDelete(Arrays.asList(ids));
-        return Result.ok();
+    @ApiOperation("删除")
+    public Result<?> delete(Long[] ids) {
+        return crmMailboxAutoReplyRemoteClient.deleteIds(ids);
     }
 
-
-    @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);
-        return Result.ok();
+    @ApiOperation("禁用或启用")
+    public Result<?> enabled(Long id, Boolean enable) {
+        return crmMailboxAutoReplyRemoteClient.enabled(id, enable);
     }
-
 }

+ 0 - 442
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/SmtpPopMailApiController.java

@@ -1,442 +0,0 @@
-package com.storlead.mail.controller;//package com.storlead.sales.mail;
-//
-//import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-//import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-//import com.baomidou.mybatisplus.core.metadata.IPage;
-//import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-//import com.storlead.framework.common.util.DateUtils;
-//import com.storlead.framework.common.util.encryptor.AccessKeyEncryptor;
-//import com.storlead.framework.util.LoginUserUtil;
-//import com.storlead.framework.common.result.Result;
-//import com.storlead.mail.connection.MailConnection;
-//import com.storlead.mail.connection.client.MailConnectionUtil;
-//import com.storlead.mail.connection.config.MailProperties;
-//import com.storlead.mail.entity.EmailsEntity;
-//import com.storlead.mail.entity.MailAttachmentEntity;
-//import com.storlead.mail.entity.SmtpPopSettingsEntity;
-//import com.storlead.mail.pojo.MailDTO;
-//import com.storlead.mail.pojo.SendMailDTO;
-//import com.storlead.mail.properties.MailFileProperties;
-//import com.storlead.mail.service.EmailsService;
-//import com.storlead.mail.service.MailAttachmentService;
-//import com.storlead.mail.service.SmtpPopSettingsService;
-//import com.sun.mail.imap.IMAPFolder;
-//import io.swagger.annotations.Api;
-//import io.swagger.annotations.ApiOperation;
-//import io.swagger.annotations.ApiResponse;
-//import io.swagger.annotations.ApiResponses;
-//import lombok.extern.log4j.Log4j2;
-////import org.springframework.mail.javamail.JavaMailSender;
-//import org.springframework.mail.javamail.MimeMessageHelper;
-//import org.springframework.util.CollectionUtils;
-//import org.springframework.web.bind.annotation.*;
-//import org.springframework.web.multipart.MultipartFile;
-//
-//import javax.annotation.Resource;
-//import javax.mail.*;
-//import javax.mail.internet.InternetAddress;
-//import javax.mail.internet.MimeMessage;
-//import javax.mail.internet.MimeUtility;
-//import javax.mail.search.ComparisonTerm;
-//import javax.mail.search.ReceivedDateTerm;
-//import javax.mail.search.SearchTerm;
-//import java.io.File;
-//import java.io.FileOutputStream;
-//import java.io.IOException;
-//import java.text.SimpleDateFormat;
-//import java.util.*;
-//import java.util.stream.Collectors;
-//
-///**
-// * @program: sp-sales-platform
-// * @description:
-// * @author: chenkq
-// * @create: 2024-05-29 16:02
-// */
-//@RestController
-//@RequestMapping("/mail")
-//@Api(tags = "邮件: 邮件管理")
-//public class SmtpPopMailApiController {
-//
-////    @Resource
-////    private JavaMailSender mailSender;
-//
-//    @Resource
-//    private EmailsService emailsService;
-//
-//    @Resource
-//    private MailFileProperties mailFileProperties;
-//
-//    @Resource
-//    private MailAttachmentService mailAttachmentService;
-//
-//    @Resource
-//    private SmtpPopSettingsService smtpPopSettingsService;
-//
-//
-//    @PostMapping(value = "/pageList")
-//    @ApiOperation(value = "获取邮件" )
-//    @ApiResponses({
-//            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-//    })
-//    public Result<?> pageList(com.storlead.common.object.Page dto) {
-//        Long currentUserId = LoginUserUtil.getCurrentUserId();
-//        SmtpPopSettingsEntity smtpPop = smtpPopSettingsService.getDefaultSmtpPop(currentUserId);
-//
-//        if (Objects.isNull(currentUserId) && Objects.nonNull(smtpPop)) {
-//            return Result.result(null);
-//        }
-//        Page<EmailsEntity> page = new Page<>(dto.getPageIndex(),dto.getPageSize());
-//        LambdaQueryWrapper<EmailsEntity> queryWrapper = new LambdaQueryWrapper();
-//        queryWrapper.eq(EmailsEntity::getOwnerBy,currentUserId);
-//        queryWrapper.eq(EmailsEntity::getFrom,smtpPop.getEmailAddress());
-//        IPage<EmailsEntity> pageList = emailsService.page(page,queryWrapper);
-//        return Result.result(pageList);
-//    }
-//
-//    @PostMapping(value = "/sendMail")
-//    @ApiOperation(value = "发送邮件" )
-//    @ApiResponses({
-//            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-//    })
-//    public Result<?> sendMail(SendMailDTO dto) {
-//        Long currentUserId = LoginUserUtil.getCurrentUserId();
-//        SmtpPopSettingsEntity smtpPop = smtpPopSettingsService.getDefaultSmtpPop(currentUserId);
-//        AccessKeyEncryptor accessKeyEncryptor = AccessKeyEncryptor.getAccessKeyEncryptor("");
-//        String pass = accessKeyEncryptor.decrypt(smtpPop.getEmailPassword());
-//        smtpPop.setEmailPassword(pass);
-//        if (Objects.isNull(currentUserId) && Objects.nonNull(smtpPop)) {
-//            return Result.result(null);
-//        }
-//        try {
-//           Session session = getSendEmailsSession(smtpPop);
-//            try {
-//
-//                InternetAddress [] froms = new InternetAddress[dto.getRecipientls().size()];
-//                for (int i = 0; i < dto.getRecipientls().size(); i++) {
-//                    froms[i] = new InternetAddress(dto.getRecipientls().get(i));
-//                }
-//                if (CollectionUtils.isEmpty(dto.getFiles())) {
-//                    Message message = new MimeMessage(session);
-//                    message.setFrom(new InternetAddress(smtpPop.getEmailAddress()));
-//                    message.setRecipients(Message.RecipientType.TO, froms);
-//                    message.setSubject(dto.getSubject());
-//                    message.setText(dto.getContent());
-//                    Transport.send(message);
-//                } else {
-////                    MimeMessage message = mailSender.createMimeMessage();
-////                    MimeMessageHelper helper = new MimeMessageHelper(message, true);
-////                    helper.setFrom(new InternetAddress(smtpPop.getEmailAddress()));
-////                    helper.setTo(froms);
-////                    helper.setSubject(dto.getSubject());
-////                    helper.setText(dto.getContent());
-////                    List<MultipartFile> files = dto.getFiles();
-////                    for (MultipartFile multipartFile : files) {
-////                        File file = multipartFile.getResource().getFile();
-////                        helper.addAttachment(file.getName(), file);
-////                    }
-//                }
-//            }catch (Exception e) {
-//                log.error("getSendEmailsSession -error",e);
-//            }
-//            return Result.ok();
-//        } catch (Exception e) {
-//            e.printStackTrace();
-//            log.error("send sendMail -error",e);
-//        }
-//        return Result.ok();
-//    }
-//
-//
-//    @PostMapping(value = "/receiveMail")
-//    @ApiOperation(value = "接收邮件" )
-//    @ApiResponses({
-//            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-//    })
-//    public Result<?> receiveMail(MailDTO dto) {
-//        Long currentUserId = LoginUserUtil.getCurrentUserId();
-//        SmtpPopSettingsEntity smtpPop = smtpPopSettingsService.getDefaultSmtpPop(currentUserId);
-//        if (Objects.isNull(currentUserId) && Objects.nonNull(smtpPop)) {
-//            return Result.error("请先设置邮箱账号!");
-//        }
-//
-//        AccessKeyEncryptor accessKeyEncryptor = AccessKeyEncryptor.getAccessKeyEncryptor("");
-//        String pass = accessKeyEncryptor.decrypt(smtpPop.getEmailPassword());
-//        smtpPop.setEmailPassword(pass);
-//        receiveEmails(smtpPop, "INBOX");
-////        receiveEmails(smtpPop, "Sent Items");
-////        receiveEmails(smtpPop, "Drafts");
-////        receiveEmails(smtpPop, "Trash");
-//        return Result.ok();
-//    }
-//
-//
-//    @PostMapping(value = "/receiveMail1")
-//    @ApiOperation(value = "接收邮件" )
-//    @ApiResponses({
-//            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-//    })
-//    public Result<?> receiveMail1(MailDTO dto) {
-//        Long currentUserId = LoginUserUtil.getCurrentUserId();
-//        SmtpPopSettingsEntity smtpPop = smtpPopSettingsService.getDefaultSmtpPop(currentUserId);
-//        if (Objects.isNull(currentUserId) && Objects.nonNull(smtpPop)) {
-//            return Result.error("请先设置邮箱账号!");
-//        }
-//        return Result.ok();
-//    }
-//
-//    private void receiveEmails(SmtpPopSettingsEntity smtpPop, String folderName) {
-//        MailProperties properties = new MailProperties(smtpPop);
-//        MailConnection mailConnection = MailConnectionUtil.receiveEmailsConnection(properties);
-//        if (Objects.isNull(mailConnection)) {
-//            return;
-//        }
-//        List<EmailsEntity> entities = new ArrayList<>();
-//
-//        QueryWrapper queryWrapper = new QueryWrapper<>();
-//        queryWrapper.eq("smtp_pop_id",smtpPop.getId());
-//        queryWrapper.eq("folder",folderName);
-//        queryWrapper.select("max(recipient_date) as recipientDate");
-//        Map<String, Date> qv = emailsService.getMap(queryWrapper);
-//
-//        QueryWrapper queryMsgIdWp = new QueryWrapper<>();
-//        queryMsgIdWp.select("msg_uid");
-//        queryMsgIdWp.eq("folder",folderName);
-//        queryMsgIdWp.eq("smtp_pop_id",smtpPop.getId());
-//        queryMsgIdWp.last("order by msg_uid desc limit 100");
-//
-//        List<String> dis = emailsService.listObjs(queryMsgIdWp);
-//        List<Long> msgUids = new ArrayList<>();
-//        if (!CollectionUtils.isEmpty(dis)) {
-//            msgUids = dis.stream().map(Long::valueOf).collect(Collectors.toList());
-//        }
-//        try {
-//            Store store = mailConnection.getStore();
-//            Folder inbox = store.getFolder(folderName);
-//            inbox.open(Folder.READ_WRITE);
-//            Date beginDate = null;
-//            if (!CollectionUtils.isEmpty(qv) && Objects.nonNull(qv.get("recipientDate"))) {
-//                beginDate = qv.get("recipientDate");
-//            }
-//            if (beginDate == null) {
-//                SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
-//                String dateStr = "2024-03-28"; // 替换为你需要的日期
-//                beginDate = dateFormat.parse(dateStr);
-//            }
-//
-//            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
-//            Date date = dateFormat.parse("2024-05-28");
-//
-//            SearchTerm searchTerm = new ReceivedDateTerm(ComparisonTerm.GT, date);
-//            Message[] messages = inbox.search(searchTerm);
-//            for (Message message : messages) {
-//                Long mesId = getMessageId(message,inbox);
-//                if (!CollectionUtils.isEmpty(msgUids) && msgUids.contains(mesId)) {
-//                    continue;
-//                }
-//                msgUids.add(mesId);
-//                EmailsEntity entity = convertMessageToEmailVo(message,smtpPop);
-//                entity.setSmtpPopId(smtpPop.getId());
-//                entity.setMsgUid(mesId);
-//                entity.setFolder(folderName);
-//                entities.add(entity);
-//                emailsService.save(entity);
-//
-//                String downloadDir = mailFileProperties.getPath().getPath()+File.separator+smtpPop.getEmailAddress()+File.separator;
-//                downloadAttachmentFromMessage(message,downloadDir,entity.getId());
-//            }
-//            inbox.close(false);
-//            mailConnection.close();
-//        }catch (Exception e) {
-//            log.error("receiveEmails - error =",e);
-//            mailConnection.close();
-//        }
-//
-//    }
-//
-//    private Session getSendEmailsSession(SmtpPopSettingsEntity smtpPop) {
-//
-//        Properties properties = new Properties();
-//        properties.put("mail.smtp.host", smtpPop.getSmtpServer());
-//        properties.put("mail.smtp.port", smtpPop.getSmtpPort());
-//        properties.put("mail.smtp.auth", "true");
-//        properties.put("mail.user", smtpPop.getEmailAddress());
-//        properties.put("mail.smtp.starttls.enable",smtpPop.getUseTls());
-//        properties.put("mail.smtp.ssl.enable", smtpPop.getUseSsl());
-//
-//        Session session = Session.getInstance(properties, new Authenticator() {
-//            protected PasswordAuthentication getPasswordAuthentication() {
-//                return new PasswordAuthentication(smtpPop.getEmailAddress(), smtpPop.getEmailPassword());
-//            }
-//        });
-//        return session;
-//    }
-//
-//        private static Long getMessageId(Message message, Folder folder) throws MessagingException {
-//        if (folder instanceof IMAPFolder) {
-//            IMAPFolder imapFolder = (IMAPFolder) folder;
-//            return  imapFolder.getUID(message);
-//        } else {
-//            return null;
-//        }
-//    }
-//
-//
-//    private EmailsEntity convertMessageToEmailVo(Message message,SmtpPopSettingsEntity smtpPop) throws MessagingException, IOException {
-//
-//        EmailsEntity entity = new EmailsEntity();
-//        String subject =  MimeUtility.decodeText(message.getSubject());
-//        entity.setSubject(subject);
-//
-//        Map<String,String>  fromMap = decodeAndPrintAddresses(message.getFrom());
-//        entity.setFrom(mapKeyToString(fromMap));
-//        entity.setFromName(mapValueToString(fromMap));
-//
-//        Map<String,String>  toMap = decodeAndPrintAddresses(message.getRecipients(Message.RecipientType.TO));
-//        entity.setRecipient(mapKeyToString(toMap));
-//        entity.setRecipientName(mapValueToString(toMap));
-//
-//        if (Objects.nonNull(message.getSentDate())) {
-//            entity.setSentDate(message.getSentDate());
-//        }
-//
-//        if (Objects.nonNull(message.getReceivedDate())) {
-//            entity.setRecipientDate(message.getReceivedDate());
-//        }
-//        boolean isSeen = message.isSet(Flags.Flag.SEEN);
-//        entity.setIsRead(isSeen);
-//
-////      Map<String,String>  ccMap = decodeAndPrintAddresses(message.getRecipients(Message.RecipientType.CC));
-////      Map<String,String>  bccMap = decodeAndPrintAddresses(message.getRecipients(Message.RecipientType.BCC));
-//        Object content = message.getContent();
-//        if (content instanceof String) {
-//            entity.setContent(message.getContent().toString());
-//        } else if (content instanceof Multipart) {
-//            Multipart multipart = (Multipart) content;
-//            String str = parseMultipart(multipart);
-//            entity.setContent(str);
-//        }
-//        return entity;
-//    }
-//
-//    private String parseMultipart(Multipart multipart) {
-//        try {
-//            StringBuilder stringBuilder = new StringBuilder();
-//            for (int i = 0; i < multipart.getCount(); i++) {
-//                BodyPart bodyPart = multipart.getBodyPart(i);
-//                if (bodyPart.isMimeType("text/plain")) {
-//                    stringBuilder.append(bodyPart.getContent());
-//                } else if (bodyPart.isMimeType("text/html")) {
-//                    stringBuilder.append(bodyPart.getContent());
-//                } else if (bodyPart.isMimeType("multipart/alternative")) {
-//                    parseMultipart((Multipart) bodyPart.getContent());
-//                } else if (bodyPart.isMimeType("multipart/*")) {
-//                    parseMultipart((Multipart) bodyPart.getContent());
-//                }
-//            }
-//            return stringBuilder.toString();
-//        }catch (Exception e) {
-//            return "";
-//        }
-//    }
-//
-//    private void downloadAttachmentFromMessage(Message message, String downloadDir, long emailId) {
-//        try {
-//            Object content = message.getContent();
-//            if (content instanceof Multipart) {
-//                Multipart multipart = (Multipart) content;
-//                for (int i = 0; i < multipart.getCount(); i++) {
-//                    BodyPart bodyPart = multipart.getBodyPart(i);
-//                    if (Part.ATTACHMENT.equalsIgnoreCase(bodyPart.getDisposition())) {
-//                        saveAttachment(bodyPart, downloadDir, emailId);
-//                    }
-//                }
-//            }
-//        }catch (Exception e) {
-//            log.error("downloadAttachmentFromMessage -- error = ",e);
-//        }
-//    }
-//
-//    private void saveAttachment(BodyPart bodyPart,  String downloadDir, long emailId) throws MessagingException, IOException {
-//        String fileName = bodyPart.getFileName();
-//        fileName = MimeUtility.decodeText(fileName);
-//        String path = DateUtils.date2Str(new Date(), DateUtils.date_yyyy_mm);
-//        // 如果不存在这创建路径
-//        downloadDir = downloadDir + File.separator + path;
-//        if (!new File(downloadDir).exists()) {
-//            new File(downloadDir).mkdirs();
-//        }
-//        File file = new File(downloadDir +  File.separator +fileName);
-//        try (FileOutputStream output = new FileOutputStream(file)) {
-//            bodyPart.getInputStream().transferTo(output);
-//        }
-//        System.out.println("Saved attachment: " + file.getAbsolutePath());
-//        saveAttachmentToDatabase(file, emailId);
-//    }
-//
-//    private void saveAttachmentToDatabase(File file, long emailId) {
-//
-//        String fileName = file.getName();
-//        MailAttachmentEntity attachment = new MailAttachmentEntity();
-//        attachment.setEmailId(emailId);
-//        attachment.setFileName(fileName);
-//        attachment.setFilePath(file.getAbsolutePath());
-//        attachment.setFilePath(file.getAbsolutePath());
-//
-//        String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1);
-//        attachment.setFileExt(fileExtension);
-//        attachment.setFileSize(file.length());
-//        mailAttachmentService.save(attachment);
-//    }
-//
-//    private static String formatDate(Date date) {
-//        if (date == null) return "N/A";
-//        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
-//        return sdf.format(date);
-//    }
-//
-//    private static String decodeAddresses(Address [] addresses) throws MessagingException {
-//        try {
-//            if (addresses == null) return "[]";
-//            String[] decodedAddresses = new String[addresses.length];
-//            for (int i = 0; i < addresses.length; i++) {
-//                decodedAddresses[i] = MimeUtility.decodeText(addresses[i].toString());
-//            }
-//            return Arrays.toString(decodedAddresses);
-//        }catch (Exception e) {
-//            return "";
-//        }
-//    }
-//
-//    private static Map<String,String> decodeAndPrintAddresses(Address[] addresses) throws MessagingException, IOException {
-//
-//        Map<String,String> map = new HashMap<>();
-//        if (addresses == null) return new HashMap<>();
-//        StringBuilder decodedAddresses = new StringBuilder();
-//        for (Address address : addresses) {
-//            InternetAddress internetAddress = (InternetAddress) address;
-//            String personal = internetAddress.getPersonal();
-//            String email = internetAddress.getAddress();
-//            if (personal != null) {
-//                personal = MimeUtility.decodeText(personal);
-//            }
-//            map.put(email,(personal != null ? personal : "N/A"));
-////            decodedAddresses.append("[Name: ").append(personal != null ? personal : "N/A").append(", Email: ").append(email).append("] ");
-//        }
-//        return map;
-//    }
-//
-//    private static String mapKeyToString(Map<String,String> map) {
-//        if (map == null) return "";
-//        return map.entrySet().stream()
-//                .map(entry -> entry.getKey())
-//                .collect(Collectors.joining(","));
-//    }
-//
-//    private static String mapValueToString(Map<String,String> map) {
-//        if (map == null) return "";
-//        return map.entrySet().stream()
-//                .map(entry -> entry.getValue())
-//                .collect(Collectors.joining(","));
-//    }
-//}

+ 38 - 413
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/SmtpPopSettingsApiController.java

@@ -1,48 +1,21 @@
 package com.storlead.mail.controller;
 
-
-import cn.hutool.core.collection.CollectionUtil;
-import cn.hutool.core.util.StrUtil;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
-import com.baomidou.mybatisplus.core.metadata.IPage;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.storlead.framework.common.util.encryptor.AccessKeyEncryptor;
-import com.storlead.framework.util.LoginUserUtil;
+import com.storlead.framework.common.dto.page.PageDTO;
 import com.storlead.framework.common.result.Result;
-import com.storlead.mail.connection.MailConnection;
-import com.storlead.mail.connection.client.MailConnectionUtil;
-import com.storlead.mail.connection.config.MailProperties;
-import com.storlead.mail.pojo.entity.EmailsEntity;
-import com.storlead.mail.pojo.entity.MailboxAutoReplySetEntity;
 import com.storlead.mail.pojo.entity.SmtpPopSettingsEntity;
-import com.storlead.mail.enums.EmailBoxEnum;
-import com.storlead.mail.pojo.vo.MailAcountVO;
-import com.storlead.mail.service.EmailsService;
-import com.storlead.mail.service.SmtpPopSettingsService;
+import com.storlead.mail.webclient.client.CrmAccountRemoteClient;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
 import lombok.extern.log4j.Log4j2;
-import org.springframework.util.CollectionUtils;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
-import javax.mail.*;
-import java.util.*;
 
 /**
- * <p>
- * 邮箱smtp_setting 前端控制器
- * </p>
- *
- * @author chenkq
- * @since 2024-05-28
+ * CRM 邮箱账户(路径不变,转发独立邮件服务)。
  */
 @Log4j2
 @RestController
@@ -51,407 +24,59 @@ import java.util.*;
 public class SmtpPopSettingsApiController {
 
     @Resource
-    private SmtpPopSettingsService smtpPopSettingsService;
-
-    @Resource
-    private EmailsService emailsService;
-
+    private CrmAccountRemoteClient crmAccountRemoteClient;
 
-    @PostMapping(value = "/pageList")
-    @ApiOperation(value = "获取我的邮箱配置信息" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-    })
-    public Result<?> pageList(com.storlead.framework.mybatis.page.Page dto) {
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        if (Objects.isNull(currentUserId)) {
-            return Result.result(null);
-        }
-        AccessKeyEncryptor encryptor = AccessKeyEncryptor.getAccessKeyEncryptor("");
-        Page<SmtpPopSettingsEntity> page = new Page<>(dto.getPageIndex(),dto.getPageSize());
-        LambdaQueryWrapper<SmtpPopSettingsEntity> queryWrapper = new LambdaQueryWrapper();
-        queryWrapper.eq(SmtpPopSettingsEntity::getIsDelete,Integer.valueOf(0));
-        queryWrapper.eq(SmtpPopSettingsEntity::getOwnerBy,currentUserId);
-        IPage<SmtpPopSettingsEntity> pageList =  smtpPopSettingsService.page(page,queryWrapper);
-        if (!CollectionUtils.isEmpty(pageList.getRecords())) {
-            pageList.getRecords().forEach(e -> {
-                e.setEmailPassword(encryptor.decrypt(e.getEmailPassword()));
-            });
-        }
-        return Result.result(pageList);
+    @PostMapping("/pageList")
+    @ApiOperation("获取我的邮箱配置信息")
+    public Result<?> pageList(PageDTO dto) {
+        return crmAccountRemoteClient.pageList(dto);
     }
 
-//    @PostMapping(value = "/listAutoReplySettings")
-//    @ApiOperation(value = "获取我的邮箱配置信息" )
-//    @ApiResponses({
-//            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-//    })
-//    public Result<?> listAutoReplySettings(com.storlead.common.object.Page dto) {
-//        Long currentUserId = LoginUserUtil.getCurrentUserId();
-//        if (Objects.isNull(currentUserId)) {
-//            return Result.result(null);
-//        }
-//        AccessKeyEncryptor encryptor = AccessKeyEncryptor.getAccessKeyEncryptor("");
-//        Page<SmtpPopSettingsEntity> page = new Page<>(dto.getPageIndex(),dto.getPageSize());
-//        LambdaQueryWrapper<SmtpPopSettingsEntity> queryWrapper = new LambdaQueryWrapper();
-//        queryWrapper.eq(SmtpPopSettingsEntity::getIsDelete,Integer.valueOf(0));
-//        queryWrapper.eq(SmtpPopSettingsEntity::getOwnerBy,currentUserId);
-//        IPage<SmtpPopSettingsEntity> pageList =  smtpPopSettingsService.page(page,queryWrapper);
-//        if (!CollectionUtils.isEmpty(pageList.getRecords())) {
-//            pageList.getRecords().forEach(e -> {
-//                e.setEmailPassword(encryptor.decrypt(e.getEmailPassword()));
-//            });
-//        }
-//        return Result.result(pageList);
-//    }
-    @PostMapping(value = "/setReplyContent")
-    @ApiOperation(value = "设置自动回复内容,并且开始" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = MailboxAutoReplySetEntity.class)
-    })
-    public Result<?> setReplyContent(Long id,String replyContent,String replyTitle,Integer autoReplyStatus) {
-        if (Objects.isNull(id) || Objects.isNull(autoReplyStatus)) {
-            return Result.error("参数错误");
-        }
-        if (Integer.valueOf(1).equals(autoReplyStatus)) {
-            if(StrUtil.isBlank(replyContent) || StrUtil.isBlank(replyTitle)) {
-                return Result.error("参数错误");
-            }
-        }
-        LambdaUpdateWrapper<SmtpPopSettingsEntity> updateWrapper = new LambdaUpdateWrapper();
-        updateWrapper.set(SmtpPopSettingsEntity::getReplyContent,replyContent);
-        updateWrapper.set(SmtpPopSettingsEntity::getReplyTitle,replyTitle);
-        updateWrapper.set(SmtpPopSettingsEntity::getReplyOnTime,new Date());
-        updateWrapper.set(SmtpPopSettingsEntity::getIsAutoReply,autoReplyStatus);
-        updateWrapper.eq(SmtpPopSettingsEntity::getId,id);
-        smtpPopSettingsService.update(updateWrapper);
-        return Result.ok();
-    }
-
-    @PostMapping(value = "/setAutoReplyStatus")
-    @ApiOperation(value = "设置自动回复是否开启:1:开启,0:关闭" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = MailboxAutoReplySetEntity.class)
-    })
-    public Result<?> setAutoReplyStatus(Long id,Integer autoReplyStatus) {
-        if (Objects.isNull(id) || Objects.isNull(autoReplyStatus)) {
-            return Result.error("参数错误");
-        }
-        LambdaUpdateWrapper<SmtpPopSettingsEntity> updateWrapper = new LambdaUpdateWrapper();
-        updateWrapper.set(SmtpPopSettingsEntity::getIsAutoReply,autoReplyStatus);
-        updateWrapper.set(SmtpPopSettingsEntity::getReplyOnTime,new Date());
-        updateWrapper.eq(SmtpPopSettingsEntity::getId,id);
-        smtpPopSettingsService.update(updateWrapper);
-        return Result.ok();
+    @PostMapping("/save")
+    @ApiOperation("保存/修改邮箱")
+    public Result<?> save(@RequestBody SmtpPopSettingsEntity entity) {
+        return crmAccountRemoteClient.save(entity);
     }
 
-//    @PostMapping(value = "/list")
-//    @ApiOperation(value = "获取我的邮箱配置信息" )
-//    @ApiResponses({
-//            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-//    })
-//    public Result<?> list(com.storlead.common.object.Page dto) {
-//        Long currentUserId = LoginUserUtil.getCurrentUserId();
-//        if (Objects.isNull(currentUserId)) {
-//            return Result.result(null);
-//        }
-//        Page<SmtpPopSettingsEntity> page = new Page<>(dto.getPageIndex(),dto.getPageSize());
-//        LambdaQueryWrapper<SmtpPopSettingsEntity> queryWrapper = new LambdaQueryWrapper();
-//        queryWrapper.eq(SmtpPopSettingsEntity::getOwnerBy,currentUserId);
-//        IPage<SmtpPopSettingsEntity> pageList =  smtpPopSettingsService.page(page,queryWrapper);
-//        return Result.result(pageList);
-//    }
-
-    @PostMapping(value = "/delete")
-    @ApiOperation(value = "删除" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-    })
+    @PostMapping("/delete")
+    @ApiOperation("删除邮箱")
     public Result<?> delete(Long id) {
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        if (Objects.isNull(currentUserId)) {
-            return Result.result(null);
-        }
-        SmtpPopSettingsEntity smtpPop = smtpPopSettingsService.getById(id);
-        if (Objects.isNull(smtpPop)) {
-            return Result.error("参数错误");
-        }
-
-        LambdaUpdateWrapper<SmtpPopSettingsEntity> updateWrapper = new LambdaUpdateWrapper<>();
-        updateWrapper.set(SmtpPopSettingsEntity::getIsDelete,Integer.valueOf(1));
-        updateWrapper.eq(SmtpPopSettingsEntity::getId,id);
-        smtpPopSettingsService.update(updateWrapper);
-        return Result.ok();
-    }
-
-    @PostMapping(value = "/save")
-    @ApiOperation(value = "保存或修改" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = Result.class)
-    })
-    public Result<?> save(@RequestBody SmtpPopSettingsEntity entity) {
-
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        if (Objects.isNull(currentUserId)) {
-            return Result.result(null);
-        }
-        /**
-         * 保存邮箱先校验
-         */
-        if ("POP3".equals(entity.getProtocolType())) {
-            if (!testPop3(entity)) {
-                MailProperties properties = new MailProperties(entity);
-                MailConnection mailConnection = MailConnectionUtil.receiveEmailsConnection(properties);
-                if (Objects.isNull(mailConnection)) {
-                    return Result.error("邮箱校验失败,请输入正确的账号密码");
-                }
-                mailConnection.close();
-            }
-        }else if("IMAP".equals(entity.getProtocolType())) {
-            MailProperties properties = new MailProperties(entity);
-            MailConnection mailConnection = MailConnectionUtil.receiveEmailsConnection(properties);
-            if (!testImap(entity)) {
-                return Result.error("邮箱校验失败,请输入正确的账号密码");
-            }
-            mailConnection.close();
-        } else {
-            return Result.error("邮箱校验失败,请输入正确的账号密码");
-        }
-        if (!testSmtp(entity)) {
-            return Result.error("邮箱校验失败,请输入正确的账号密码");
-        }
-        /**
-         * 修改和增加根据邮箱进行查重
-         */
-        AccessKeyEncryptor encryptor = AccessKeyEncryptor.getAccessKeyEncryptor("");
-        entity.setEmailPassword(encryptor.encrypt(entity.getEmailPassword()));
-        if (!Integer.valueOf(1).equals(entity.getUseDefault())) {
-            LambdaQueryWrapper<SmtpPopSettingsEntity> queryWrapper = new LambdaQueryWrapper();
-            queryWrapper.eq(SmtpPopSettingsEntity::getUseDefault, Integer.valueOf(1));
-            queryWrapper.eq(SmtpPopSettingsEntity::getOwnerBy, currentUserId);
-            queryWrapper.eq(SmtpPopSettingsEntity::getIsDelete, Integer.valueOf(0));
-            queryWrapper.last("limit 1");
-            SmtpPopSettingsEntity c = smtpPopSettingsService.getOne(queryWrapper);
-            if (Objects.isNull(c)) {
-                return Result.error("请设置默认邮箱");
-            }
-            if (Objects.nonNull(c) && c.getId().equals(entity.getId())) {
-                return Result.error("请设置默认邮箱");
-            }
-        }
-
-        if (Objects.nonNull(entity.getId())) {
-            // 修改
-            SmtpPopSettingsEntity old = smtpPopSettingsService.getById(entity.getId());
-            if (Objects.nonNull(old)) {
-                if(!old.getEmailAddress().equals(entity.getEmailAddress())){
-                    return Result.error("已绑定的邮箱无法修改,可新增邮箱配置!");
-                }
-            }
-        }
-        Boolean b = smtpPopSettingsService.saveOrUpdate(entity);
-        if (Integer.valueOf(1).equals(entity.getUseDefault()) && b) {
-            // 如果设置了默认邮箱,则将所有默认邮箱改为非默认
-            LambdaUpdateWrapper<SmtpPopSettingsEntity> updateWrapper = new LambdaUpdateWrapper();
-            updateWrapper.set(SmtpPopSettingsEntity::getUseDefault,Integer.valueOf(0));
-            updateWrapper.eq(SmtpPopSettingsEntity::getOwnerBy,currentUserId);
-            updateWrapper.ne(SmtpPopSettingsEntity::getId,entity.getId());
-            smtpPopSettingsService.update(updateWrapper);
-        }
-
-        return Result.ok();
+        return crmAccountRemoteClient.delete(id);
     }
 
-    @PostMapping(value = "/defaultUse")
-    @ApiOperation(value = "设置默认邮箱" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = Result.class)
-    })
+    @PostMapping("/defaultUse")
+    @ApiOperation("设置默认邮箱")
     public Result<?> defaultUse(Long id) {
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        if (Objects.isNull(currentUserId) || Objects.isNull(id)) {
-            return Result.result(null);
-        }
-
-        LambdaUpdateWrapper<SmtpPopSettingsEntity> updateWrapper = new LambdaUpdateWrapper();
-        updateWrapper.set(SmtpPopSettingsEntity::getUseDefault,Integer.valueOf(0));
-        updateWrapper.eq(SmtpPopSettingsEntity::getOwnerBy,currentUserId);
-        smtpPopSettingsService.update(updateWrapper);
-
-        updateWrapper = new LambdaUpdateWrapper();
-        updateWrapper.eq(SmtpPopSettingsEntity::getId,id);
-        updateWrapper.eq(SmtpPopSettingsEntity::getOwnerBy,currentUserId);
-        updateWrapper.set(SmtpPopSettingsEntity::getUseDefault,Integer.valueOf(1));
-        smtpPopSettingsService.update(updateWrapper);
-        /**
-         * 需要当前默认邮箱的邮件和未读数
-         */
-        return Result.ok();
+        return crmAccountRemoteClient.defaultUse(id);
     }
 
-
-    @PostMapping(value = "/isEnabledDelete")
-    @ApiOperation(value = "是否开启删除服务器邮件;0 不开启,1:开启" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-    })
-    public Result<?> setReplyContent(Long id,Integer isEnabled) {
-        if (Objects.isNull(id) || Objects.isNull(isEnabled)) {
-            return Result.error("参数错误");
-        }
-        LambdaUpdateWrapper<SmtpPopSettingsEntity> updateWrapper = new LambdaUpdateWrapper();
-        updateWrapper.set(SmtpPopSettingsEntity::getIsDeleteMail,isEnabled);
-        updateWrapper.eq(SmtpPopSettingsEntity::getId,id);
-        smtpPopSettingsService.update(updateWrapper);
-        return Result.ok();
+    @PostMapping("/setReplyContent")
+    @ApiOperation("设置自动回复内容")
+    public Result<?> setReplyContent(Long id, String replyContent, String replyTitle, Integer autoReplyStatus) {
+        return crmAccountRemoteClient.setReplyContent(id, replyContent, replyTitle, autoReplyStatus);
     }
 
-
-    @PostMapping(value = "/personal_mail_account")
-    @ApiOperation(value = "获取个人邮箱账户" )
-    public Result personalMailAccount() {
-
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        List<SmtpPopSettingsEntity> emails =  smtpPopSettingsService.getEmailAccount(currentUserId);
-        QueryWrapper<EmailsEntity> queryWrapper = new QueryWrapper<EmailsEntity>();
-        queryWrapper.select("smtp_pop_id", "COUNT(*) AS email_count");
-        queryWrapper.eq("owner_by", currentUserId);
-        queryWrapper.eq("folder", EmailBoxEnum.INBOX.code);
-        queryWrapper.eq("is_trash", Integer.valueOf(0));
-        queryWrapper.eq("is_read", Integer.valueOf(0));
-        queryWrapper.eq("is_delete", Integer.valueOf(0));
-        queryWrapper.isNotNull("smtp_pop_id");
-        queryWrapper.ne("`from`", "system@storlead.com");
-        queryWrapper.groupBy("smtp_pop_id");
-        List<Map<String,Object>> readMapls = emailsService.listMaps(queryWrapper);
-        Map<Long, Integer> readMap = new HashMap<>();
-        if (!CollectionUtils.isEmpty(readMapls)) {
-            for (Map<String, Object> map : readMapls) {
-                Long key = Long.valueOf(map.get("smtp_pop_id").toString());
-                Integer val = Integer.valueOf(map.get("email_count").toString());
-                readMap.put(key, val);
-            }
-        }
-
-        List<MailAcountVO> mails = new ArrayList<>();
-        if (CollectionUtil.isNotEmpty(emails)) {
-            emails.forEach(e -> {
-                MailAcountVO mailvo = new MailAcountVO();
-                mailvo.setMailAccount(e.getEmailAddress());
-                mailvo.setId(e.getId());
-                mailvo.setUseDefault(e.getUseDefault());
-                Integer readNum = readMap.get(e.getId());
-                if (Objects.nonNull(readNum)) {
-                    mailvo.setUnReadCount(readNum);
-                } else {
-                    mailvo.setUnReadCount(0);
-                }
-                mails.add(mailvo);
-            });
-        }
-        return Result.result(mails);
+    @PostMapping("/setAutoReplyStatus")
+    @ApiOperation("开关自动回复")
+    public Result<?> setAutoReplyStatus(Long id, Integer autoReplyStatus) {
+        return crmAccountRemoteClient.setAutoReplyStatus(id, autoReplyStatus);
     }
 
-    @PostMapping(value = "/test_connect")
-    @ApiOperation(value = "测试" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-    })
-    public Result<?> receiveMail(@RequestBody SmtpPopSettingsEntity mailSet) {
-
-        if (!testSmtp(mailSet)) {
-            return Result.error("邮箱校验失败,请输入正确的账号密码");
-        }
-        if ("POP3".equals(mailSet.getProtocolType())) {
-            if (!testPop3(mailSet)) {
-                return Result.error("邮箱校验失败,请输入正确的账号密码");
-            }
-        }else if("IMAP".equals(mailSet.getProtocolType())) {
-            if (!testImap(mailSet)) {
-                return Result.error("邮箱校验失败,请输入正确的账号密码");
-            }
-        } else {
-            return Result.error("邮箱校验失败,请输入正确的账号密码");
-        }
-        return Result.ok();
+    @PostMapping("/isEnabledDelete")
+    @ApiOperation("是否开启删除服务器邮件")
+    public Result<?> isEnabledDelete(Long id, Integer isEnabled) {
+        return crmAccountRemoteClient.isEnabledDelete(id, isEnabled);
     }
 
-
-
-    public Boolean testImap(SmtpPopSettingsEntity mailSet){
-        try {
-            MailProperties properties = new MailProperties(mailSet);
-            MailConnection mailConnection = MailConnectionUtil.testReceiveEmailsConnection(properties);
-            if (Objects.isNull(mailConnection)) {
-                return false;
-            }
-            log.error("mailConnection -- testImap == "+mailConnection);
-//            String host = mailSet.getReceiveServer();
-//            String port = mailSet.getReceivePort();
-//            String username = mailSet.getEmailAddress();
-//            String password = mailSet.getEmailPassword();;
-//
-//            Properties properties = new Properties();
-//            properties.put("mail.imap.host", host);
-//            properties.put("mail.imap.port", port);
-//            properties.put("mail.imap.ssl.enable",Integer.valueOf(1).equals(mailSet.getReceiveSsl()) ? "true" : "false");
-//            Session session = Session.getDefaultInstance(properties);
-//            Store store = session.getStore("imap");
-//            store.connect(username, password);
-//            store.close();
-//            session = null;
-            mailConnection.close();
-
-            return true;
-        } catch (Exception e) {
-            log.error("testImap - error",e);
-            return false;
-        }
+    @PostMapping("/personal_mail_account")
+    @ApiOperation("个人邮箱账户(含未读数)")
+    public Result<?> personalMailAccount() {
+        return crmAccountRemoteClient.personalMailAccount();
     }
 
-    public Boolean testPop3(SmtpPopSettingsEntity mailSet){
-        try {
-            MailProperties properties = new MailProperties(mailSet);
-            MailConnection mailConnection = MailConnectionUtil.testReceiveEmailsConnection(properties);
-            if (Objects.isNull(mailConnection)) {
-                return false;
-            }
-            log.error("mailConnection -- testPop3 == "+mailConnection);
-            mailConnection.close();
-
-            return true;
-        } catch (Exception e) {
-            log.error("testPop3 - error",e);
-            return false;
-        }
+    @PostMapping("/test_connect")
+    @ApiOperation("测试邮箱连通性")
+    public Result<?> testConnect(@RequestBody SmtpPopSettingsEntity mailSet) {
+        return crmAccountRemoteClient.testConnect(mailSet);
     }
-
-    public Boolean testSmtp(SmtpPopSettingsEntity smtpPop){
-        try {
-            Properties properties = new Properties();
-            properties.put("mail.smtp.host", smtpPop.getSmtpServer());
-            properties.put("mail.smtp.port", smtpPop.getSmtpPort());
-            properties.put("mail.smtp.auth", "true");
-            properties.put("mail.user", smtpPop.getEmailAddress());
-            properties.put("mail.smtp.starttls.enable",Integer.valueOf(1).equals(smtpPop.getSmtpTls()) ? "true" : "false");
-            properties.put("mail.smtp.ssl.enable", Integer.valueOf(1).equals(smtpPop.getSmtpSsl()) ? "true" : "false");
-
-            String username = smtpPop.getEmailAddress();
-            String password = smtpPop.getEmailPassword();
-
-            Session session = Session.getInstance(properties, new Authenticator() {
-                protected PasswordAuthentication getPasswordAuthentication() {
-                    return new PasswordAuthentication(username, password);
-                }
-            });
-            Transport transport = session.getTransport("smtp");
-            transport.connect();
-            transport.close();
-            return true;
-        } catch (Exception e) {
-            log.error("testSmtp - error",e);
-            return false;
-        }
-    }
-
 }

+ 17 - 100
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/controller/UserEmailFolderApiController.java

@@ -1,135 +1,52 @@
 package com.storlead.mail.controller;
 
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
-import com.baomidou.mybatisplus.core.metadata.IPage;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.storlead.framework.common.constant.CommonConstant;
-import com.storlead.framework.util.LoginUserUtil;
 import com.storlead.framework.common.result.Result;
 import com.storlead.mail.pojo.dto.EmailTemplatesDTO;
-import com.storlead.mail.pojo.entity.EmailsEntity;
-import com.storlead.mail.pojo.entity.SmtpPopSettingsEntity;
 import com.storlead.mail.pojo.entity.UserEmailFolderEntity;
-import com.storlead.mail.service.EmailsService;
-import com.storlead.mail.service.UserEmailFolderService;
+import com.storlead.mail.webclient.client.CrmUserEmailFolderRemoteClient;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Objects;
 
-/**
- * <p>
- * 用户文件夹 前端控制器
- * </p>
- *
- * @author chenkq
- * @since 2024-11-12
- */
 @RestController
 @RequestMapping("/user/email/folder")
 @Api(tags = "邮件: 邮件自定义文件夹")
 public class UserEmailFolderApiController {
-    @Resource
-    private UserEmailFolderService emailFolderService;
 
     @Resource
-    private EmailsService emailsService;
+    private CrmUserEmailFolderRemoteClient crmUserEmailFolderRemoteClient;
 
-    @PostMapping(value = "/page_list")
-    @ApiOperation(value = "获取用户文件夹(分页)" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-    })
+    @PostMapping("/page_list")
+    @ApiOperation("获取用户文件夹(分页)")
     public Result<?> pageList(EmailTemplatesDTO dto) {
-
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        Page<UserEmailFolderEntity> page = new Page<>(dto.getPageIndex(),dto.getPageSize());
-        LambdaQueryWrapper<UserEmailFolderEntity> queryWrapper = new LambdaQueryWrapper();
-        queryWrapper.eq(UserEmailFolderEntity::getOwnerBy,currentUserId);
-        queryWrapper.eq(UserEmailFolderEntity::getIsDelete,Integer.valueOf(0));
-        IPage<UserEmailFolderEntity> pageList = emailFolderService.page(page,queryWrapper);
-        return Result.result(pageList);
+        return crmUserEmailFolderRemoteClient.pageList(dto);
     }
 
-    @PostMapping(value = "/list")
-    @ApiOperation(value = "获取用户文件夹" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-    })
+    @PostMapping("/list")
+    @ApiOperation("获取用户文件夹")
     public Result<?> list() {
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        LambdaQueryWrapper<UserEmailFolderEntity> queryWrapper = new LambdaQueryWrapper();
-        queryWrapper.eq(UserEmailFolderEntity::getOwnerBy,currentUserId);
-        queryWrapper.eq(UserEmailFolderEntity::getIsDelete,Integer.valueOf(0));
-        queryWrapper.eq(UserEmailFolderEntity::getEnabled,Integer.valueOf(1));
-        List<UserEmailFolderEntity> ls = emailFolderService.list(queryWrapper);
-        return Result.result(ls);
+        return crmUserEmailFolderRemoteClient.list();
     }
 
-    @PostMapping(value = "/save")
-    @ApiOperation(value = "绑定文件夹入站规则" )
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-    })
+    @PostMapping("/save")
+    @ApiOperation("保存文件夹")
     public Result<?> save(UserEmailFolderEntity entity) {
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        LambdaQueryWrapper<UserEmailFolderEntity> queryWrapper = new LambdaQueryWrapper<>();
-        queryWrapper.eq(UserEmailFolderEntity::getFolderName,entity.getFolderName());
-        queryWrapper.eq(UserEmailFolderEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
-        queryWrapper.eq(UserEmailFolderEntity::getOwnerBy,currentUserId);
-
-        if (Objects.isNull(entity.getId())) {
-            Integer exit = emailFolderService.count(queryWrapper);
-            if (exit > 0) {
-                return Result.error("文件夹'"+ entity.getFolderName() +"'已存在!");
-            }
-        } else {
-            UserEmailFolderEntity oldFolder = emailFolderService.getOne(queryWrapper);
-            if (Objects.nonNull(oldFolder)) {
-                if (!oldFolder.getId().equals(entity.getId())) {
-                    return Result.error("文件夹'"+ entity.getFolderName() +"'已存在,无法修改!");
-                }
-            }
-        }
-        emailFolderService.saveOrUpdate(entity);
-        return Result.ok();
+        return crmUserEmailFolderRemoteClient.save(entity);
     }
 
-    @ApiOperation(value = "删除")
     @PostMapping("deleteIds")
-    public Result delete(Long [] ids) {
-        if(Objects.isNull(ids)) {
-            return Result.error("参数错误");
-        }
-        /**
-         * 需要修改邮件文件夹标记为''
-         */
-        emailFolderService.lgDelete(Arrays.asList(ids));
-
-        LambdaUpdateWrapper<EmailsEntity> updateWrapper = new LambdaUpdateWrapper<>();
-        updateWrapper.in(EmailsEntity::getCustomFolderId,Arrays.asList(ids));
-        updateWrapper.set(EmailsEntity::getCustomFolderId,null);
-        emailsService.update(updateWrapper);
-        return Result.ok();
+    @ApiOperation("删除")
+    public Result<?> delete(Long[] ids) {
+        return crmUserEmailFolderRemoteClient.deleteIds(ids);
     }
 
-
-    @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);
-        return Result.ok();
+    @ApiOperation("禁用或启用")
+    public Result<?> enabled(Long id, Boolean enable) {
+        return crmUserEmailFolderRemoteClient.enabled(id, enable);
     }
 }

+ 26 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/MailRemoteClientFacade.java

@@ -0,0 +1,26 @@
+package com.storlead.mail.webclient;
+
+import com.storlead.mail.webclient.client.*;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 邮件远程客户端聚合入口,业务侧可只注入本类。
+ */
+@Getter
+@RequiredArgsConstructor
+public class MailRemoteClientFacade {
+
+    private final MktMailRemoteClient mktMail;
+    private final MktAccountRemoteClient mktAccount;
+    private final CrmMailRemoteClient crmMail;
+    private final CrmAccountRemoteClient crmAccount;
+    private final CrmMailFileRemoteClient crmMailFile;
+    private final CrmMailTemplatesRemoteClient crmMailTemplates;
+    private final CrmMailBlacklistRemoteClient crmMailBlacklist;
+    private final CrmUserEmailFolderRemoteClient crmUserEmailFolder;
+    private final CrmEmailFolderRuleRemoteClient crmEmailFolderRule;
+    private final CrmMailboxAutoReplyRemoteClient crmMailboxAutoReply;
+    private final CrmMailSignatureRemoteClient crmMailSignature;
+    private final CrmThumbnailRemoteClient crmThumbnail;
+}

+ 56 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmAccountRemoteClient.java

@@ -0,0 +1,56 @@
+package com.storlead.mail.webclient.client;
+
+import com.storlead.framework.common.dto.page.PageDTO;
+import com.storlead.framework.common.result.Result;
+import com.storlead.mail.pojo.entity.SmtpPopSettingsEntity;
+import com.storlead.mail.webclient.constant.MailRemoteApiConstants;
+import com.storlead.mail.webclient.support.MailRemoteHttpSupport;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * CRM 邮箱账户远程客户端,对齐 B 服务 {@code /smtp/pop/setting/**}。
+ */
+@RequiredArgsConstructor
+public class CrmAccountRemoteClient {
+
+    private final MailRemoteHttpSupport http;
+
+    public Result<?> pageList(PageDTO dto) {
+        return http.postForm(MailRemoteApiConstants.CRM_ACCOUNT_PAGE_LIST, dto);
+    }
+
+    public Result<?> save(SmtpPopSettingsEntity entity) {
+        return http.postJson(MailRemoteApiConstants.CRM_ACCOUNT_SAVE, entity);
+    }
+
+    public Result<?> delete(Long id) {
+        return http.postForm(MailRemoteApiConstants.CRM_ACCOUNT_DELETE, http.formOf("id", id));
+    }
+
+    public Result<?> defaultUse(Long id) {
+        return http.postForm(MailRemoteApiConstants.CRM_ACCOUNT_DEFAULT_USE, http.formOf("id", id));
+    }
+
+    public Result<?> setReplyContent(Long id, String replyContent, String replyTitle, Integer autoReplyStatus) {
+        return http.postForm(MailRemoteApiConstants.CRM_ACCOUNT_SET_REPLY_CONTENT,
+                http.formOf("id", id, "replyContent", replyContent, "replyTitle", replyTitle, "autoReplyStatus", autoReplyStatus));
+    }
+
+    public Result<?> setAutoReplyStatus(Long id, Integer autoReplyStatus) {
+        return http.postForm(MailRemoteApiConstants.CRM_ACCOUNT_SET_AUTO_REPLY_STATUS,
+                http.formOf("id", id, "autoReplyStatus", autoReplyStatus));
+    }
+
+    public Result<?> isEnabledDelete(Long id, Integer isEnabled) {
+        return http.postForm(MailRemoteApiConstants.CRM_ACCOUNT_IS_ENABLED_DELETE,
+                http.formOf("id", id, "isEnabled", isEnabled));
+    }
+
+    public Result<?> personalMailAccount() {
+        return http.postForm(MailRemoteApiConstants.CRM_ACCOUNT_PERSONAL, http.formOf());
+    }
+
+    public Result<?> testConnect(SmtpPopSettingsEntity mailSet) {
+        return http.postJson(MailRemoteApiConstants.CRM_ACCOUNT_TEST_CONNECT, mailSet);
+    }
+}

+ 46 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmEmailFolderRuleRemoteClient.java

@@ -0,0 +1,46 @@
+package com.storlead.mail.webclient.client;
+
+import com.storlead.framework.common.result.Result;
+import com.storlead.mail.pojo.dto.EmailTemplatesDTO;
+import com.storlead.mail.pojo.dto.FolderRuleDTO;
+import com.storlead.mail.pojo.entity.EmailFolderRuleEntity;
+import com.storlead.mail.webclient.constant.MailRemoteApiConstants;
+import com.storlead.mail.webclient.support.MailRemoteHttpSupport;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 邮件入站规则远程客户端,对齐 B 服务 {@code /email/folder/rule/**}。
+ */
+@RequiredArgsConstructor
+public class CrmEmailFolderRuleRemoteClient {
+
+    private final MailRemoteHttpSupport http;
+
+    public Result<?> getFolderRule() {
+        return http.postForm(MailRemoteApiConstants.FOLDER_RULE_GET, http.formOf());
+    }
+
+    public Result<?> pageList(EmailTemplatesDTO dto) {
+        return http.postForm(MailRemoteApiConstants.FOLDER_RULE_PAGE_LIST, dto);
+    }
+
+    public Result<?> list() {
+        return http.postForm(MailRemoteApiConstants.FOLDER_RULE_LIST, http.formOf());
+    }
+
+    public Result<?> save(EmailFolderRuleEntity entity) {
+        return http.postForm(MailRemoteApiConstants.FOLDER_RULE_SAVE, entity);
+    }
+
+    public Result<?> saveBatch(FolderRuleDTO dto) {
+        return http.postJson(MailRemoteApiConstants.FOLDER_RULE_SAVE_BATCH, dto);
+    }
+
+    public Result<?> deleteIds(Long[] ids) {
+        return http.postForm(MailRemoteApiConstants.FOLDER_RULE_DELETE_IDS, http.formOf("ids", ids));
+    }
+
+    public Result<?> enabled(Long id, Boolean enable) {
+        return http.postForm(MailRemoteApiConstants.FOLDER_RULE_ENABLED, http.formOf("id", id, "enable", enable));
+    }
+}

+ 37 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmMailBlacklistRemoteClient.java

@@ -0,0 +1,37 @@
+package com.storlead.mail.webclient.client;
+
+import com.storlead.framework.common.result.Result;
+import com.storlead.mail.pojo.dto.EmailTemplatesDTO;
+import com.storlead.mail.pojo.entity.EmailBlacklistRecordEntity;
+import com.storlead.mail.webclient.constant.MailRemoteApiConstants;
+import com.storlead.mail.webclient.support.MailRemoteHttpSupport;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 邮件黑名单远程客户端,对齐 B 服务 {@code /email/blacklist/**}。
+ */
+@RequiredArgsConstructor
+public class CrmMailBlacklistRemoteClient {
+
+    private final MailRemoteHttpSupport http;
+
+    public Result<?> pageList(EmailTemplatesDTO dto) {
+        return http.postForm(MailRemoteApiConstants.BLACKLIST_PAGE_LIST, dto);
+    }
+
+    public Result<?> list(EmailTemplatesDTO dto) {
+        return http.postForm(MailRemoteApiConstants.BLACKLIST_LIST, dto);
+    }
+
+    public Result<?> save(EmailBlacklistRecordEntity entity) {
+        return http.postForm(MailRemoteApiConstants.BLACKLIST_SAVE, entity);
+    }
+
+    public Result<?> deleteIds(Long[] ids) {
+        return http.postForm(MailRemoteApiConstants.BLACKLIST_DELETE_IDS, http.formOf("ids", ids));
+    }
+
+    public Result<?> enabled(Long id, Boolean enable) {
+        return http.postForm(MailRemoteApiConstants.BLACKLIST_ENABLED, http.formOf("id", id, "enable", enable));
+    }
+}

+ 36 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmMailFileRemoteClient.java

@@ -0,0 +1,36 @@
+package com.storlead.mail.webclient.client;
+
+import com.storlead.framework.common.result.Result;
+import com.storlead.mail.webclient.constant.MailRemoteApiConstants;
+import com.storlead.mail.webclient.support.MailRemoteHttpSupport;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * CRM 邮件附件远程客户端,对齐 B 服务 {@code /mail/file/**}。
+ */
+@RequiredArgsConstructor
+public class CrmMailFileRemoteClient {
+
+    private final MailRemoteHttpSupport http;
+
+    public Result<?> cacheMailFile(List<MultipartFile> files) {
+        return http.postFiles(MailRemoteApiConstants.CRM_FILE_CACHE, "files", files, null);
+    }
+
+    /** B 服务该接口直接返回 Map(非 Result 包装)。 */
+    public Map<String, Object> cacheImageFile(MultipartFile file) {
+        return http.postFileForMap(MailRemoteApiConstants.CRM_FILE_CACHE_IMAGE, "upload", file, null);
+    }
+
+    public byte[] downloadMailFile(Long fileId) {
+        return http.postForBytes(MailRemoteApiConstants.CRM_FILE_DOWNLOAD, http.formOf("fileId", fileId));
+    }
+
+    public byte[] getImage(String imageName) {
+        return http.getForBytes(MailRemoteApiConstants.CRM_FILE_IMAGES_PREFIX + imageName, null);
+    }
+}

+ 116 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmMailRemoteClient.java

@@ -0,0 +1,116 @@
+package com.storlead.mail.webclient.client;
+
+import com.storlead.framework.common.result.Result;
+import com.storlead.mail.pojo.AttachmentDTO;
+import com.storlead.mail.pojo.MailDTO;
+import com.storlead.mail.pojo.MailMoveDTO;
+import com.storlead.mail.pojo.SendMailDTO;
+import com.storlead.mail.webclient.constant.MailRemoteApiConstants;
+import com.storlead.mail.webclient.support.MailRemoteHttpSupport;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * CRM 邮件主流程远程客户端,对齐 B 服务 {@code /mail/**}。
+ */
+@RequiredArgsConstructor
+public class CrmMailRemoteClient {
+
+    private final MailRemoteHttpSupport http;
+
+    public Result<?> sendMail(SendMailDTO dto) {
+        return http.postForm(MailRemoteApiConstants.CRM_SEND_MAIL, dto);
+    }
+
+    public Result<?> asynSendMail(SendMailDTO dto) {
+        return http.postForm(MailRemoteApiConstants.CRM_ASYN_SEND_MAIL, dto);
+    }
+
+    public Result<?> saveDraft(SendMailDTO dto) {
+        return http.postForm(MailRemoteApiConstants.CRM_SAVE_DRAFT, dto);
+    }
+
+    public Result<?> receiveMail() {
+        return http.postForm(MailRemoteApiConstants.CRM_RECEIVE_MAIL, http.formOf());
+    }
+
+    public byte[] downloadMailEmlZip(MailDTO dto) {
+        return http.postForBytes(MailRemoteApiConstants.CRM_DOWNLOAD_MAIL_EML_ZIP, http.toForm(dto));
+    }
+
+    public byte[] downloadMailEml(MailDTO dto) {
+        return http.postForBytes(MailRemoteApiConstants.CRM_DOWNLOAD_MAIL_EML, http.toForm(dto));
+    }
+
+    public Result<?> mvMailBox(MailMoveDTO dto) {
+        return http.postForm(MailRemoteApiConstants.CRM_MV_MAIL_BOX, dto);
+    }
+
+    public Result<?> markTrash(MailMoveDTO dto) {
+        return http.postForm(MailRemoteApiConstants.CRM_MARK_TRASH, dto);
+    }
+
+    public Result<?> deleteIds(Long[] ids) {
+        return http.postForm(MailRemoteApiConstants.CRM_DELETE_IDS, http.formOf("ids", ids));
+    }
+
+    public Result<?> deleteDraftIds(Long[] ids) {
+        return http.postForm(MailRemoteApiConstants.CRM_DELETE_DRAFT_IDS, http.formOf("ids", ids));
+    }
+
+    public Result<?> getMailDetail(Long mailId) {
+        return http.postForm(MailRemoteApiConstants.CRM_GET_MAIL_DETAIL, http.formOf("mailId", mailId));
+    }
+
+    public Result<?> mvCustomFolder(Long[] mailIds, Long folderId) {
+        return http.postForm(MailRemoteApiConstants.CRM_MV_CUSTOM_FOLDER,
+                http.formOf("mailIds", mailIds, "folderId", folderId));
+    }
+
+    public Result<?> delCustomFolderMail(Long[] mailIds) {
+        return http.postForm(MailRemoteApiConstants.CRM_DEL_CUSTOM_FOLDER_MAIL, http.formOf("mailIds", mailIds));
+    }
+
+    public Result<?> markStar(MailMoveDTO dto) {
+        return http.postForm(MailRemoteApiConstants.CRM_MARK_STAR, dto);
+    }
+
+    public Result<?> markRead(MailMoveDTO dto) {
+        return http.postForm(MailRemoteApiConstants.CRM_MARK_READ, dto);
+    }
+
+    public Result<?> markReplyMsgId(MailMoveDTO dto) {
+        return http.postForm(MailRemoteApiConstants.CRM_MARK_REPLY_MSG_ID, dto);
+    }
+
+    public Result<?> markAllRead() {
+        return http.postForm(MailRemoteApiConstants.CRM_MARK_ALL_READ, http.formOf());
+    }
+
+    public Result<?> listMailAttachment(AttachmentDTO dto) {
+        return http.postForm(MailRemoteApiConstants.CRM_LIST_MAIL_ATTACHMENT, dto);
+    }
+
+    public byte[] downloadZip(AttachmentDTO dto) {
+        return http.postForBytes(MailRemoteApiConstants.CRM_DOWNLOAD_ZIP, http.toForm(dto));
+    }
+
+    public Result<?> downloadUrl(Long attachmentId) {
+        return http.postForm(MailRemoteApiConstants.CRM_DOWNLOAD_URL, http.formOf("attachmentId", attachmentId));
+    }
+
+    public Result<?> draftDownloadUrl(Long attachmentId) {
+        return http.postForm(MailRemoteApiConstants.CRM_DRAFT_DOWNLOAD_URL, http.formOf("attachmentId", attachmentId));
+    }
+
+    public byte[] draftDownload(Long attachmentId) {
+        return http.postForBytes(MailRemoteApiConstants.CRM_DRAFT_DOWNLOAD, http.formOf("attachmentId", attachmentId));
+    }
+
+    public byte[] download(Long attachmentId) {
+        return http.postForBytes(MailRemoteApiConstants.CRM_DOWNLOAD, http.formOf("attachmentId", attachmentId));
+    }
+
+    public Result<?> mailPortConfig(String mailAccount) {
+        return http.postForm(MailRemoteApiConstants.CRM_MAIL_PORT_CONFIG, http.formOf("mailAccount", mailAccount));
+    }
+}

+ 49 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmMailSignatureRemoteClient.java

@@ -0,0 +1,49 @@
+package com.storlead.mail.webclient.client;
+
+import com.storlead.framework.common.result.Result;
+import com.storlead.mail.pojo.dto.EmailSignatureDTO;
+import com.storlead.mail.pojo.entity.EmailSignatureEntity;
+import com.storlead.mail.webclient.constant.MailRemoteApiConstants;
+import com.storlead.mail.webclient.support.MailRemoteHttpSupport;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 邮件签名远程客户端,对齐 B 服务 {@code /email/signature/**}。
+ */
+@RequiredArgsConstructor
+public class CrmMailSignatureRemoteClient {
+
+    private final MailRemoteHttpSupport http;
+
+    public Result<?> pageList(EmailSignatureDTO dto) {
+        return http.postForm(MailRemoteApiConstants.SIGNATURE_PAGE_LIST, dto);
+    }
+
+    public Result<?> list(EmailSignatureDTO dto) {
+        return http.postForm(MailRemoteApiConstants.SIGNATURE_LIST, dto);
+    }
+
+    public Result<?> getById(Long id) {
+        return http.postForm(MailRemoteApiConstants.SIGNATURE_GET_BY_ID, http.formOf("id", id));
+    }
+
+    public Result<?> getDefault(Long smtpPopId) {
+        return http.postForm(MailRemoteApiConstants.SIGNATURE_GET_DEFAULT, http.formOf("smtpPopId", smtpPopId));
+    }
+
+    public Result<?> save(EmailSignatureEntity entity) {
+        return http.postJson(MailRemoteApiConstants.SIGNATURE_SAVE, entity);
+    }
+
+    public Result<?> deleteIds(Long[] ids) {
+        return http.postForm(MailRemoteApiConstants.SIGNATURE_DELETE_IDS, http.formOf("ids", ids));
+    }
+
+    public Result<?> enabled(Long id, Boolean enable) {
+        return http.postForm(MailRemoteApiConstants.SIGNATURE_ENABLED, http.formOf("id", id, "enable", enable));
+    }
+
+    public Result<?> setDefault(Long id) {
+        return http.postForm(MailRemoteApiConstants.SIGNATURE_SET_DEFAULT, http.formOf("id", id));
+    }
+}

+ 41 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmMailTemplatesRemoteClient.java

@@ -0,0 +1,41 @@
+package com.storlead.mail.webclient.client;
+
+import com.storlead.framework.common.result.Result;
+import com.storlead.mail.pojo.dto.EmailTemplatesDTO;
+import com.storlead.mail.pojo.entity.EmailTemplatesEntity;
+import com.storlead.mail.webclient.constant.MailRemoteApiConstants;
+import com.storlead.mail.webclient.support.MailRemoteHttpSupport;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 邮件模板远程客户端,对齐 B 服务 {@code /email/templates/**}。
+ */
+@RequiredArgsConstructor
+public class CrmMailTemplatesRemoteClient {
+
+    private final MailRemoteHttpSupport http;
+
+    public Result<?> pageList(EmailTemplatesDTO dto) {
+        return http.postForm(MailRemoteApiConstants.TEMPLATES_PAGE_LIST, dto);
+    }
+
+    public Result<?> getById(Long id) {
+        return http.postForm(MailRemoteApiConstants.TEMPLATES_GET_BY_ID, http.formOf("id", id));
+    }
+
+    public Result<?> list(EmailTemplatesDTO dto) {
+        return http.postForm(MailRemoteApiConstants.TEMPLATES_LIST, dto);
+    }
+
+    public Result<?> save(EmailTemplatesEntity entity) {
+        return http.postForm(MailRemoteApiConstants.TEMPLATES_SAVE, entity);
+    }
+
+    public Result<?> deleteIds(Long[] ids) {
+        return http.postForm(MailRemoteApiConstants.TEMPLATES_DELETE_IDS, http.formOf("ids", ids));
+    }
+
+    public Result<?> enabled(Long id, Boolean enable) {
+        return http.postForm(MailRemoteApiConstants.TEMPLATES_ENABLED, http.formOf("id", id, "enable", enable));
+    }
+}

+ 37 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmMailboxAutoReplyRemoteClient.java

@@ -0,0 +1,37 @@
+package com.storlead.mail.webclient.client;
+
+import com.storlead.framework.common.result.Result;
+import com.storlead.mail.pojo.dto.EmailTemplatesDTO;
+import com.storlead.mail.pojo.entity.MailboxAutoReplySetEntity;
+import com.storlead.mail.webclient.constant.MailRemoteApiConstants;
+import com.storlead.mail.webclient.support.MailRemoteHttpSupport;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 邮箱自动回复远程客户端,对齐 B 服务 {@code /mail/mailbox/reply/**}。
+ */
+@RequiredArgsConstructor
+public class CrmMailboxAutoReplyRemoteClient {
+
+    private final MailRemoteHttpSupport http;
+
+    public Result<?> pageList(EmailTemplatesDTO dto) {
+        return http.postForm(MailRemoteApiConstants.AUTO_REPLY_PAGE_LIST, dto);
+    }
+
+    public Result<?> list(EmailTemplatesDTO dto) {
+        return http.postForm(MailRemoteApiConstants.AUTO_REPLY_LIST, dto);
+    }
+
+    public Result<?> save(MailboxAutoReplySetEntity entity) {
+        return http.postForm(MailRemoteApiConstants.AUTO_REPLY_SAVE, entity);
+    }
+
+    public Result<?> deleteIds(Long[] ids) {
+        return http.postForm(MailRemoteApiConstants.AUTO_REPLY_DELETE_IDS, http.formOf("ids", ids));
+    }
+
+    public Result<?> enabled(Long id, Boolean enable) {
+        return http.postForm(MailRemoteApiConstants.AUTO_REPLY_ENABLED, http.formOf("id", id, "enable", enable));
+    }
+}

+ 20 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmThumbnailRemoteClient.java

@@ -0,0 +1,20 @@
+package com.storlead.mail.webclient.client;
+
+import com.storlead.mail.webclient.constant.MailRemoteApiConstants;
+import com.storlead.mail.webclient.support.MailRemoteHttpSupport;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Collections;
+
+/**
+ * 缩略图预览远程客户端,对齐 B 服务 {@code /view/thumbnail}。
+ */
+@RequiredArgsConstructor
+public class CrmThumbnailRemoteClient {
+
+    private final MailRemoteHttpSupport http;
+
+    public byte[] thumbnail(String url) {
+        return http.getForBytes(MailRemoteApiConstants.VIEW_THUMBNAIL, Collections.singletonMap("url", url));
+    }
+}

+ 37 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/CrmUserEmailFolderRemoteClient.java

@@ -0,0 +1,37 @@
+package com.storlead.mail.webclient.client;
+
+import com.storlead.framework.common.result.Result;
+import com.storlead.mail.pojo.dto.EmailTemplatesDTO;
+import com.storlead.mail.pojo.entity.UserEmailFolderEntity;
+import com.storlead.mail.webclient.constant.MailRemoteApiConstants;
+import com.storlead.mail.webclient.support.MailRemoteHttpSupport;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 用户自定义邮件文件夹远程客户端,对齐 B 服务 {@code /user/email/folder/**}。
+ */
+@RequiredArgsConstructor
+public class CrmUserEmailFolderRemoteClient {
+
+    private final MailRemoteHttpSupport http;
+
+    public Result<?> pageList(EmailTemplatesDTO dto) {
+        return http.postForm(MailRemoteApiConstants.USER_FOLDER_PAGE_LIST, dto);
+    }
+
+    public Result<?> list() {
+        return http.postForm(MailRemoteApiConstants.USER_FOLDER_LIST, http.formOf());
+    }
+
+    public Result<?> save(UserEmailFolderEntity entity) {
+        return http.postForm(MailRemoteApiConstants.USER_FOLDER_SAVE, entity);
+    }
+
+    public Result<?> deleteIds(Long[] ids) {
+        return http.postForm(MailRemoteApiConstants.USER_FOLDER_DELETE_IDS, http.formOf("ids", ids));
+    }
+
+    public Result<?> enabled(Long id, Boolean enable) {
+        return http.postForm(MailRemoteApiConstants.USER_FOLDER_ENABLED, http.formOf("id", id, "enable", enable));
+    }
+}

+ 41 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/MktAccountRemoteClient.java

@@ -0,0 +1,41 @@
+package com.storlead.mail.webclient.client;
+
+import com.storlead.framework.common.dto.page.PageDTO;
+import com.storlead.framework.common.result.Result;
+import com.storlead.mail.pojo.entity.SmtpPopSettingsEntity;
+import com.storlead.mail.webclient.constant.MailRemoteApiConstants;
+import com.storlead.mail.webclient.support.MailRemoteHttpSupport;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 营销邮箱账户远程客户端,对齐 B 服务 {@code /mail/mkt/account/**}。
+ */
+@RequiredArgsConstructor
+public class MktAccountRemoteClient {
+
+    private final MailRemoteHttpSupport http;
+
+    public Result<?> pageList(PageDTO dto) {
+        return http.postForm(MailRemoteApiConstants.MKT_ACCOUNT_PAGE_LIST, dto);
+    }
+
+    public Result<?> list(Long orgId) {
+        return http.postForm(MailRemoteApiConstants.MKT_ACCOUNT_LIST, http.formOf("orgId", orgId));
+    }
+
+    public Result<?> save(SmtpPopSettingsEntity entity) {
+        return http.postJson(MailRemoteApiConstants.MKT_ACCOUNT_SAVE, entity);
+    }
+
+    public Result<?> delete(Long id) {
+        return http.postForm(MailRemoteApiConstants.MKT_ACCOUNT_DELETE, http.formOf("id", id));
+    }
+
+    public Result<?> defaultUse(Long id) {
+        return http.postForm(MailRemoteApiConstants.MKT_ACCOUNT_DEFAULT_USE, http.formOf("id", id));
+    }
+
+    public Result<?> testConnect(SmtpPopSettingsEntity mailSet) {
+        return http.postJson(MailRemoteApiConstants.MKT_ACCOUNT_TEST_CONNECT, mailSet);
+    }
+}

+ 71 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/MktMailRemoteClient.java

@@ -0,0 +1,71 @@
+package com.storlead.mail.webclient.client;
+
+import com.storlead.framework.common.result.Result;
+import com.storlead.mail.webclient.constant.MailRemoteApiConstants;
+import com.storlead.mail.webclient.dto.MktMailQueryRemoteDTO;
+import com.storlead.mail.webclient.dto.MktSendMailRemoteDTO;
+import com.storlead.mail.webclient.support.MailRemoteHttpSupport;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 营销邮件远程客户端,对齐 B 服务 {@code /mkt/**}。
+ */
+@RequiredArgsConstructor
+public class MktMailRemoteClient {
+
+    private final MailRemoteHttpSupport http;
+
+    public Result<?> cacheMailFile(List<MultipartFile> files, Long accountId) {
+        Map<String, String> extra = new HashMap<>();
+        if (accountId != null) {
+            extra.put("accountId", String.valueOf(accountId));
+        }
+        return http.postFiles(MailRemoteApiConstants.MKT_FILE_CACHE, "files", files, extra);
+    }
+
+    public Result<?> listAttachments(Long mailId) {
+        return http.postForm(MailRemoteApiConstants.MKT_FILE_LIST, http.formOf("mailId", mailId));
+    }
+
+    public byte[] downloadFormalFile(Long fileId) {
+        return http.postForBytes(MailRemoteApiConstants.MKT_FILE_DOWNLOAD, http.formOf("fileId", fileId));
+    }
+
+    public byte[] downloadTempFile(Long fileId) {
+        return http.postForBytes(MailRemoteApiConstants.MKT_FILE_DOWNLOAD_TEMP, http.formOf("fileId", fileId));
+    }
+
+    public Result<?> receiveMail(Long accountId) {
+        return http.postForm(MailRemoteApiConstants.MKT_RECEIVE_MAIL, http.formOf("accountId", accountId));
+    }
+
+    public Result<?> saveDraft(MktSendMailRemoteDTO dto) {
+        return http.postJson(MailRemoteApiConstants.MKT_SAVE_DRAFT, dto);
+    }
+
+    public Result<?> send(MktSendMailRemoteDTO dto) {
+        return http.postJson(MailRemoteApiConstants.MKT_SEND, dto);
+    }
+
+    public Result<?> pageList(MktMailQueryRemoteDTO query) {
+        return http.postJson(MailRemoteApiConstants.MKT_PAGE_LIST, query);
+    }
+
+    public Result<?> detail(Long id) {
+        return http.postForm(MailRemoteApiConstants.MKT_DETAIL, http.formOf("id", id));
+    }
+
+    public Result<?> openStats(String bizRef) {
+        return http.postForm(MailRemoteApiConstants.MKT_OPEN_STATS, http.formOf("bizRef", bizRef));
+    }
+
+    /** 打开追踪像素(通常由收件端浏览器直接访问 B 服务)。 */
+    public byte[] track(String token) {
+        return http.getForBytes(MailRemoteApiConstants.MKT_TRACK_PREFIX + token, null);
+    }
+}

+ 27 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/config/MailRemoteClientCondition.java

@@ -0,0 +1,27 @@
+package com.storlead.mail.webclient.config;
+
+import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
+import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
+import org.springframework.context.annotation.ConditionContext;
+import org.springframework.core.type.AnnotatedTypeMetadata;
+import org.springframework.util.StringUtils;
+
+/**
+ * 邮件远程客户端装配条件:enabled=true 且已配置 base-url 或 host。
+ */
+public class MailRemoteClientCondition extends SpringBootCondition {
+
+    @Override
+    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
+        String enabled = context.getEnvironment().getProperty("storlead.mail.remote.enabled", "true");
+        if (!"true".equalsIgnoreCase(enabled)) {
+            return ConditionOutcome.noMatch("storlead.mail.remote.enabled=false");
+        }
+        String baseUrl = context.getEnvironment().getProperty("storlead.mail.remote.base-url");
+        String host = context.getEnvironment().getProperty("storlead.mail.remote.host");
+        if (StringUtils.hasText(baseUrl) || StringUtils.hasText(host)) {
+            return ConditionOutcome.match("mail remote address configured");
+        }
+        return ConditionOutcome.noMatch("缺少 storlead.mail.remote.base-url 或 host");
+    }
+}

+ 150 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/config/MailRemoteProperties.java

@@ -0,0 +1,150 @@
+package com.storlead.mail.webclient.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * 独立邮件服务(B)远程调用配置。
+ * <p>
+ * 推荐按环境分别配置 host + context-path;也可直接给完整 base-url。
+ * <pre>
+ * storlead.mail.remote.enabled=true
+ * storlead.mail.remote.scheme=http
+ * storlead.mail.remote.host=127.0.0.1   # 或域名 mail.example.com
+ * storlead.mail.remote.port=18090        # 可省略(http=80 / https=443)
+ * storlead.mail.remote.context-path=/router/rest
+ * storlead.mail.remote.token-header=token
+ * # 或一次性写死:
+ * # storlead.mail.remote.base-url=http://127.0.0.1:18090/router/rest
+ * </pre>
+ */
+@Data
+@ConfigurationProperties(prefix = "storlead.mail.remote")
+public class MailRemoteProperties {
+
+    /**
+     * 是否启用远程客户端
+     */
+    private boolean enabled = true;
+
+    /**
+     * 完整根地址(优先)。配置后忽略 scheme/host/port/context-path 拼装。
+     * 例:http://127.0.0.1:18090/router/rest
+     */
+    private String baseUrl;
+
+    /**
+     * 协议:http / https
+     */
+    private String scheme = "http";
+
+    /**
+     * 邮件服务 IP 或域名(与 base-url 二选一)
+     */
+    private String host;
+
+    /**
+     * 端口;为空时 http 默认 80、https 默认 443(URL 中不显式带端口)
+     */
+    private Integer port;
+
+    /**
+     * 上下文路径,如 /router/rest;可为空
+     */
+    private String contextPath = "";
+
+    /**
+     * 连接超时(毫秒)
+     */
+    private int connectTimeoutMs = 5000;
+
+    /**
+     * 读超时(毫秒)
+     */
+    private int readTimeoutMs = 60000;
+
+    /**
+     * 写超时(毫秒)
+     */
+    private int writeTimeoutMs = 60000;
+
+    /**
+     * 鉴权请求头名,与 SaaS / B 服务 AuthFilter 一致(默认 token)
+     */
+    private String tokenHeader = "token";
+
+    /**
+     * 是否向 B 服务透传当前登录 token(默认 true)
+     */
+    private boolean forwardToken = true;
+
+    /**
+     * 是否透传 X-Tenant-Id(默认 true)
+     */
+    private boolean forwardTenant = true;
+
+    /**
+     * token 缺失时是否打 warn 日志(默认 true)
+     */
+    private boolean warnIfTokenMissing = true;
+
+    /**
+     * 是否已具备可用地址配置
+     */
+    public boolean isAddressConfigured() {
+        return StringUtils.hasText(baseUrl) || StringUtils.hasText(host);
+    }
+
+    /**
+     * 解析最终 WebClient baseUrl(无尾斜杠)。
+     */
+    public String resolveBaseUrl() {
+        if (StringUtils.hasText(baseUrl)) {
+            return trimTrailingSlash(baseUrl.trim());
+        }
+        Assert.hasText(host, "请配置 storlead.mail.remote.host 或 storlead.mail.remote.base-url");
+
+        String proto = StringUtils.hasText(scheme) ? scheme.trim().toLowerCase() : "http";
+        StringBuilder sb = new StringBuilder();
+        sb.append(proto).append("://").append(host.trim());
+
+        Integer effectivePort = port;
+        if (effectivePort != null
+                && !(("http".equals(proto) && effectivePort == 80)
+                || ("https".equals(proto) && effectivePort == 443))) {
+            sb.append(':').append(effectivePort);
+        }
+
+        String ctx = normalizeContextPath(contextPath);
+        if (StringUtils.hasText(ctx)) {
+            sb.append(ctx);
+        }
+        return sb.toString();
+    }
+
+    private static String normalizeContextPath(String path) {
+        if (!StringUtils.hasText(path)) {
+            return "";
+        }
+        String p = path.trim();
+        if (!p.startsWith("/")) {
+            p = "/" + p;
+        }
+        while (p.endsWith("/") && p.length() > 1) {
+            p = p.substring(0, p.length() - 1);
+        }
+        return "/".equals(p) ? "" : p;
+    }
+
+    private static String trimTrailingSlash(String url) {
+        if (!StringUtils.hasText(url)) {
+            return url;
+        }
+        while (url.endsWith("/")) {
+            url = url.substring(0, url.length() - 1);
+        }
+        return url;
+    }
+}

+ 148 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/config/MailWebClientAutoConfiguration.java

@@ -0,0 +1,148 @@
+package com.storlead.mail.webclient.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.storlead.mail.webclient.MailRemoteClientFacade;
+import com.storlead.mail.webclient.client.*;
+import com.storlead.mail.webclient.support.MailRemoteHttpSupport;
+import io.netty.channel.ChannelOption;
+import io.netty.handler.timeout.ReadTimeoutHandler;
+import io.netty.handler.timeout.WriteTimeoutHandler;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
+import org.springframework.web.reactive.function.client.ExchangeStrategies;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.netty.http.client.HttpClient;
+
+import java.util.concurrent.TimeUnit;
+
+@Configuration
+@EnableConfigurationProperties(MailRemoteProperties.class)
+@Conditional(MailRemoteClientCondition.class)
+public class MailWebClientAutoConfiguration {
+
+    @Bean
+    @ConditionalOnMissingBean(name = "mailWebClient")
+    public WebClient mailWebClient(MailRemoteProperties properties, WebClient.Builder builder) {
+        String baseUrl = properties.resolveBaseUrl();
+
+        HttpClient httpClient = HttpClient.create()
+                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, properties.getConnectTimeoutMs())
+                .doOnConnected(conn -> conn
+                        .addHandlerLast(new ReadTimeoutHandler(properties.getReadTimeoutMs(), TimeUnit.MILLISECONDS))
+                        .addHandlerLast(new WriteTimeoutHandler(properties.getWriteTimeoutMs(), TimeUnit.MILLISECONDS)));
+
+        ExchangeStrategies strategies = ExchangeStrategies.builder()
+                .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024))
+                .build();
+
+        return builder
+                .baseUrl(baseUrl)
+                .clientConnector(new ReactorClientHttpConnector(httpClient))
+                .exchangeStrategies(strategies)
+                .build();
+    }
+
+    @Bean
+    @ConditionalOnMissingBean
+    public MailRemoteHttpSupport mailRemoteHttpSupport(WebClient mailWebClient,
+                                                       MailRemoteProperties properties,
+                                                       ObjectMapper objectMapper) {
+        return new MailRemoteHttpSupport(mailWebClient, properties, objectMapper);
+    }
+
+    @Bean
+    @ConditionalOnMissingBean
+    public MktMailRemoteClient mktMailRemoteClient(MailRemoteHttpSupport http) {
+        return new MktMailRemoteClient(http);
+    }
+
+    @Bean
+    @ConditionalOnMissingBean
+    public MktAccountRemoteClient mktAccountRemoteClient(MailRemoteHttpSupport http) {
+        return new MktAccountRemoteClient(http);
+    }
+
+    @Bean
+    @ConditionalOnMissingBean
+    public CrmMailRemoteClient crmMailRemoteClient(MailRemoteHttpSupport http) {
+        return new CrmMailRemoteClient(http);
+    }
+
+    @Bean
+    @ConditionalOnMissingBean
+    public CrmAccountRemoteClient crmAccountRemoteClient(MailRemoteHttpSupport http) {
+        return new CrmAccountRemoteClient(http);
+    }
+
+    @Bean
+    @ConditionalOnMissingBean
+    public CrmMailFileRemoteClient crmMailFileRemoteClient(MailRemoteHttpSupport http) {
+        return new CrmMailFileRemoteClient(http);
+    }
+
+    @Bean
+    @ConditionalOnMissingBean
+    public CrmMailTemplatesRemoteClient crmMailTemplatesRemoteClient(MailRemoteHttpSupport http) {
+        return new CrmMailTemplatesRemoteClient(http);
+    }
+
+    @Bean
+    @ConditionalOnMissingBean
+    public CrmMailBlacklistRemoteClient crmMailBlacklistRemoteClient(MailRemoteHttpSupport http) {
+        return new CrmMailBlacklistRemoteClient(http);
+    }
+
+    @Bean
+    @ConditionalOnMissingBean
+    public CrmUserEmailFolderRemoteClient crmUserEmailFolderRemoteClient(MailRemoteHttpSupport http) {
+        return new CrmUserEmailFolderRemoteClient(http);
+    }
+
+    @Bean
+    @ConditionalOnMissingBean
+    public CrmEmailFolderRuleRemoteClient crmEmailFolderRuleRemoteClient(MailRemoteHttpSupport http) {
+        return new CrmEmailFolderRuleRemoteClient(http);
+    }
+
+    @Bean
+    @ConditionalOnMissingBean
+    public CrmMailboxAutoReplyRemoteClient crmMailboxAutoReplyRemoteClient(MailRemoteHttpSupport http) {
+        return new CrmMailboxAutoReplyRemoteClient(http);
+    }
+
+    @Bean
+    @ConditionalOnMissingBean
+    public CrmMailSignatureRemoteClient crmMailSignatureRemoteClient(MailRemoteHttpSupport http) {
+        return new CrmMailSignatureRemoteClient(http);
+    }
+
+    @Bean
+    @ConditionalOnMissingBean
+    public CrmThumbnailRemoteClient crmThumbnailRemoteClient(MailRemoteHttpSupport http) {
+        return new CrmThumbnailRemoteClient(http);
+    }
+
+    @Bean
+    @ConditionalOnMissingBean
+    public MailRemoteClientFacade mailRemoteClientFacade(MktMailRemoteClient mktMail,
+                                                         MktAccountRemoteClient mktAccount,
+                                                         CrmMailRemoteClient crmMail,
+                                                         CrmAccountRemoteClient crmAccount,
+                                                         CrmMailFileRemoteClient crmMailFile,
+                                                         CrmMailTemplatesRemoteClient crmMailTemplates,
+                                                         CrmMailBlacklistRemoteClient crmMailBlacklist,
+                                                         CrmUserEmailFolderRemoteClient crmUserEmailFolder,
+                                                         CrmEmailFolderRuleRemoteClient crmEmailFolderRule,
+                                                         CrmMailboxAutoReplyRemoteClient crmMailboxAutoReply,
+                                                         CrmMailSignatureRemoteClient crmMailSignature,
+                                                         CrmThumbnailRemoteClient crmThumbnail) {
+        return new MailRemoteClientFacade(
+                mktMail, mktAccount, crmMail, crmAccount, crmMailFile,
+                crmMailTemplates, crmMailBlacklist, crmUserEmailFolder,
+                crmEmailFolderRule, crmMailboxAutoReply, crmMailSignature, crmThumbnail);
+    }
+}

+ 315 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/constant/MailRemoteApiConstants.java

@@ -0,0 +1,315 @@
+package com.storlead.mail.webclient.constant;
+
+/**
+ * 独立邮件服务(B)远程接口路径常量。
+ * <p>
+ * 实际请求地址 = {@code storlead.mail.remote.base-url} + 本类常量。
+ * 分组与 B 服务 Controller 一致:MKT 营销 / CRM 邮件主流程 / 账户 / 附件 / 模板等。
+ */
+public final class MailRemoteApiConstants {
+
+    private MailRemoteApiConstants() {
+    }
+
+    // ========================= 营销邮件 /mkt =========================
+
+    /** 缓存营销邮件临时附件(scene=MKT),multipart:files + accountId */
+    public static final String MKT_FILE_CACHE = "/mkt/file/cache_mail_file";
+
+    /** 按邮件查询正式附件列表(mkt_mail_attachment),参数:mailId */
+    public static final String MKT_FILE_LIST = "/mkt/file/list";
+
+    /** 下载营销正式附件,参数:fileId,返回二进制流 */
+    public static final String MKT_FILE_DOWNLOAD = "/mkt/file/download";
+
+    /** 下载营销临时附件,参数:fileId,返回二进制流 */
+    public static final String MKT_FILE_DOWNLOAD_TEMP = "/mkt/file/download_temp";
+
+    /** 接收营销邮件(异步拉取 INBOX+Sent),参数:accountId */
+    public static final String MKT_RECEIVE_MAIL = "/mkt/receive_mail";
+
+    /** 保存营销邮件草稿,JSON:MktSendMailRemoteDTO */
+    public static final String MKT_SAVE_DRAFT = "/mkt/save_draft";
+
+    /** 发送营销邮件(默认同步;async=true 异步),JSON:MktSendMailRemoteDTO */
+    public static final String MKT_SEND = "/mkt/send";
+
+    /** 营销邮件投递分页,JSON:MktMailQueryRemoteDTO */
+    public static final String MKT_PAGE_LIST = "/mkt/page_list";
+
+    /** 营销邮件详情(含附件),参数:id */
+    public static final String MKT_DETAIL = "/mkt/detail";
+
+    /** 按业务单号汇总打开追踪(opened / open_count),参数:bizRef */
+    public static final String MKT_OPEN_STATS = "/mkt/open_stats";
+
+    /**
+     * 打开追踪像素(免登录,返回 1x1 GIF)。
+     * 完整路径:{@value #MKT_TRACK_PREFIX}{token}
+     */
+    public static final String MKT_TRACK_PREFIX = "/mkt/track/";
+
+    // ========================= 营销邮箱账户 /mail/mkt/account =========================
+
+    /** 营销邮箱配置分页,form:PageDTO */
+    public static final String MKT_ACCOUNT_PAGE_LIST = "/mkt/account/page_list";
+
+    /** 营销邮箱账户列表,参数:orgId(可选) */
+    public static final String MKT_ACCOUNT_LIST = "/mkt/account/list";
+
+    /** 保存/修改营销邮箱,JSON:SmtpPopSettingsEntity */
+    public static final String MKT_ACCOUNT_SAVE = "/mkt/account/save";
+
+    /** 删除营销邮箱,参数:id */
+    public static final String MKT_ACCOUNT_DELETE = "/mkt/account/delete";
+
+    /** 设置默认营销发信账户,参数:id */
+    public static final String MKT_ACCOUNT_DEFAULT_USE = "/mkt/account/default_use";
+
+    /** 测试营销邮箱连通性(以 SMTP 为主),JSON:SmtpPopSettingsEntity */
+    public static final String MKT_ACCOUNT_TEST_CONNECT = "/mkt/account/test_connect";
+
+    // ========================= CRM 邮件主流程 /mail =========================
+
+    /** 同步发送邮件,form:SendMailDTO */
+    public static final String CRM_SEND_MAIL = "/mail/send_mail";
+
+    /** 异步发送邮件,form:SendMailDTO */
+    public static final String CRM_ASYN_SEND_MAIL = "/mail/asyn_send_mail";
+
+    /** 保存草稿,form:SendMailDTO */
+    public static final String CRM_SAVE_DRAFT = "/mail/save_draft";
+
+    /** 收取邮件(当前用户默认账户) */
+    public static final String CRM_RECEIVE_MAIL = "/mail/receive_mail";
+
+    /** 批量下载邮件为 eml zip,form:MailDTO,返回二进制 */
+    public static final String CRM_DOWNLOAD_MAIL_EML_ZIP = "/mail/download_mail_eml_zip";
+
+    /** 下载单封邮件 eml,form:MailDTO,返回二进制 */
+    public static final String CRM_DOWNLOAD_MAIL_EML = "/mail/download_mail_eml";
+
+    /** 移动邮件到指定箱,form:MailMoveDTO */
+    public static final String CRM_MV_MAIL_BOX = "/mail/mv_mail_box";
+
+    /** 标记/移入垃圾箱,form:MailMoveDTO */
+    public static final String CRM_MARK_TRASH = "/mail/mark_trash";
+
+    /** 删除邮件(软删),参数:ids */
+    public static final String CRM_DELETE_IDS = "/mail/deleteIds";
+
+    /** 删除草稿,参数:ids */
+    public static final String CRM_DELETE_DRAFT_IDS = "/mail/deleteDraftIds";
+
+    /** 获取邮件详情,参数:mailId(路径拼写与 B 服务保持一致 detial) */
+    public static final String CRM_GET_MAIL_DETAIL = "/mail/get_mail_detial";
+
+    /** 邮件移入自定义文件夹,参数:mailIds、folderId */
+    public static final String CRM_MV_CUSTOM_FOLDER = "/mail/mv_custom_folder";
+
+    /** 从自定义文件夹移除邮件,参数:mailIds */
+    public static final String CRM_DEL_CUSTOM_FOLDER_MAIL = "/mail/del_custom_folder_mail";
+
+    /** 星标/取消星标,form:MailMoveDTO */
+    public static final String CRM_MARK_STAR = "/mail/mark_star";
+
+    /** 标记已读/未读,form:MailMoveDTO */
+    public static final String CRM_MARK_READ = "/mail/mark_read";
+
+    /** 标记回复关联原信,form:MailMoveDTO */
+    public static final String CRM_MARK_REPLY_MSG_ID = "/mail/mark_reply_msg_id";
+
+    /** 全部标为已读 */
+    public static final String CRM_MARK_ALL_READ = "/mail/mark_all_read";
+
+    /** 邮件附件列表,form:AttachmentDTO */
+    public static final String CRM_LIST_MAIL_ATTACHMENT = "/mail/list_mail_attachment";
+
+    /** 附件打包下载 zip,form:AttachmentDTO,返回二进制 */
+    public static final String CRM_DOWNLOAD_ZIP = "/mail/download_zip";
+
+    /** 获取正式附件下载地址,参数:attachmentId */
+    public static final String CRM_DOWNLOAD_URL = "/mail/download_url";
+
+    /** 获取草稿附件下载地址,参数:attachmentId */
+    public static final String CRM_DRAFT_DOWNLOAD_URL = "/mail/draft_download_url";
+
+    /** 下载草稿附件文件流,参数:attachmentId */
+    public static final String CRM_DRAFT_DOWNLOAD = "/mail/draft_download";
+
+    /** 下载正式附件文件流,参数:attachmentId */
+    public static final String CRM_DOWNLOAD = "/mail/download";
+
+    /** 根据邮箱账号查询端口/服务器配置,参数:mailAccount */
+    public static final String CRM_MAIL_PORT_CONFIG = "/mail/mail_port_config";
+
+    // ========================= CRM 邮箱账户 /smtp/pop/setting =========================
+
+    /** CRM 邮箱配置分页,form:PageDTO */
+    public static final String CRM_ACCOUNT_PAGE_LIST = "/smtp/pop/setting/pageList";
+
+    /** 保存/修改 CRM 邮箱,JSON:SmtpPopSettingsEntity */
+    public static final String CRM_ACCOUNT_SAVE = "/smtp/pop/setting/save";
+
+    /** 删除 CRM 邮箱,参数:id */
+    public static final String CRM_ACCOUNT_DELETE = "/smtp/pop/setting/delete";
+
+    /** 设置默认 CRM 邮箱,参数:id */
+    public static final String CRM_ACCOUNT_DEFAULT_USE = "/smtp/pop/setting/defaultUse";
+
+    /** 设置自动回复内容并开启,参数:id、replyContent、replyTitle、autoReplyStatus */
+    public static final String CRM_ACCOUNT_SET_REPLY_CONTENT = "/smtp/pop/setting/setReplyContent";
+
+    /** 开关自动回复,参数:id、autoReplyStatus */
+    public static final String CRM_ACCOUNT_SET_AUTO_REPLY_STATUS = "/smtp/pop/setting/setAutoReplyStatus";
+
+    /** 是否开启删除服务器邮件,参数:id、isEnabled */
+    public static final String CRM_ACCOUNT_IS_ENABLED_DELETE = "/smtp/pop/setting/isEnabledDelete";
+
+    /** 个人邮箱账户列表(含未读数) */
+    public static final String CRM_ACCOUNT_PERSONAL = "/smtp/pop/setting/personal_mail_account";
+
+    /** 测试 CRM 邮箱连通性,JSON:SmtpPopSettingsEntity */
+    public static final String CRM_ACCOUNT_TEST_CONNECT = "/smtp/pop/setting/test_connect";
+
+    // ========================= CRM 附件 /mail/file =========================
+
+    /** 缓存需上传的临时附件,multipart:files */
+    public static final String CRM_FILE_CACHE = "/mail/file/cache_mail_file";
+
+    /** 缓存正文图片,multipart:upload;响应为 Map(非 Result) */
+    public static final String CRM_FILE_CACHE_IMAGE = "/mail/file/cache_image_file";
+
+    /** 下载临时附件,参数:fileId,返回二进制 */
+    public static final String CRM_FILE_DOWNLOAD = "/mail/file/download_mail_file";
+
+    /**
+     * 获取缓存图片。完整路径:{@value #CRM_FILE_IMAGES_PREFIX}{imageName}
+     */
+    public static final String CRM_FILE_IMAGES_PREFIX = "/mail/file/images/";
+
+    // ========================= 邮件模板 /email/templates =========================
+
+    /** 邮件模板分页 */
+    public static final String TEMPLATES_PAGE_LIST = "/email/templates/page_list";
+
+    /** 邮件模板详情,参数:id */
+    public static final String TEMPLATES_GET_BY_ID = "/email/templates/getById";
+
+    /** 邮件模板列表 */
+    public static final String TEMPLATES_LIST = "/email/templates/list";
+
+    /** 保存邮件模板 */
+    public static final String TEMPLATES_SAVE = "/email/templates/save";
+
+    /** 删除邮件模板,参数:ids */
+    public static final String TEMPLATES_DELETE_IDS = "/email/templates/deleteIds";
+
+    /** 启用/禁用邮件模板,参数:id、enable */
+    public static final String TEMPLATES_ENABLED = "/email/templates/enabled";
+
+    // ========================= 黑名单 /email/blacklist =========================
+
+    /** 黑名单分页 */
+    public static final String BLACKLIST_PAGE_LIST = "/email/blacklist/page_list";
+
+    /** 黑名单列表 */
+    public static final String BLACKLIST_LIST = "/email/blacklist/list";
+
+    /** 保存黑名单 */
+    public static final String BLACKLIST_SAVE = "/email/blacklist/save";
+
+    /** 删除黑名单,参数:ids */
+    public static final String BLACKLIST_DELETE_IDS = "/email/blacklist/deleteIds";
+
+    /** 启用/禁用黑名单,参数:id、enable */
+    public static final String BLACKLIST_ENABLED = "/email/blacklist/enabled";
+
+    // ========================= 自定义文件夹 /user/email/folder =========================
+
+    /** 用户文件夹分页 */
+    public static final String USER_FOLDER_PAGE_LIST = "/user/email/folder/page_list";
+
+    /** 用户文件夹列表(启用) */
+    public static final String USER_FOLDER_LIST = "/user/email/folder/list";
+
+    /** 保存用户文件夹 */
+    public static final String USER_FOLDER_SAVE = "/user/email/folder/save";
+
+    /** 删除用户文件夹,参数:ids */
+    public static final String USER_FOLDER_DELETE_IDS = "/user/email/folder/deleteIds";
+
+    /** 启用/禁用用户文件夹,参数:id、enable */
+    public static final String USER_FOLDER_ENABLED = "/user/email/folder/enabled";
+
+    // ========================= 入站规则 /email/folder/rule =========================
+
+    /** 获取入站规则枚举说明 */
+    public static final String FOLDER_RULE_GET = "/email/folder/rule/get_folder_rule";
+
+    /** 入站规则分页 */
+    public static final String FOLDER_RULE_PAGE_LIST = "/email/folder/rule/page_list";
+
+    /** 入站规则列表 */
+    public static final String FOLDER_RULE_LIST = "/email/folder/rule/list";
+
+    /** 保存单条入站规则 */
+    public static final String FOLDER_RULE_SAVE = "/email/folder/rule/save";
+
+    /** 批量覆盖保存入站规则,JSON:FolderRuleDTO */
+    public static final String FOLDER_RULE_SAVE_BATCH = "/email/folder/rule/saveBatch";
+
+    /** 删除入站规则,参数:ids */
+    public static final String FOLDER_RULE_DELETE_IDS = "/email/folder/rule/deleteIds";
+
+    /** 启用/禁用入站规则,参数:id、enable */
+    public static final String FOLDER_RULE_ENABLED = "/email/folder/rule/enabled";
+
+    // ========================= 自动回复 /mail/mailbox/reply =========================
+
+    /** 自动回复配置分页 */
+    public static final String AUTO_REPLY_PAGE_LIST = "/mail/mailbox/reply/page_list";
+
+    /** 自动回复配置列表 */
+    public static final String AUTO_REPLY_LIST = "/mail/mailbox/reply/list";
+
+    /** 保存自动回复配置 */
+    public static final String AUTO_REPLY_SAVE = "/mail/mailbox/reply/save";
+
+    /** 删除自动回复配置,参数:ids */
+    public static final String AUTO_REPLY_DELETE_IDS = "/mail/mailbox/reply/deleteIds";
+
+    /** 启用/禁用自动回复配置,参数:id、enable */
+    public static final String AUTO_REPLY_ENABLED = "/mail/mailbox/reply/enabled";
+
+    // ========================= 其它 =========================
+
+    /** 缩略图预览,query:url,返回图片二进制 */
+    public static final String VIEW_THUMBNAIL = "/view/thumbnail";
+
+    // ========================= 邮件签名 /email/signature =========================
+
+    /** 签名分页(注意 B 路径拼写 pageLlist) */
+    public static final String SIGNATURE_PAGE_LIST = "/email/signature/pageLlist";
+
+    /** 签名列表 */
+    public static final String SIGNATURE_LIST = "/email/signature/list";
+
+    /** 签名详情,参数:id */
+    public static final String SIGNATURE_GET_BY_ID = "/email/signature/getById";
+
+    /** 当前用户默认签名,参数:smtpPopId(可选) */
+    public static final String SIGNATURE_GET_DEFAULT = "/email/signature/getDefault";
+
+    /** 保存签名,JSON:EmailSignatureEntity */
+    public static final String SIGNATURE_SAVE = "/email/signature/save";
+
+    /** 删除签名,参数:ids */
+    public static final String SIGNATURE_DELETE_IDS = "/email/signature/deleteIds";
+
+    /** 启用/禁用签名,参数:id、enable */
+    public static final String SIGNATURE_ENABLED = "/email/signature/enabled";
+
+    /** 设为默认签名,参数:id */
+    public static final String SIGNATURE_SET_DEFAULT = "/email/signature/setDefault";
+}

+ 29 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/dto/MktMailQueryRemoteDTO.java

@@ -0,0 +1,29 @@
+package com.storlead.mail.webclient.dto;
+
+import com.storlead.framework.common.dto.page.PageDTO;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 营销邮件分页查询(对齐 B 服务 MktMailQueryDTO)。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class MktMailQueryRemoteDTO extends PageDTO {
+
+    @ApiModelProperty("发信账户 id")
+    private Long accountId;
+
+    @ApiModelProperty("业务单号")
+    private String bizRef;
+
+    @ApiModelProperty("主题关键字")
+    private String subject;
+
+    @ApiModelProperty("状态")
+    private Integer status;
+
+    @ApiModelProperty("收件人关键字")
+    private String recipient;
+}

+ 63 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/dto/MktSendMailRemoteDTO.java

@@ -0,0 +1,63 @@
+package com.storlead.mail.webclient.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 营销发信/草稿 DTO(对齐 B 服务 MktSendMailDTO)。
+ */
+@Data
+public class MktSendMailRemoteDTO {
+
+    @ApiModelProperty("草稿/邮件主键(编辑时传)")
+    private Long id;
+
+    @ApiModelProperty("发信账户 id")
+    private Long accountId;
+
+    @ApiModelProperty("业务单号,用于打开追踪汇总")
+    private String bizRef;
+
+    @ApiModelProperty("收件人")
+    private List<String> recipientls;
+
+    @ApiModelProperty("抄送")
+    private List<String> recipientCcls;
+
+    @ApiModelProperty("密送")
+    private List<String> recipientBccls;
+
+    @ApiModelProperty("主题")
+    private String subject;
+
+    @ApiModelProperty("正文 HTML/文本")
+    private String content;
+
+    @ApiModelProperty("临时附件 id 列表")
+    private List<Long> fileIds;
+
+    @ApiModelProperty("正式附件/旧附件 id 列表")
+    private List<Long> oldFileIds;
+
+    @ApiModelProperty("是否追踪打开:0否 1是")
+    private Integer isTrack;
+
+    @ApiModelProperty("是否回执")
+    private Integer isReceipt;
+
+    @ApiModelProperty("true=异步投递")
+    private Boolean async;
+
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm")
+    @ApiModelProperty("定时发送时间")
+    private LocalDateTime delaySendTime;
+
+    @ApiModelProperty("回复原信 id")
+    private Long replyMsgId;
+}

+ 32 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/support/MailRemoteException.java

@@ -0,0 +1,32 @@
+package com.storlead.mail.webclient.support;
+
+/**
+ * 调用独立邮件服务失败。
+ */
+public class MailRemoteException extends RuntimeException {
+
+    private final Integer httpStatus;
+    private final Integer bizCode;
+
+    public MailRemoteException(String message) {
+        this(message, null, null, null);
+    }
+
+    public MailRemoteException(String message, Throwable cause) {
+        this(message, null, null, cause);
+    }
+
+    public MailRemoteException(String message, Integer httpStatus, Integer bizCode, Throwable cause) {
+        super(message, cause);
+        this.httpStatus = httpStatus;
+        this.bizCode = bizCode;
+    }
+
+    public Integer getHttpStatus() {
+        return httpStatus;
+    }
+
+    public Integer getBizCode() {
+        return bizCode;
+    }
+}

+ 381 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/support/MailRemoteHttpSupport.java

@@ -0,0 +1,381 @@
+package com.storlead.mail.webclient.support;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.storlead.framework.common.constant.DefContants;
+import com.storlead.framework.common.result.Result;
+import com.storlead.framework.core.tenant.TenantConstants;
+import com.storlead.framework.core.tenant.TenantContext;
+import com.storlead.framework.util.LoginUserUtil;
+import com.storlead.mail.webclient.config.MailRemoteProperties;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.Resource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.client.MultipartBodyBuilder;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.reactive.function.BodyInserters;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.WebClientResponseException;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 邮件远程 HTTP 通用能力:鉴权头透传、JSON / form / multipart / 二进制下载。
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class MailRemoteHttpSupport {
+
+    private final WebClient mailWebClient;
+    private final MailRemoteProperties properties;
+    private final ObjectMapper objectMapper;
+
+    private Duration readTimeout() {
+        return Duration.ofMillis(Math.max(properties.getReadTimeoutMs(), 1000));
+    }
+
+    public HttpHeaders authHeaders() {
+        HttpHeaders headers = new HttpHeaders();
+        if (properties.isForwardToken()) {
+            String tokenHeader = StringUtils.hasText(properties.getTokenHeader())
+                    ? properties.getTokenHeader() : DefContants.ACCESS_TOKEN;
+            String token = resolveToken(tokenHeader);
+            if (StringUtils.hasText(token)) {
+                headers.set(tokenHeader, token);
+            } else if (properties.isWarnIfTokenMissing()) {
+                log.warn("调用邮件服务未携带 token(header={}),B 服务可能返回未登录", tokenHeader);
+            }
+        }
+        if (properties.isForwardTenant()) {
+            Long tenantId = TenantContext.getTenantId();
+            if (tenantId != null) {
+                headers.set(TenantConstants.HTTP_HEADER_TENANT_ID, String.valueOf(tenantId));
+            }
+        }
+        return headers;
+    }
+
+    /**
+     * 解析当前请求 token:优先 LoginUserUtil,其次当前 HttpServletRequest 头。
+     */
+    private String resolveToken(String tokenHeader) {
+        String token = LoginUserUtil.getLoginToken();
+        if (StringUtils.hasText(token)) {
+            return token;
+        }
+        try {
+            org.springframework.web.context.request.RequestAttributes attrs =
+                    org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();
+            if (attrs instanceof org.springframework.web.context.request.ServletRequestAttributes) {
+                javax.servlet.http.HttpServletRequest request =
+                        ((org.springframework.web.context.request.ServletRequestAttributes) attrs).getRequest();
+                token = request.getHeader(tokenHeader);
+                if (!StringUtils.hasText(token) && !DefContants.ACCESS_TOKEN.equals(tokenHeader)) {
+                    token = request.getHeader(DefContants.ACCESS_TOKEN);
+                }
+                if (!StringUtils.hasText(token)) {
+                    Object attr = request.getAttribute("token");
+                    if (attr != null) {
+                        token = String.valueOf(attr);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.debug("从 Request 解析 token 失败: {}", e.getMessage());
+        }
+        return token;
+    }
+
+    public Result<?> postJson(String path, Object body) {
+        try {
+            return mailWebClient.post()
+                    .uri(ub -> ub.path(path).build())
+                    .headers(h -> h.addAll(authHeaders()))
+                    .contentType(MediaType.APPLICATION_JSON)
+                    .bodyValue(body == null ? new LinkedHashMap<>() : body)
+                    .retrieve()
+                    .bodyToMono(new ParameterizedTypeReference<Result<Object>>() {})
+                    .timeout(readTimeout())
+                    .block();
+        } catch (WebClientResponseException e) {
+            throw wrapHttp(e, path);
+        } catch (Exception e) {
+            throw new MailRemoteException("调用邮件服务失败: POST " + path, e);
+        }
+    }
+
+    public <T> Result<T> postJson(String path, Object body, Class<T> dataType) {
+        Result<?> raw = postJson(path, body);
+        return convertResult(raw, dataType);
+    }
+
+    public Result<?> postForm(String path, Object formObject) {
+        return postForm(path, toForm(formObject));
+    }
+
+    public Result<?> postForm(String path, MultiValueMap<String, String> form) {
+        try {
+            return mailWebClient.post()
+                    .uri(ub -> ub.path(path).build())
+                    .headers(h -> h.addAll(authHeaders()))
+                    .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+                    .body(BodyInserters.fromFormData(form == null ? new LinkedMultiValueMap<>() : form))
+                    .retrieve()
+                    .bodyToMono(new ParameterizedTypeReference<Result<Object>>() {})
+                    .timeout(readTimeout())
+                    .block();
+        } catch (WebClientResponseException e) {
+            throw wrapHttp(e, path);
+        } catch (Exception e) {
+            throw new MailRemoteException("调用邮件服务失败: POST " + path, e);
+        }
+    }
+
+    public Result<?> postMultipart(String path, MultiValueMap<String, ?> multipart) {
+        try {
+            return mailWebClient.post()
+                    .uri(ub -> ub.path(path).build())
+                    .headers(h -> h.addAll(authHeaders()))
+                    .contentType(MediaType.MULTIPART_FORM_DATA)
+                    .body(BodyInserters.fromMultipartData(multipart))
+                    .retrieve()
+                    .bodyToMono(new ParameterizedTypeReference<Result<Object>>() {})
+                    .timeout(readTimeout())
+                    .block();
+        } catch (WebClientResponseException e) {
+            throw wrapHttp(e, path);
+        } catch (Exception e) {
+            throw new MailRemoteException("调用邮件服务失败: POST " + path, e);
+        }
+    }
+
+    public Result<?> postFiles(String path, String fileField, List<MultipartFile> files, Map<String, String> extraFields) {
+        MultipartBodyBuilder builder = new MultipartBodyBuilder();
+        if (extraFields != null) {
+            extraFields.forEach(builder::part);
+        }
+        if (files != null) {
+            for (MultipartFile file : files) {
+                if (file == null || file.isEmpty()) {
+                    continue;
+                }
+                try {
+                    Resource resource = new ByteArrayResource(file.getBytes()) {
+                        @Override
+                        public String getFilename() {
+                            return file.getOriginalFilename();
+                        }
+                    };
+                    builder.part(fileField, resource)
+                            .filename(file.getOriginalFilename() == null ? "file" : file.getOriginalFilename())
+                            .contentType(MediaType.parseMediaType(
+                                    file.getContentType() == null ? MediaType.APPLICATION_OCTET_STREAM_VALUE : file.getContentType()));
+                } catch (Exception e) {
+                    throw new MailRemoteException("读取上传文件失败", e);
+                }
+            }
+        }
+        return postMultipart(path, builder.build());
+    }
+
+    public Result<?> postFile(String path, String fileField, MultipartFile file, Map<String, String> extraFields) {
+        return postFiles(path, fileField, file == null ? null : List.of(file), extraFields);
+    }
+
+    /**
+     * 上传单文件并按 Map 解析响应(B 服务部分接口不包 Result,如 cache_image_file)。
+     */
+    @SuppressWarnings("unchecked")
+    public Map<String, Object> postFileForMap(String path, String fileField, MultipartFile file,
+                                              Map<String, String> extraFields) {
+        MultipartBodyBuilder builder = new MultipartBodyBuilder();
+        if (extraFields != null) {
+            extraFields.forEach(builder::part);
+        }
+        if (file != null && !file.isEmpty()) {
+            try {
+                Resource resource = new ByteArrayResource(file.getBytes()) {
+                    @Override
+                    public String getFilename() {
+                        return file.getOriginalFilename();
+                    }
+                };
+                builder.part(fileField, resource)
+                        .filename(file.getOriginalFilename() == null ? "file" : file.getOriginalFilename())
+                        .contentType(MediaType.parseMediaType(
+                                file.getContentType() == null ? MediaType.APPLICATION_OCTET_STREAM_VALUE : file.getContentType()));
+            } catch (Exception e) {
+                throw new MailRemoteException("读取上传文件失败", e);
+            }
+        }
+        try {
+            Map<String, Object> body = mailWebClient.post()
+                    .uri(ub -> ub.path(path).build())
+                    .headers(h -> h.addAll(authHeaders()))
+                    .contentType(MediaType.MULTIPART_FORM_DATA)
+                    .body(BodyInserters.fromMultipartData(builder.build()))
+                    .retrieve()
+                    .bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
+                    .timeout(readTimeout())
+                    .block();
+            return body == null ? new LinkedHashMap<>() : body;
+        } catch (WebClientResponseException e) {
+            throw wrapHttp(e, path);
+        } catch (Exception e) {
+            throw new MailRemoteException("调用邮件服务失败: POST " + path, e);
+        }
+    }
+
+    public byte[] postForBytes(String path, MultiValueMap<String, String> form) {
+        try {
+            return mailWebClient.post()
+                    .uri(ub -> ub.path(path).build())
+                    .headers(h -> h.addAll(authHeaders()))
+                    .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+                    .body(BodyInserters.fromFormData(form == null ? new LinkedMultiValueMap<>() : form))
+                    .retrieve()
+                    .bodyToMono(byte[].class)
+                    .timeout(readTimeout())
+                    .block();
+        } catch (WebClientResponseException e) {
+            throw wrapHttp(e, path);
+        } catch (Exception e) {
+            throw new MailRemoteException("调用邮件服务下载失败: POST " + path, e);
+        }
+    }
+
+    public byte[] getForBytes(String path, Map<String, ?> query) {
+        try {
+            return mailWebClient.get()
+                    .uri(uriBuilder -> {
+                        uriBuilder.path(path);
+                        if (query != null) {
+                            query.forEach((k, v) -> {
+                                if (v != null) {
+                                    uriBuilder.queryParam(k, v);
+                                }
+                            });
+                        }
+                        return uriBuilder.build();
+                    })
+                    .headers(h -> h.addAll(authHeaders()))
+                    .retrieve()
+                    .bodyToMono(byte[].class)
+                    .timeout(readTimeout())
+                    .onErrorResume(WebClientResponseException.NotFound.class, e -> Mono.empty())
+                    .block();
+        } catch (WebClientResponseException e) {
+            throw wrapHttp(e, path);
+        } catch (Exception e) {
+            throw new MailRemoteException("调用邮件服务下载失败: GET " + path, e);
+        }
+    }
+
+    public MultiValueMap<String, String> formOf(Object... keyValues) {
+        MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
+        if (keyValues == null) {
+            return form;
+        }
+        if (keyValues.length % 2 != 0) {
+            throw new IllegalArgumentException("formOf 参数必须成对出现");
+        }
+        for (int i = 0; i < keyValues.length; i += 2) {
+            Object key = keyValues[i];
+            Object val = keyValues[i + 1];
+            if (key == null || val == null) {
+                continue;
+            }
+            addFormValue(form, String.valueOf(key), val);
+        }
+        return form;
+    }
+
+    @SuppressWarnings("unchecked")
+    public MultiValueMap<String, String> toForm(Object formObject) {
+        MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
+        if (formObject == null) {
+            return form;
+        }
+        if (formObject instanceof MultiValueMap) {
+            ((MultiValueMap<String, ?>) formObject).forEach((k, values) -> {
+                if (values == null) {
+                    return;
+                }
+                for (Object v : values) {
+                    addFormValue(form, k, v);
+                }
+            });
+            return form;
+        }
+        Map<String, Object> map = objectMapper.convertValue(formObject, new TypeReference<Map<String, Object>>() {});
+        map.forEach((k, v) -> addFormValue(form, k, v));
+        return form;
+    }
+
+    private void addFormValue(MultiValueMap<String, String> form, String key, Object value) {
+        if (!StringUtils.hasText(key) || value == null) {
+            return;
+        }
+        if (value instanceof Collection) {
+            for (Object item : (Collection<?>) value) {
+                if (item != null) {
+                    form.add(key, String.valueOf(item));
+                }
+            }
+            return;
+        }
+        if (value.getClass().isArray()) {
+            int len = java.lang.reflect.Array.getLength(value);
+            for (int i = 0; i < len; i++) {
+                Object item = java.lang.reflect.Array.get(value, i);
+                if (item != null) {
+                    form.add(key, String.valueOf(item));
+                }
+            }
+            return;
+        }
+        form.add(key, String.valueOf(value));
+    }
+
+    @SuppressWarnings("unchecked")
+    private <T> Result<T> convertResult(Result<?> raw, Class<T> dataType) {
+        if (raw == null) {
+            return null;
+        }
+        Result<T> typed = new Result<>();
+        typed.setSuccess(raw.isSuccess());
+        typed.setMessage(raw.getMessage());
+        typed.setCode(raw.getCode());
+        typed.setTimestamp(raw.getTimestamp());
+        Object data = raw.getResult();
+        if (data == null || dataType == null || dataType == Object.class || dataType.isInstance(data)) {
+            typed.setResult((T) data);
+            return typed;
+        }
+        JavaType javaType = objectMapper.getTypeFactory().constructType(dataType);
+        typed.setResult(objectMapper.convertValue(data, javaType));
+        return typed;
+    }
+
+    private MailRemoteException wrapHttp(WebClientResponseException e, String path) {
+        log.warn("邮件服务 HTTP 错误 path={} status={} body={}", path, e.getRawStatusCode(), e.getResponseBodyAsString());
+        return new MailRemoteException(
+                "邮件服务返回错误: HTTP " + e.getRawStatusCode() + " " + path,
+                e.getRawStatusCode(),
+                null,
+                e);
+    }
+}

+ 53 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/support/MailRemoteResponseWriter.java

@@ -0,0 +1,53 @@
+package com.storlead.mail.webclient.support;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.util.StringUtils;
+
+import javax.servlet.http.HttpServletResponse;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 将 B 服务返回的二进制写回当前 SaaS 接口响应,保持原下载接口形态。
+ */
+@Slf4j
+public final class MailRemoteResponseWriter {
+
+    private MailRemoteResponseWriter() {
+    }
+
+    public static void write(HttpServletResponse response, byte[] data) {
+        write(response, data, null);
+    }
+
+    public static void write(HttpServletResponse response, byte[] data, String filename) {
+        if (response == null || data == null) {
+            return;
+        }
+        try {
+            response.reset();
+            response.setContentType("application/octet-stream;charset=utf-8");
+            response.addHeader("Access-Control-Allow-Origin", "*");
+            if (StringUtils.hasText(filename)) {
+                String encoded = URLEncoder.encode(filename.replace(" ", ""), StandardCharsets.UTF_8.name());
+                response.addHeader("Content-Disposition",
+                        "attachment;filename=" + encoded + ";filename*=utf-8''" + encoded);
+            }
+            response.setContentLength(data.length);
+            response.getOutputStream().write(data);
+            response.getOutputStream().flush();
+        } catch (Exception e) {
+            log.error("write remote mail bytes error", e);
+        }
+    }
+
+    public static org.springframework.http.ResponseEntity<byte[]> image(byte[] data) {
+        if (data == null || data.length == 0) {
+            return org.springframework.http.ResponseEntity.notFound().build();
+        }
+        return org.springframework.http.ResponseEntity.ok()
+                .contentType(MediaType.IMAGE_JPEG)
+                .body(data);
+    }
+}

+ 2 - 0
java/storlead-mail/storlead-mail-api/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+com.storlead.mail.webclient.config.MailWebClientAutoConfiguration

+ 2 - 2
java/storlead-sasa/storlead-sales/pom.xml

@@ -15,8 +15,8 @@
     <name>${project.artifactId}</name>
 
     <properties>
-        <maven.compiler.source>17</maven.compiler.source>
-        <maven.compiler.target>17</maven.compiler.target>
+        <maven.compiler.source>11</maven.compiler.source>
+        <maven.compiler.target>11</maven.compiler.target>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>
 

+ 37 - 707
java/storlead-sasa/storlead-sales/src/main/java/com/storlead/sales/controller/mail/MailApiController.java

@@ -12,7 +12,8 @@ import com.storlead.framework.common.util.encryptor.AccessKeyEncryptor;
 import com.storlead.framework.util.LoginUserUtil;
 import com.storlead.framework.common.result.Result;
 import com.storlead.framework.mybatis.entity.SysBaseField;
-import com.storlead.mail.controller.EmailServiceChecker;
+import com.storlead.mail.webclient.client.CrmMailRemoteClient;
+import com.storlead.mail.webclient.support.MailRemoteResponseWriter;
 import com.storlead.mail.pojo.*;
 import com.storlead.mail.pojo.entity.*;
 import com.storlead.mail.pojo.vo.*;
@@ -109,6 +110,9 @@ public class MailApiController {
     @Resource
     private Environment environment;
 
+    @Resource
+    private CrmMailRemoteClient crmMailRemoteClient;
+
     @PostMapping(value = "/page_list")
     @ApiOperation(value = "获取邮件")
     @ApiResponses({
@@ -272,21 +276,7 @@ public class MailApiController {
             @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
     })
     public Result<?> sendMail(SendMailDTO dto) {
-
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        SmtpPopSettingsEntity smtpPop = smtpPopSettingsService.getDefaultSmtpPop(currentUserId);
-        AccessKeyEncryptor accessKeyEncryptor = AccessKeyEncryptor.getAccessKeyEncryptor("");
-        String pass = accessKeyEncryptor.decrypt(smtpPop.getEmailPassword());
-        smtpPop.setEmailPassword(pass);
-        if (Objects.isNull(currentUserId) && Objects.nonNull(smtpPop)) {
-            return Result.result(null);
-        }
-        SendStatus result = emailsService.sendEmail(dto, smtpPop);
-        if (result.code == SendStatus.SUCCESS.code || result.code == SendStatus.PARTIAL_SUCCESS.code) {
-            return Result.ok();
-        } else {
-            return Result.error(result.getDesc());
-        }
+        return crmMailRemoteClient.sendMail(dto);
     }
 
     @PostMapping(value = "/asyn_send_mail")
@@ -295,17 +285,7 @@ public class MailApiController {
             @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
     })
     public Result<?> asynSendMail(SendMailDTO dto) {
-
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        SmtpPopSettingsEntity smtpPop = smtpPopSettingsService.getDefaultSmtpPop(currentUserId);
-        AccessKeyEncryptor accessKeyEncryptor = AccessKeyEncryptor.getAccessKeyEncryptor("");
-        String pass = accessKeyEncryptor.decrypt(smtpPop.getEmailPassword());
-        smtpPop.setEmailPassword(pass);
-        if (Objects.isNull(currentUserId) && Objects.nonNull(smtpPop)) {
-            return Result.result(null);
-        }
-        emailsService.sendEmail(dto, smtpPop);
-        return Result.ok();
+        return crmMailRemoteClient.asynSendMail(dto);
     }
 
     @PostMapping(value = "/save_draft")
@@ -314,59 +294,7 @@ public class MailApiController {
             @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
     })
     public Result<?> saveDraft(SendMailDTO dto) {
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        SmtpPopSettingsEntity smtpPop = smtpPopSettingsService.getDefaultSmtpPop(currentUserId);
-        if (Objects.isNull(currentUserId) || Objects.isNull(smtpPop)) {
-            return Result.error("请先设置邮箱账号!");
-        }
-        EmailsEntity entity = new EmailsEntity();
-        if (!CollectionUtils.isEmpty(dto.getRecipientls())) {
-            entity.setRecipient(org.apache.commons.lang3.StringUtils.join(dto.getRecipientls(), ","));
-        }
-        if (!CollectionUtils.isEmpty(dto.getRecipientCcls())) {
-            entity.setRecipientCc(org.apache.commons.lang3.StringUtils.join(dto.getRecipientCcls(), ","));
-        }
-        if (!CollectionUtils.isEmpty(dto.getRecipientBccls())) {
-            entity.setRecipientBcc(org.apache.commons.lang3.StringUtils.join(dto.getRecipientBccls(), ","));
-        }
-        if (Objects.nonNull(dto.getId())) {
-            entity.setId(dto.getId());
-        }
-
-        entity.setSubject(dto.getSubject());
-        entity.setContent(dto.getContent());
-        entity.setReplyMsgId(dto.getReplyMsgId());
-        entity.setOwnerBy(currentUserId);
-        entity.setSmtpPopId(smtpPop.getId());
-        entity.setFolder(EmailBoxEnum.DRAFT.code);
-        entity.setEmailSize(Long.valueOf(entity.getContent().length()));
-
-        emailsService.saveOrUpdate(entity);
-        if (!CollectionUtils.isEmpty(dto.getFileIds())) {
-            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())) {
-            LambdaUpdateWrapper<MailAttachmentEntity> fileWrapper = new LambdaUpdateWrapper<>();
-            fileWrapper.in(MailAttachmentEntity::getId, dto.getOldfileIds());
-            List<MailAttachmentEntity> mailAttachments = mailAttachmentService.list(fileWrapper);
-            if (!CollectionUtils.isEmpty(mailAttachments)) {
-                List<MailTempAttachmentEntity> files = new ArrayList<>();
-                for(MailAttachmentEntity attachment : mailAttachments) {
-                    MailTempAttachmentEntity tempAttachment = new MailTempAttachmentEntity();
-                    BeanUtils.copyProperties(attachment,tempAttachment);
-                    tempAttachment.setId(entity.getId());
-                    tempAttachment.setOwnerBy(entity.getOwnerBy());
-                    files.add(tempAttachment);
-                }
-                if (!CollectionUtils.isEmpty(files)) {
-                    tempAttachmentService.saveBatch(files);
-                }
-            }
-        }
-        return Result.ok();
+        return crmMailRemoteClient.saveDraft(dto);
     }
 
     @PostMapping(value = "/receive_mail")
@@ -375,16 +303,7 @@ public class MailApiController {
             @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
     })
     public Result<?> receiveMail() {
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        SmtpPopSettingsEntity smtpPop = smtpPopSettingsService.getDefaultSmtpPop(currentUserId);
-        if (Objects.isNull(currentUserId) || Objects.isNull(smtpPop)) {
-            return Result.error("请先设置邮箱账号!");
-        }
-        AccessKeyEncryptor accessKeyEncryptor = AccessKeyEncryptor.getAccessKeyEncryptor("");
-        String pass = accessKeyEncryptor.decrypt(smtpPop.getEmailPassword());
-        smtpPop.setEmailPassword(pass);
-        emailsService.receiveEmails(smtpPop);
-        return Result.ok();
+        return crmMailRemoteClient.receiveMail();
     }
 
     @PostMapping(value = "/download_mail_eml_zip")
@@ -392,130 +311,17 @@ public class MailApiController {
     @ApiResponses({
             @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
     })
-    public void downloadMailEmlZip(MailDTO dto, HttpServletResponse response) throws MessagingException {
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-
-        if (CollectionUtils.isEmpty(dto.getMailIds())) {
-            return;
-        }
-        List<EmailsEntity> entities = emailsService.list(new LambdaQueryWrapper<EmailsEntity>().in(EmailsEntity::getId, dto.getMailIds()));
-        if (CollectionUtils.isEmpty(entities)) {
-            return;
-        }
-        String zipFilePath = mailFileProperties.getPath().getPath() + "temp_eml_files";
-        File tempDir = new File(zipFilePath + File.separator + "temp");
-//        if (tempDir.exists()) {
-//
-//        }
-        if (!tempDir.exists()) {
-            tempDir.mkdirs();
-        }
-
-        List<Message> messages = new ArrayList<>();
-
-        entities.forEach(e -> {
-            Message message = mailVoToMessages(e);
-            if (Objects.nonNull(message)) {
-                messages.add(message);
-            }
-        });
-        for (int i = 0; i < messages.size(); i++) {
-            Message message = messages.get(i);
-            String subject = StrUtil.isNotBlank(message.getSubject()) ? message.getSubject() : "未命名";
-            ZipUtility.saveEmlFile(messages.get(i), tempDir.getPath() + "/" + subject + (i + 1) + ".eml");
-        }
-        File downloadFile = new File(tempDir.getPath());
-        if (!downloadFile.exists()) {
-            downloadFile.mkdirs();
-        }
-        File zipFile = new File(zipFilePath + File.separator + "emails" + currentUserId + ".zip");
-        try {
-            ZipUtility.zipFiles(tempDir, zipFile);
-        } catch (IOException e) {
-            return;
-        }
-        try {
-            FileInputStream inStream = new FileInputStream(zipFile);
-            // 设置响应头
-            response.setContentType("application/zip");
-            response.setContentLength((int) zipFile.length());
-            response.setHeader("Content-Disposition", "attachment; filename=\"" + zipFile.getName() + "\"");
-            // 获取输出流
-            OutputStream outStream = response.getOutputStream();
-            byte[] buffer = new byte[1024];
-            int bytesRead;
-
-            // 写入响应输出流
-            while ((bytesRead = inStream.read(buffer)) != -1) {
-                outStream.write(buffer, 0, bytesRead);
-            }
-        } catch (Exception e) {
-            log.error("Error occurred while downloading the file: ", e);
-        } finally {
-            ZipUtility.deleteDirectory(tempDir);
-        }
+    public void downloadMailEmlZip(MailDTO dto, HttpServletResponse response) {
+        MailRemoteResponseWriter.write(response, crmMailRemoteClient.downloadMailEmlZip(dto), "emails.zip");
     }
 
-
     @PostMapping(value = "/download_mail_eml")
     @ApiOperation(value = "下载邮件")
     @ApiResponses({
             @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
     })
-    public void downloadMail(MailDTO dto, HttpServletResponse response) throws MessagingException {
-        if (Objects.isNull(dto.getMailId())) {
-            return;
-        }
-        EmailsEntity entitie = emailsService.getById(dto.getMailId());
-        if (Objects.isNull(entitie)) {
-            return;
-        }
-        String zipFilePath = mailFileProperties.getPath().getPath() + "temp_eml_files";
-        File tempDir = new File(zipFilePath + File.separator + "temp");
-        if (!tempDir.exists()) {
-            tempDir.mkdirs();
-        }
-
-        Message message = mailVoToMessages(entitie);
-        String subject = StrUtil.isNotBlank(message.getSubject()) ? message.getSubject() : "未命名";
-        String tempPath = tempDir.getPath() + "/" + subject + ".eml";
-        ZipUtility.saveEmlFile(message, tempPath);
-
-        File downloadFile = new File(tempPath);
-        if (!downloadFile.exists()) {
-            downloadFile.mkdirs();
-        }
-        try {
-            FileInputStream inStream = new FileInputStream(downloadFile);
-            // 设置响应头
-            response.reset();
-            response.setContentType("application/octet-stream;charset=utf-8");
-            /*
-             * 跨域配置
-             * */
-            response.addHeader("Access-Control-Allow-Origin", "*");
-            response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
-            response.addHeader("Access-Control-Allow-Headers", "Content-Type");
-//        String filename = "测试.docx";
-            String filename = downloadFile.getName();
-            response.addHeader("content-disposition",
-                    "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")
-                            + ";filename*=utf-8''" + URLEncoder.encode(filename, "UTF-8"));
-//		response.addHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(filename, "UTF-8"));
-            byte[] b = new byte[1024];
-            int len;
-//从输入流中读取一定数量的字节,并将其存储在缓冲区字节数组中,读到末尾返回-1
-            ServletOutputStream outputStream = response.getOutputStream();
-            while ((len = inStream.read(b)) > 0) {
-                outputStream.write(b, 0, len);
-            }
-            inStream.close();
-            outputStream.close();
-        } catch (Exception e) {
-            log.error("Error occurred while downloading the file: ", e);
-        } finally {
-            ZipUtility.deleteDirectory(tempDir);
-        }
+    public void downloadMail(MailDTO dto, HttpServletResponse response) {
+        MailRemoteResponseWriter.write(response, crmMailRemoteClient.downloadMailEml(dto), "mail.eml");
     }
 
     public Message mailVoToMessages(EmailsEntity emails) {
@@ -742,23 +548,7 @@ public class MailApiController {
             @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
     })
     public Result<?> mvMailBox(MailMoveDTO dto) {
-        if (Objects.isNull(dto.getEmailId()) || Objects.isNull(dto.getBoxCode())) {
-            return Result.error("参数错误!");
-        }
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        EmailBoxEnum boxEnum = EmailBoxEnum.getEnumByCode(dto.getBoxCode());
-        if (Objects.isNull(boxEnum)) {
-            return Result.error("参数错误!");
-        }
-        EmailsEntity entity = emailsService.getById(dto.getEmailId());
-        if (Objects.nonNull(entity) && entity.getOwnerBy().equals(currentUserId)) {
-            entity.setFolder(boxEnum.code);
-            entity.setUpdateBy(currentUserId);
-            emailsService.updateById(entity);
-            return Result.ok();
-        } else {
-            return Result.error("参数错误!");
-        }
+        return crmMailRemoteClient.mvMailBox(dto);
     }
 
 
@@ -922,20 +712,7 @@ public class MailApiController {
             @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
     })
     public Result<?> mvTrashBox(MailMoveDTO dto) {
-        if (Objects.isNull(dto.getEmailId()) && StringUtils.isEmpty(dto.getEmailIds())) {
-            return Result.error("参数错误!");
-        }
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        LambdaUpdateWrapper<EmailsEntity> uWrapper = new LambdaUpdateWrapper<>();
-        uWrapper.set(EmailsEntity::getIsTrash, dto.getIsTrash());
-        uWrapper.eq(EmailsEntity::getOwnerBy, currentUserId);
-        if (Objects.nonNull(dto.getEmailId())) {
-            uWrapper.eq(EmailsEntity::getId, dto.getEmailId());
-        } else {
-            uWrapper.in(EmailsEntity::getId, dto.getEmailIds());
-        }
-        emailsService.update(uWrapper);
-        return Result.ok();
+        return crmMailRemoteClient.markTrash(dto);
     }
 
     @ApiOperation(value = "彻底删除")
@@ -944,14 +721,7 @@ public class MailApiController {
     })
     @PostMapping("/deleteIds")
     public Result<?> deleteIds(Long[] ids) {
-        if (Objects.isNull(ids)) {
-            return Result.error("参数错误");
-        }
-        LambdaUpdateWrapper<EmailsEntity> uWrapper = new LambdaUpdateWrapper<>();
-        uWrapper.set(EmailsEntity::getIsDelete, Integer.valueOf(1));
-        uWrapper.in(EmailsEntity::getId, Arrays.asList(ids));
-        emailsService.update(uWrapper);
-        return Result.ok();
+        return crmMailRemoteClient.deleteIds(ids);
     }
 
     @ApiOperation(value = "彻底删除草稿")
@@ -960,13 +730,7 @@ public class MailApiController {
     })
     @PostMapping("/deleteDraftIds")
     public Result<?> deleteDraftIds(Long[] ids) {
-        if (Objects.isNull(ids)) {
-            return Result.error("删除草稿");
-        }
-        LambdaUpdateWrapper<EmailsEntity> uWrapper = new LambdaUpdateWrapper<>();
-        uWrapper.in(EmailsEntity::getId, Arrays.asList(ids));
-        emailsService.remove(uWrapper);
-        return Result.ok();
+        return crmMailRemoteClient.deleteDraftIds(ids);
     }
 
     @PostMapping(value = "/get_mail_detial")
@@ -975,55 +739,19 @@ public class MailApiController {
             @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
     })
     public Result<?> getMailDetial(Long mailId) {
-        if (Objects.isNull(mailId)) {
-            return Result.error("参数错误!");
-        }
-        EmailsEntity email = emailsService.getById(mailId);
-        if (Objects.nonNull(email) && 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 -> {
-                    e.setFileSizeName(FileHelper.formatFileSize(e.getFileSize()));
-                });
-            }
-            email.setAttachments(atts);
-        } else {
-            List<MailAttachmentEntity> atts = mailAttachmentService.list(new LambdaQueryWrapper<MailAttachmentEntity>().eq(MailAttachmentEntity::getEmailId, mailId));
-            if (!CollectionUtils.isEmpty(atts)) {
-                atts.forEach(e -> {
-                    e.setFileSizeName(FileHelper.formatFileSize(e.getFileSize()));
-                });
-            }
-            email.setAttachments(atts);
-        }
-
-        return Result.result(email);
+        return crmMailRemoteClient.getMailDetail(mailId);
     }
 
     @PostMapping(value = "/mv_custom_folder")
     @ApiOperation(value = "迁移邮件到自定义文件夹", notes = "迁移邮件到自定义文件夹")
     public Result getMailPortConfig(Long[] mailIds, Long folderId) {
-        if (Objects.isNull(mailIds) || Objects.isNull(folderId)) {
-            return Result.error("参数错误");
-        }
-        LambdaUpdateWrapper<EmailsEntity> updateWrapper = new LambdaUpdateWrapper<>();
-        updateWrapper.set(EmailsEntity::getCustomFolderId, folderId);
-        updateWrapper.in(EmailsEntity::getId, Arrays.asList(mailIds));
-        emailsService.update(updateWrapper);
-        return Result.ok();
+        return crmMailRemoteClient.mvCustomFolder(mailIds, folderId);
     }
 
     @PostMapping(value = "/del_custom_folder_mail")
     @ApiOperation(value = "获取邮箱端口配置", notes = "获取邮箱端口配置")
     public Result delCustomFolderMail(Long[] mailIds) {
-        if (Objects.isNull(mailIds)) {
-            return Result.error("参数错误");
-        }
-        LambdaUpdateWrapper<EmailsEntity> updateWrapper = new LambdaUpdateWrapper<>();
-        updateWrapper.in(EmailsEntity::getId, Arrays.asList(mailIds));
-        updateWrapper.set(EmailsEntity::getCustomFolderId, null);
-        emailsService.update(updateWrapper);
-        return Result.ok();
+        return crmMailRemoteClient.delCustomFolderMail(mailIds);
     }
 
     @PostMapping(value = "/mark_star")
@@ -1032,20 +760,7 @@ public class MailApiController {
             @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
     })
     public Result<?> takeStar(MailMoveDTO dto) {
-        if (Objects.isNull(dto.getEmailId()) && StringUtils.isEmpty(dto.getEmailIds())) {
-            return Result.error("参数错误!");
-        }
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        LambdaUpdateWrapper<EmailsEntity> uWrapper = new LambdaUpdateWrapper<>();
-        uWrapper.set(EmailsEntity::getIsStar, dto.getIsStar());
-        uWrapper.eq(EmailsEntity::getOwnerBy, currentUserId);
-        if (Objects.nonNull(dto.getEmailId())) {
-            uWrapper.eq(EmailsEntity::getId, dto.getEmailId());
-        } else {
-            uWrapper.in(EmailsEntity::getId, dto.getEmailIds());
-        }
-        emailsService.update(uWrapper);
-        return Result.ok();
+        return crmMailRemoteClient.markStar(dto);
     }
 
     @PostMapping(value = "/mark_read")
@@ -1054,18 +769,7 @@ public class MailApiController {
             @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
     })
     public Result<?> markRead(MailMoveDTO dto) {
-        if (Objects.isNull(dto.getEmailId()) && StringUtils.isEmpty(dto.getEmailIds())) {
-            return Result.error("参数错误!");
-        }
-        LambdaUpdateWrapper<EmailsEntity> uWrapper = new LambdaUpdateWrapper<>();
-        uWrapper.set(EmailsEntity::getIsRead, dto.getIsRead());
-        if (Objects.nonNull(dto.getEmailId())) {
-            uWrapper.eq(EmailsEntity::getId, dto.getEmailId());
-        } else {
-            uWrapper.in(EmailsEntity::getId, dto.getEmailIds());
-        }
-        emailsService.update(uWrapper);
-        return Result.ok();
+        return crmMailRemoteClient.markRead(dto);
     }
 
     @PostMapping(value = "/mark_reply_msg_id")
@@ -1074,18 +778,7 @@ public class MailApiController {
             @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
     })
     public Result<?> markReplyMsgId(MailMoveDTO dto) {
-        if (Objects.isNull(dto.getEmailId()) && StringUtils.isEmpty(dto.getEmailIds())) {
-            return Result.error("参数错误!");
-        }
-        LambdaUpdateWrapper<EmailsEntity> uWrapper = new LambdaUpdateWrapper<>();
-        uWrapper.set(EmailsEntity::getReplyMsgId, dto.getReplyMsgId());
-        if (Objects.nonNull(dto.getEmailId())) {
-            uWrapper.eq(EmailsEntity::getId, dto.getEmailId());
-        } else {
-            uWrapper.in(EmailsEntity::getId, dto.getEmailIds());
-        }
-        emailsService.update(uWrapper);
-        return Result.ok();
+        return crmMailRemoteClient.markReplyMsgId(dto);
     }
 
     @PostMapping(value = "/mark_all_read")
@@ -1094,16 +787,7 @@ public class MailApiController {
             @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
     })
     public Result<?> markAllRead() {
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        if (Objects.isNull(currentUserId)) {
-            return Result.error("登录失效,请重新登陆!");
-        }
-        LambdaUpdateWrapper<EmailsEntity> uWrapper = new LambdaUpdateWrapper<>();
-        uWrapper.set(EmailsEntity::getIsRead, Integer.valueOf(1));
-        uWrapper.eq(EmailsEntity::getOwnerBy, currentUserId);
-        uWrapper.eq(EmailsEntity::getIsRead, Integer.valueOf(0));
-        emailsService.update(uWrapper);
-        return Result.ok();
+        return crmMailRemoteClient.markAllRead();
     }
 
     @PostMapping(value = "/list_mail_attachment")
@@ -1112,398 +796,44 @@ public class MailApiController {
             @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
     })
     public Result listMailAttachment(AttachmentDTO dto) {
-
-        Long currentUserId = LoginUserUtil.getCurrentUserId();
-        SmtpPopSettingsEntity emailPop = smtpPopSettingsService.getDefaultSmtpPop(currentUserId);
-        if (Objects.isNull(emailPop)) {
-            return Result.result(null);
-        }
-
-        QueryWrapper<MailAttachmentEntity> queryWrapper = new QueryWrapper();
-        queryWrapper.eq("att.smtp_pop_id", emailPop.getId());
-
-        if (StrUtil.isNotBlank(dto.getFileExt())) {
-            if ("png".equals(dto.getFileExt())) {
-                queryWrapper.in("att.file_ext", Arrays.asList("png", "jpg"));
-            } else if ("video".equals(dto.getFileExt())) {
-                queryWrapper.in("att.file_ext", Arrays.asList("mp4", "video"));
-            } else if ("zip".equals(dto.getFileExt())) {
-                queryWrapper.in("att.file_ext", Arrays.asList("zip", "rar", "7z"));
-            } else if ("txt".equals(dto.getFileExt())) {
-                queryWrapper.in("att.file_ext", Arrays.asList("pdf", "word", "excel"));
-            } else {
-                queryWrapper.eq("att.file_ext", dto.getFileExt());
-            }
-        }
-        Page<MailAttachmentEntity> page = new Page<>(dto.getPageIndex(), dto.getPageSize());
-
-        queryWrapper.eq("att.is_delete", CommonConstant.DEL_FLAG_0);
-        queryWrapper.eq("e.smtp_pop_id", emailPop.getId());
-
-        queryWrapper.orderByDesc("e.sent_date");
-
-        IPage<MailAttachmentEntity> pageList = mailAttachmentService.pateList(page, queryWrapper);
-
-        Map<Long, String> emailMap = new HashMap<>();
-        if (!CollectionUtils.isEmpty(pageList.getRecords())) {
-            List<Long> mailIds = pageList.getRecords().stream().map(MailAttachmentEntity::getEmailId).collect(Collectors.toList());
-            LambdaQueryWrapper<EmailsEntity> eQueryWrapper = new LambdaQueryWrapper<>();
-            eQueryWrapper.in(EmailsEntity::getId, mailIds);
-            List<EmailsEntity> emails = emailsService.list(eQueryWrapper);
-            if (!CollectionUtils.isEmpty(emails)) {
-                emailMap = emails.stream().collect(Collectors.toMap(EmailsEntity::getId, EmailsEntity::getSubject));
-            }
-            Map<Long, String> finalEmailMap = emailMap;
-            pageList.getRecords().forEach(e -> {
-                e.setFileSizeName(FileHelper.formatFileSize(e.getFileSize()));
-                e.setSubject(finalEmailMap.get(e.getEmailId()));
-            });
-        }
-        return Result.result(pageList);
+        return crmMailRemoteClient.listMailAttachment(dto);
     }
 
 
     @PostMapping(value = "/download_zip")
     @ApiOperation(value = "打包下载")
-    @ApiResponses({
-            @ApiResponse(code = 200, message = "", response = SmtpPopSettingsEntity.class)
-    })
     public void downloadZip(AttachmentDTO dto, HttpServletResponse response) {
-        if (Objects.isNull(dto.getMailId()) && CollectionUtils.isEmpty(dto.getAttachmentIds())) {
-            return;
-        }
-        String zipFilePath = mailFileProperties.getPath().getPath() + System.currentTimeMillis() + RandomGenerateHelper.generateRandomString(6) + ".zip";
-        List<MailAttachmentEntity> attachments;
-        if (Objects.nonNull(dto.getMailId())) {
-            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)));
-        }
-        if (CollectionUtils.isEmpty(attachments)) {
-            return;
-        }
-        try {
-            ZipUtility.zipFiles(attachments, zipFilePath, mailFileProperties);
-        } catch (IOException e) {
-            return;
-        }
-        File downloadFile = new File(zipFilePath);
-        try {
-            FileInputStream inStream = new FileInputStream(downloadFile);
-            // 设置响应头
-            response.setContentType("application/zip");
-            response.setContentLength((int) downloadFile.length());
-            response.setHeader("Content-Disposition", "attachment; filename=\"" + downloadFile.getName() + "\"");
-            // 获取输出流
-            OutputStream outStream = response.getOutputStream();
-            byte[] buffer = new byte[1024];
-            int bytesRead;
-
-            // 写入响应输出流
-            while ((bytesRead = inStream.read(buffer)) != -1) {
-                outStream.write(buffer, 0, bytesRead);
-            }
-        } catch (Exception e) {
-            log.error("Error occurred while downloading the file: ", e);
-        } finally {
-            // 下载完成后删除临时ZIP文件
-            if (downloadFile.exists()) {
-                if (!downloadFile.delete()) {
-                    System.err.println("Failed to delete the temporary file: " + zipFilePath);
-                }
-            }
-        }
+        MailRemoteResponseWriter.write(response, crmMailRemoteClient.downloadZip(dto), "attachments.zip");
     }
 
     @PostMapping(value = "/download_url")
     @ApiOperation(value = "下载", notes = "下载")
-    public Result downloadUrl(Long attachmentId) throws IOException {
-        try {
-            if (attachmentId == null) {
-                return Result.error("附未找到");
-            }
-            MailAttachmentEntity attachment = mailAttachmentService.getById(attachmentId);
-            if (attachment == null) {
-                return Result.error("附未找到");
-            }
-
-            String url =  mailFileProperties.getPath().getPath() + File.separator + attachment.getFilePath();
-
-
-            // 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();
-
-            Path target = Paths.get(exportDir.getAbsolutePath());
-            // 创建目标目录(如果不存在)
-            if (!Files.exists(target.getParent())) {
-                Files.createDirectories(target.getParent());
-            }
-            Path targetPath = target.resolve(fileName);
-            Files.copy(filePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
-
-            // 2. 拼接 URL(这里假设映射后访问路径是 /export/)
-            String fileUrl = "/static/file/" + targetPath.getFileName();
-
-            String domainUrl = "";
-            if(active.equals("prod")) {
-                domainUrl = "https://sales.storlead.com"+fileUrl;
-            } else if (active.equals("test")){
-                domainUrl = "https://sales.test.storlead.com"+fileUrl;
-            } else if (active.equals("dev")){
-                domainUrl = "http://localhost:"+port+fileUrl;
-            }
-            return Result.result(domainUrl);
-        } catch (Exception e) {
-            log.error("download--error", e);
-            return Result.error("附未找到");
-        }
+    public Result downloadUrl(Long attachmentId) {
+        return crmMailRemoteClient.downloadUrl(attachmentId);
     }
 
     @PostMapping(value = "/draft_download_url")
-    @ApiOperation(value = "下载", notes = "下载")
-    public Result draft_download(Long attachmentId) throws IOException {
-        try {
-            if (attachmentId == null) {
-                return Result.error("附未找到");
-            }
-            MailTempAttachmentEntity attachment = tempAttachmentService.getById(attachmentId);
-            if (attachment == null) {
-                return Result.error("附未找到");
-            }
-
-            String url =  mailFileProperties.getPath().getPath() + File.separator + attachment.getFilePath();
-
-            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();
-
-            Path target = Paths.get(exportDir.getAbsolutePath());
-            // 创建目标目录(如果不存在)
-            if (!Files.exists(target.getParent())) {
-                Files.createDirectories(target.getParent());
-            }
-            Path targetPath = target.resolve(fileName);
-            Files.copy(filePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
-
-            // 2. 拼接 URL(这里假设映射后访问路径是 /export/)
-            String fileUrl = "/static/file/" + targetPath.getFileName();
-
-            String domainUrl = "";
-            if(active.equals("prod")) {
-                domainUrl = "https://sales.storlead.com"+fileUrl;
-            } else if (active.equals("test")){
-                domainUrl = "https://sales.test.storlead.com"+fileUrl;
-            } else if (active.equals("dev")){
-                domainUrl = "http://localhost:"+port+fileUrl;
-            }
-            return Result.result(domainUrl);
-        } catch (Exception e) {
-            log.error("download--error", e);
-            return Result.error("附未找到");
-        }
+    @ApiOperation(value = "草稿附件下载地址")
+    public Result draft_download(Long attachmentId) {
+        return crmMailRemoteClient.draftDownloadUrl(attachmentId);
     }
 
     @PostMapping(value = "/draft_download")
     @ApiOperation(value = "下载", notes = "下载")
-    public void draft_download(Long attachmentId, HttpServletResponse response) throws IOException {
-        try {
-            if (attachmentId == null) {
-                return;
-            }
-            MailTempAttachmentEntity attachment = tempAttachmentService.getById(attachmentId);
-            if (attachment == null) {
-                return;
-            }
-            InputStream inputStream;
-            String url = mailFileProperties.getPath().getPath() + attachment.getFilePath();
-            inputStream = new BufferedInputStream(new FileInputStream(url));
-            response.reset();
-            response.setContentType("application/octet-stream;charset=utf-8");
-            /*
-             * 跨域配置
-             * */
-            response.addHeader("Access-Control-Allow-Origin", "*");
-            response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
-            response.addHeader("Access-Control-Allow-Headers", "Content-Type");
-//        String filename = "测试.docx";
-            String filename = attachment.getFileName() + "." + attachment.getFileExt();
-            response.addHeader("content-disposition",
-                    "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")
-                            + ";filename*=utf-8''" + URLEncoder.encode(filename, "UTF-8"));
-//		response.addHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(filename, "UTF-8"));
-            byte[] b = new byte[1024];
-            int len;
-//从输入流中读取一定数量的字节,并将其存储在缓冲区字节数组中,读到末尾返回-1
-            ServletOutputStream outputStream = response.getOutputStream();
-            while ((len = inputStream.read(b)) > 0) {
-                outputStream.write(b, 0, len);
-            }
-            inputStream.close();
-            outputStream.close();
-        } catch (Exception e) {
-            log.error("download--error", e);
-        }
+    public void draft_download(Long attachmentId, HttpServletResponse response) {
+        MailRemoteResponseWriter.write(response, crmMailRemoteClient.draftDownload(attachmentId));
     }
 
     @RequestMapping(value = "/download")
     @ApiOperation(value = "下载", notes = "下载")
-    public void download(Long attachmentId, HttpServletResponse response) throws IOException {
-        try {
-            if (attachmentId == null) {
-                return;
-            }
-            MailAttachmentEntity attachment = mailAttachmentService.getById(attachmentId);
-            if (attachment == null) {
-                return;
-            }
-            InputStream inputStream;
-            String url = mailFileProperties.getPath().getPath() + attachment.getFilePath();
-            if (Integer.valueOf(0).equals(attachment.getDownload())) {
-                if (new File(url).exists()) {
-                    LambdaUpdateWrapper<MailAttachmentEntity> attachmentUpdate = new LambdaUpdateWrapper<>();
-                    attachmentUpdate.set(MailAttachmentEntity::getDownload, Integer.valueOf(1));
-                    attachmentUpdate.eq(MailAttachmentEntity::getId, attachment.getId());
-                    mailAttachmentService.update(attachmentUpdate);
-                }
-                //没有下载
-                EmailsEntity email = emailsService.getById(attachment.getEmailId());
-                SmtpPopSettingsEntity smtpPop = smtpPopSettingsService.getById(email.getSmtpPopId());
-                inputStream = emailsService.loadEmailsFile(smtpPop, email, attachment.getFileName() + "." + attachment.getFileExt());
-                if (Objects.isNull(inputStream)) {
-                    return;
-                }
-                String path = mailFileProperties.getPath().getPath() + attachment.getFilePath();
-                File file = new File(path);
-                try (FileOutputStream output = new FileOutputStream(file)) {
-                    inputStream.transferTo(output);
-                }
-                LambdaUpdateWrapper<MailAttachmentEntity> attachmentUpdate = new LambdaUpdateWrapper<>();
-                attachmentUpdate.set(MailAttachmentEntity::getDownload, Integer.valueOf(1));
-                attachmentUpdate.set(MailAttachmentEntity::getFileSize, file.length());
-                attachmentUpdate.eq(MailAttachmentEntity::getId, attachment.getId());
-                mailAttachmentService.update(attachmentUpdate);
-            }
-            inputStream = new BufferedInputStream(new FileInputStream(url));
-            response.reset();
-            /*
-             * 跨域配置
-             * */
-            response.addHeader("Access-Control-Allow-Origin", "*");
-            response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
-            response.addHeader("Access-Control-Allow-Headers", "Content-Type");
-//        String filename = "测试.docx";
-            String filename = attachment.getFileName() + "." + attachment.getFileExt();
-
-            String ext = attachment.getFileExt().toLowerCase();
-            boolean inline = ext.equals("pdf")
-                    || ext.equals("png")
-                    || ext.equals("jpg")
-                    || ext.equals("jpeg")
-                    || ext.equals("gif");
-            if (inline) {
-                if ("pdf".equals(ext)) {
-                    response.setContentType("application/pdf");
-                } else {
-                    response.setContentType("image/" + ext);
-                }
-            } else {
-//                response.setContentType("application/octet-stream");
-                response.setContentType("application/octet-stream;charset=utf-8");
-            }
-            String dispositionType = inline ? "inline" : "attachment";
-            response.setHeader(
-                    "Content-Disposition",
-                    dispositionType + "; filename=" + URLEncoder.encode(filename, "UTF-8")
-                            + ";filename*=utf-8''" + URLEncoder.encode(filename, "UTF-8"));
-
-//            response.addHeader("content-disposition",
-//                    "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")
-//                            + ";filename*=utf-8''" + URLEncoder.encode(filename, "UTF-8"));
-//		response.addHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(filename, "UTF-8"));
-            byte[] b = new byte[1024];
-            int len;
-//从输入流中读取一定数量的字节,并将其存储在缓冲区字节数组中,读到末尾返回-1
-            ServletOutputStream outputStream = response.getOutputStream();
-            while ((len = inputStream.read(b)) > 0) {
-                outputStream.write(b, 0, len);
-            }
-            inputStream.close();
-            outputStream.close();
-        } catch (Exception e) {
-            log.error("download--error", e);
-        }
-    }
-
-    // 读取 InputStream 并计算其大小
-    public int getAttachmentSize(InputStream inputStream) throws Exception {
-        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
-        byte[] buffer = new byte[4096];
-        int bytesRead;
-        while ((bytesRead = inputStream.read(buffer)) != -1) {
-            byteArrayOutputStream.write(buffer, 0, bytesRead);
-        }
-        return byteArrayOutputStream.size();
-    }
-
-    public InputStream copyInputStream(InputStream original) throws IOException {
-        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
-        byte[] buffer = new byte[4096];
-        int bytesRead;
-        while ((bytesRead = original.read(buffer)) != -1) {
-            byteArrayOutputStream.write(buffer, 0, bytesRead);
-        }
-        // 生成的字节数组可以用来创建多个 InputStream 副本
-        return new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
+    public void download(Long attachmentId, HttpServletResponse response) {
+        MailRemoteResponseWriter.write(response, crmMailRemoteClient.download(attachmentId));
     }
 
     @PostMapping(value = "/mail_port_config")
     @ApiOperation(value = "获取邮箱端口配置", notes = "获取邮箱端口配置")
-    public Result getMailPortConfig(String mailAccount) throws IOException {
-        if (StrUtil.isBlank(mailAccount)) {
-            return Result.result("参数错误");
-        }
-        LambdaQueryWrapper<MailServerPortConfigEntity> queryWrapper = new LambdaQueryWrapper<>();
-        queryWrapper.eq(MailServerPortConfigEntity::getMailServerSuffix, EmailServiceChecker.getEmailService(mailAccount));
-        queryWrapper.last("limit 1");
-        MailServerPortConfigEntity portConfig = serverPortConfigService.getOne(queryWrapper);
-        return Result.result(portConfig);
+    public Result getMailPortConfig(String mailAccount) {
+        return crmMailRemoteClient.mailPortConfig(mailAccount);
     }
 
     @PostMapping(value = "/get_unread_count")

+ 2 - 2
java/storlead-sasa/storlead-trade/pom.xml

@@ -15,8 +15,8 @@
     <name>${project.artifactId}</name>
 
     <properties>
-        <maven.compiler.source>17</maven.compiler.source>
-        <maven.compiler.target>17</maven.compiler.target>
+        <maven.compiler.source>11</maven.compiler.source>
+        <maven.compiler.target>11</maven.compiler.target>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>