SpringCloud微服務實戰——搭建企業級開發框架(四十三):多租戶可設定的電子郵件傳送系統設計與實現

2022-07-07 15:00:50

  在日常生活中,郵件已經被聊天軟體、簡訊等更便捷的資訊傳送方式代替。但在日常工作中,我們的重要的資訊通知等非常有必要去歸檔追溯,那麼郵件就是不可或缺的資訊傳送渠道。對於我們工作中經常用到的系統,裡面也基本都整合了郵件傳送功能。
  SpringBoot提供了基於JavaMail的starter,我們只要按照官方的說明設定郵件伺服器資訊,即可使我們的系統擁有傳送電子郵件的功能。但是,在我們GitEgg開發框架的實際業務開發過程中,有兩個問題需要解決:一個是SpringBoot郵箱伺服器的設定是設定在組態檔中的,不支援靈活的介面設定。另外一個是我們的開發框架需要支援多租戶,那麼此時需要對SpringBoot提供的郵件傳送功能進行擴充套件,以滿足我們的需求。

那麼,基於以上需求和問題,我們對GitEgg框架進行擴充套件,增加以下功能:

1、擴充套件系統設定:將郵箱伺服器的設定資訊持久化到資料庫、Redis快取,和組態檔一起使用,制定讀取優先順序。
2、擴充套件多租戶設定:如果系統開啟了多租戶功能,那麼在郵件傳送時,首先讀取租戶的當前設定,如果沒有設定,那麼在讀取系統設定。
3、自有選擇伺服器:使用者可在系統介面上選擇指定的郵箱伺服器進行郵件傳送。
4、提供郵件傳送模板:使用者可選擇預先制定的郵件模板進行傳送特定郵件。
5、增加傳送數量、頻率限制:增加設定,限制模板郵件的傳送數量和頻率。
6、儲存郵件傳送記錄:不一定把所有附件都儲存,只需儲存郵件傳送關鍵資訊,如果需要儲存所有附件等需要自己擴充套件。

  同一個租戶可以設定多個電子郵件伺服器,但只可以設定一個伺服器為啟用狀態。預設情況下,系統通知類的功能只使用啟用狀態的伺服器進行郵件傳送。在有客製化化需求的情況下,比如從頁面直接指定某個伺服器進行郵件傳送,那麼提供可以選擇的介面,指定某個伺服器進行郵件傳送。

一、整合spring-boot-starter-mail擴充套件基礎郵件傳送功能

1、在基礎框架gitegg-platform中新建gitegg-platform-mail子專案,引入郵件必需的相關依賴包。
    <dependencies>
        <!-- gitegg Spring Boot自定義及擴充套件 -->
        <dependency>
            <groupId>com.gitegg.platform</groupId>
            <artifactId>gitegg-platform-boot</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
            <!-- 去除springboot預設的logback設定-->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
2、擴充套件郵件伺服器設定類,增加租戶等資訊,方便從快取讀取到資訊之後進行設定轉換。
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class GitEggMailProperties extends MailProperties {

    /**
     * 設定id
     */
    private Long id;

    /**
     * 租戶id
     */
    private Long tenantId;

    /**
     * 渠道id
     */
    private String channelCode;

    /**
     * 狀態
     */
    private Integer channelStatus;

    /**
     * 設定的md5值
     */
    private String md5;
}
3、擴充套件郵件傳送實現類JavaMailSenderImpl,新增多租戶和郵箱伺服器編碼,便於多租戶和渠道選擇。
@Data
public class GitEggJavaMailSenderImpl extends JavaMailSenderImpl {

    /**
     * 設定id
     */
    private Long id;

    /**
     * 租戶id
     */
    private Long tenantId;

    /**
     * 渠道編碼
     */
    private String channelCode;

    /**
     * 設定的md5值
     */
    private String md5;

}
4、新建郵件傳送範例工廠類JavaMailSenderFactory,在郵件傳送時,根據需求生產需要的郵件傳送範例。
@Slf4j
public class JavaMailSenderFactory {

    private RedisTemplate redisTemplate;

    private JavaMailSenderImpl javaMailSenderImpl;

    /**
     * 是否開啟租戶模式
     */
    private Boolean enable;

    /**
     * JavaMailSender 快取
     * 儘管存在多個微服務,但是隻需要在每個微服務初始化一次即可
     */
    private final static Map<String, GitEggJavaMailSenderImpl> javaMailSenderMap = new ConcurrentHashMap<>();

    public JavaMailSenderFactory(RedisTemplate redisTemplate, JavaMailSenderImpl javaMailSenderImpl, Boolean enable) {
        this.redisTemplate = redisTemplate;
        this.javaMailSenderImpl = javaMailSenderImpl;
        this.enable = enable;
    }

    /**
     * 指定郵件傳送渠道
     * @return
     */
    public JavaMailSenderImpl getMailSender(String... channelCode){
        if (null == channelCode || channelCode.length == GitEggConstant.COUNT_ZERO
                || null == channelCode[GitEggConstant.Number.ZERO])
        {
            return this.getDefaultMailSender();
        }
        // 首先判斷是否開啟多租戶
        String mailConfigKey = JavaMailConstant.MAIL_TENANT_CONFIG_KEY;

        if (enable) {
            mailConfigKey += GitEggAuthUtils.getTenantId();
        } else {
            mailConfigKey = JavaMailConstant.MAIL_CONFIG_KEY;
        }

        // 從快取獲取郵件設定資訊
        // 根據channel code獲取設定,用channel code時,不區分是否是預設設定
        String propertiesStr = (String) redisTemplate.opsForHash().get(mailConfigKey, channelCode[GitEggConstant.Number.ZERO]);
        if (StringUtils.isEmpty(propertiesStr))
        {
            throw new BusinessException("未獲取到[" + channelCode[GitEggConstant.Number.ZERO] + "]的郵件設定資訊");
        }
        GitEggMailProperties properties = null;
        try {
            properties = JsonUtils.jsonToPojo(propertiesStr, GitEggMailProperties.class);
        } catch (Exception e) {
            log.error("轉換郵件設定資訊異常:{}", e);
            throw new BusinessException("轉換郵件設定資訊異常:" + e);
        }
        return this.getMailSender(mailConfigKey, properties);
    }

    /**
     * 不指定郵件傳送渠道,取預設設定
     * @return
     */
    public JavaMailSenderImpl getDefaultMailSender(){
        // 首先判斷是否開啟多租戶
        String mailConfigKey = JavaMailConstant.MAIL_TENANT_CONFIG_KEY;

        if (enable) {
            mailConfigKey += GitEggAuthUtils.getTenantId();
        } else {
            mailConfigKey = JavaMailConstant.MAIL_CONFIG_KEY;
        }

        // 獲取所有郵件設定列表
        Map<Object, Object> propertiesMap = redisTemplate.opsForHash().entries(mailConfigKey);
        Iterator<Map.Entry<Object, Object>> entries = propertiesMap.entrySet().iterator();
        // 如果沒有設定取哪個設定,那麼獲取預設的設定
        GitEggMailProperties properties = null;
        try {
            while (entries.hasNext()) {
                Map.Entry<Object, Object> entry = entries.next();
                // 轉為系統設定物件
                GitEggMailProperties propertiesEnable = JsonUtils.jsonToPojo((String) entry.getValue(), GitEggMailProperties.class);
                if (propertiesEnable.getChannelStatus().intValue() == GitEggConstant.ENABLE) {
                    properties = propertiesEnable;
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return this.getMailSender(mailConfigKey, properties);
    }

    private JavaMailSenderImpl getMailSender(String mailConfigKey, GitEggMailProperties properties) {
        // 根據最新設定資訊判斷是否從本地獲取mailSender,在設定儲存時,計算實體設定的md5值,然後進行比較,不要在每次對比的時候進行md5計算
        if (null != properties && !StringUtils.isEmpty(properties.getMd5()))
        {
            GitEggJavaMailSenderImpl javaMailSender = javaMailSenderMap.get(mailConfigKey);
            if (null == javaMailSender || !properties.getMd5().equals(javaMailSender.getMd5()))
            {
                // 如果沒有設定資訊,那麼直接返回系統預設設定的mailSender
                javaMailSender = new GitEggJavaMailSenderImpl();
                this.applyProperties(properties, javaMailSender);
                javaMailSender.setMd5(properties.getMd5());
                javaMailSender.setId(properties.getId());
                // 將MailSender放入快取
                javaMailSenderMap.put(mailConfigKey, javaMailSender);
            }
            return javaMailSender;
        }
        else
        {
            return this.javaMailSenderImpl;
        }
    }

    private void applyProperties(MailProperties properties, JavaMailSenderImpl sender) {
        sender.setHost(properties.getHost());
        if (properties.getPort() != null) {
            sender.setPort(properties.getPort());
        }

        sender.setUsername(properties.getUsername());
        sender.setPassword(properties.getPassword());
        sender.setProtocol(properties.getProtocol());
        if (properties.getDefaultEncoding() != null) {
            sender.setDefaultEncoding(properties.getDefaultEncoding().name());
        }

        if (!properties.getProperties().isEmpty()) {
            sender.setJavaMailProperties(this.asProperties(properties.getProperties()));
        }

    }

    private Properties asProperties(Map<String, String> source) {
        Properties properties = new Properties();
        properties.putAll(source);
        return properties;
    }
}
5、設定非同步郵件傳送的執行緒池,這裡需注意非同步執行緒池上下文變數共用問題,有兩種方式解決,一個是使用裝飾器TaskDecorator將父子執行緒變數進行復制,還有一種方式是transmittable-thread-local來共用執行緒上下文,這裡不展開描述,後續會專門針對如何在微服務非同步執行緒池中共用上線文進行說明。
@Configuration
public class MailThreadPoolConfig {

    @Value("${spring.mail-task.execution.pool.core-size}")
    private int corePoolSize;

    @Value("${spring.mail-task.execution.pool.max-size}")
    private int maxPoolSize;

    @Value("${spring.mail-task.execution.pool.queue-capacity}")
    private int queueCapacity;

    @Value("${spring.mail-task.execution.thread-name-prefix}")
    private String namePrefix;

    @Value("${spring.mail-task.execution.pool.keep-alive}")
    private int keepAliveSeconds;

    /**
     * 郵件傳送的執行緒池
     * @return
     */
    @Bean("mailTaskExecutor")
    public Executor mailTaskExecutor(){

        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //最大執行緒數
        executor.setMaxPoolSize(maxPoolSize);
        //核心執行緒數
        executor.setCorePoolSize(corePoolSize);
        //任務佇列的大小
        executor.setQueueCapacity(queueCapacity);
        //執行緒字首名
        executor.setThreadNamePrefix(namePrefix);
        //執行緒存活時間
        executor.setKeepAliveSeconds(keepAliveSeconds);

        // 設定裝飾器,父子執行緒共用request header變數
        executor.setTaskDecorator(new RequestHeaderTaskDecorator());

        /**
         * 拒絕處理策略
         * CallerRunsPolicy():交由呼叫方執行緒執行,比如 main 執行緒。
         * AbortPolicy():直接丟擲異常。
         * DiscardPolicy():直接丟棄。
         * DiscardOldestPolicy():丟棄佇列中最老的任務。
         */
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 執行緒初始化
        executor.initialize();
        return executor;
    }
}
6、增加郵件傳送結果的列舉類MailResultCodeEnum
public enum MailResultCodeEnum {

    /**
     * 預設
     */
    SUCCESS("success", "郵件傳送成功"),

    /**
     * 自定義
     */
    ERROR("error", "郵件傳送失敗");

    public String code;

    public String message;

    MailResultCodeEnum(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

}
7、增加郵箱伺服器相關預設設定的常數類JavaMailConstant.java
public class JavaMailConstant {
    /**
     * Redis JavaMail設定config key
     */
    public static final String MAIL_CONFIG_KEY = "mail:config";

    /**
     * 當開啟多租戶模式時,Redis JavaMail設定config key
     */
    public static final String MAIL_TENANT_CONFIG_KEY = "mail:tenant:config:";
}
8、增加GitEggJavaMail自動裝配類,根據Nacos或者系統設定進行裝配。
@Slf4j
@Configuration
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class GitEggJavaMailConfiguration {

    private final JavaMailSenderImpl javaMailSenderImpl;
    
    private final RedisTemplate redisTemplate;

    /**
     * 是否開啟租戶模式
     */
    @Value("${tenant.enable}")
    private Boolean enable;
    
    @Bean
    public JavaMailSenderFactory gitEggAuthRequestFactory() {
        return new JavaMailSenderFactory(redisTemplate, javaMailSenderImpl, enable);
    }
}

二、增加郵箱伺服器設定介面

  郵箱伺服器的設定,實際就是不同郵箱渠道的設定,這裡我們將表和欄位設計好,然後使用GitEgg自帶程式碼生成器,生成業務的CRUD程式碼即可。

1、郵箱渠道設定表設計
CREATE TABLE `t_sys_mail_channel`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租戶id',
  `channel_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '渠道編碼',
  `channel_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '渠道名稱',
  `host` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'SMTP伺服器地址',
  `port` int(11) NULL DEFAULT NULL COMMENT 'SMTP伺服器埠',
  `username` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '賬戶名',
  `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密碼',
  `protocol` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'smtp' COMMENT '協定',
  `default_encoding` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '預設編碼',
  `jndi_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '對談JNDI名稱',
  `properties` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'JavaMail 設定',
  `channel_status` tinyint(2) NOT NULL DEFAULT 0 COMMENT '渠道狀態 1有效 0禁用',
  `md5` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'MD5',
  `comments` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '建立時間',
  `creator` bigint(20) NULL DEFAULT NULL COMMENT '建立者',
  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新時間',
  `operator` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
  `del_flag` tinyint(2) NULL DEFAULT 0 COMMENT '是否刪除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '郵件渠道' ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;
2、根據表設計,然後設定程式碼生成介面,生成前後端程式碼。

3、生成程式碼後,進行相關許可權設定,前端介面展示:

三、以同樣的方式增加郵箱模板設定介面和郵件傳送紀錄檔記錄

1、郵箱模板和郵件傳送紀錄檔資料庫表設計

郵件模板資料庫表設計:

CREATE TABLE `t_sys_mail_template`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租戶id',
  `template_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '模板編碼',
  `template_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '模板名稱',
  `sign_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '模板簽名',
  `template_status` tinyint(2) NOT NULL DEFAULT 1 COMMENT '模板狀態',
  `template_type` tinyint(2) NULL DEFAULT NULL COMMENT '模板型別',
  `template_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '模板內容',
  `cache_code_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '快取key',
  `cache_time_out` bigint(20) NULL DEFAULT 0 COMMENT '快取有效期 值',
  `cache_time_out_unit` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '快取有效期 單位',
  `send_times_limit` bigint(20) NULL DEFAULT 0 COMMENT '傳送次數限制',
  `send_times_limit_period` bigint(20) NULL DEFAULT 0 COMMENT '限制時間間隔',
  `send_times_limit_period_unit` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '限制時間間隔 單位',
  `comments` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '建立時間',
  `creator` bigint(20) NULL DEFAULT NULL COMMENT '建立者',
  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新時間',
  `operator` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
  `del_flag` tinyint(2) NULL DEFAULT 0 COMMENT '是否刪除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '郵件模板' ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;

郵件紀錄檔資料庫表設計:

CREATE TABLE `t_sys_mail_log`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租戶id',
  `channel_id` bigint(20) NULL DEFAULT NULL COMMENT 'mail渠道id',
  `template_id` bigint(20) NULL DEFAULT NULL COMMENT 'mail模板id',
  `mail_subject` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '郵件主題',
  `mail_from` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '傳送人',
  `mail_to` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '收件人',
  `mail_cc` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '抄送',
  `mail_bcc` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '密抄送',
  `mail_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '郵件內容',
  `attachment_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '附件名稱',
  `attachment_size` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '附件大小',
  `send_time` datetime(0) NULL DEFAULT NULL COMMENT '傳送時間',
  `send_result_code` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '1' COMMENT '傳送結果碼',
  `send_result_msg` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '傳送結果訊息',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '建立日期',
  `creator` bigint(20) NULL DEFAULT NULL COMMENT '建立者',
  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新日期',
  `operator` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
  `del_flag` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否刪除 1:刪除 0:不刪除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '郵件記錄' ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;
2、郵件模板和郵件傳送紀錄檔介面

四、QQ郵箱設定和阿里雲企業郵箱設定測試

  上面的基本功能開發完成之後,那麼我們就需要進行測試,這裡選擇兩種型別的郵箱進行測試,一種是QQ郵箱,還有一種是阿里雲企業郵箱。

1、QQ郵箱設定

QQ郵箱在設定的時候不能使用QQ的登入密碼,需要單獨設定QQ郵箱的授權碼,下面是操作步驟:

  • 開通qq郵箱的smtp功能



  • 經過一系列的驗證之後,會獲取到一個授權碼:
  • 系統中設定QQ郵箱相關資訊
2、 阿里雲企業郵箱設定

阿里雲企業郵箱的設定相比較而言就簡單一些,設定的密碼就是企業郵箱登入的密碼。

  • 賬戶設定,開啟POP3/SMTP和IMAP/SMTP服務
  • 系統中設定阿里雲企業郵箱相關資訊
3、Nacos中設定預設郵件伺服器,同時增加郵件非同步執行緒池設定
  mail:
    username: XXXXXXXXXXX
    password: XXXXXXXXXX
    default-encoding: UTF-8
    host: smtp.mxhichina.com
    port: 25
    protocol: smtp
    properties:
      mail:
        smtp:
          auth: true
          ssl:
            enable: false
  # 非同步傳送郵件,核心執行緒池數設定
  mail-task:
    execution:
      pool:
        core-size: 5
        max-size: 10
        queue-capacity: 5
        keep-alive: 60
      thread-name-prefix: mail-send-task-
4、在郵件渠道設定介面進行郵件傳送測試,有兩種測試方式,一種是選擇指定渠道進行傳送,另外一種是選擇系統預設渠道進行郵件傳送。傳送完成後檢視郵件紀錄檔模組,檢查是否有郵件傳送成功的記錄。
  • 選擇需要測試的郵箱伺服器

  • 填寫測試郵箱傳送內容

  • 檢視郵箱傳送紀錄檔

原始碼地址:

Gitee: https://gitee.com/wmz1930/GitEgg

GitHub: https://github.com/wmz1930/GitEgg