在日常生活中,邮件已经被聊天软件、短信等更便捷的信息传送方式代替。但在日常工作中,我们的重要的信息通知等非常有必要去归档追溯,那么邮件就是不可或缺的信息传送渠道。对于我们工作中经常用到的系统,里面也基本都集成了邮件发送功能。SpringBoot提供了基于JavaMail的starter,我们只要按照官方的说明配置邮件服务器信息,即可使我们的系统拥有发送电子邮件的功能。但是,在我们GitEgg开发框架的实际业务开发过程中,有两个问题需要解决:一个是SpringBoot邮箱服务器的配置是配置在配置文件中的,不支持灵活的界面配置。另外一个是我们的开发框架需要支持多租户,那么此时需要对SpringBoot提供的邮件发送功能进行扩展,以满足我们的需求。
那么,基于以上需求和问题,我们对GitEgg框架进行扩展,增加以下功能:
扩展系统配置:将邮箱服务器的配置信息持久化到数据库、Redis缓存,和配置文件一起使用,制定读取优先级。
扩展多租户配置:如果系统开启了多租户功能,那么在邮件发送时,首先读取租户的当前配置,如果没有配置,那么在读取系统配置。
自有选择服务器:用户可在系统界面上选择指定的邮箱服务器进行邮件发送。
提供邮件发送模板:用户可选择预先制定的邮件模板进行发送特定邮件。
增加发送数量、频率限制:增加配置,限制模板邮件的发送数量和频率。
保存邮件发送记录:不一定把所有附件都保存,只需保存邮件发送关键信息,如果需要保存所有附件等需要自己扩展。
同一个租户可以配置多个电子邮件服务器,但只可以设置一个服务器为启用状态。默认情况下,系统通知类的功能只使用启用状态的服务器进行邮件发送。在有定制化需求的情况下,比如从页面直接指定某个服务器进行邮件发送,那么提供可以选择的接口,指定某个服务器进行邮件发送。
复制
<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>
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
复制
@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; }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
复制
@Datapublic class GitEggJavaMailSenderImpl extends JavaMailSenderImpl {/** * 配置id */private Long id;/** * 租户id */private Long tenantId;/** * 渠道编码 */private String channelCode;/** * 配置的md5值 */private String md5; }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
复制
@Slf4jpublic 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())) {// 如果没有配置信息,那么直接返回系统默认配置的mailSenderjavaMailSender = 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; } }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
复制
@Configurationpublic 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; } }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
复制
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; } }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
复制
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:"; }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
复制
@Slf4j@Configuration@RequiredArgsConstructor(onConstructor_ = @Autowired)public class GitEggJavaMailConfiguration {private final JavaMailSenderImpl javaMailSenderImpl; private final RedisTemplate redisTemplate;/** * 是否开启租户模式 */@Value("${tenant.enable}")private Boolean enable; @Beanpublic JavaMailSenderFactory gitEggAuthRequestFactory() {return new JavaMailSenderFactory(redisTemplate, javaMailSenderImpl, enable); } }
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
邮箱服务器的配置,实际就是不同邮箱渠道的配置,这里我们将表和字段设计好,然后使用GitEgg自带代码生成器,生成业务的CRUD代码即可。
复制
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;
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
邮件模板数据库表设计:
复制
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;
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
邮件日志数据库表设计:
复制
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;
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
上面的基本功能开发完成之后,那么我们就需要进行测试,这里选择两种类型的邮箱进行测试,一种是QQ邮箱,还有一种是阿里云企业邮箱。
QQ邮箱在配置的时候不能使用QQ的登录密码,需要单独设置QQ邮箱的授权码,下面是操作步骤:
开通qq邮箱的smtp功能
经过一系列的验证之后,会获取到一个授权码:
系统中配置QQ邮箱相关信息。
阿里云企业邮箱的配置相比较而言就简单一些,配置的密码就是企业邮箱登录的密码。
账户设置,开启POP3/SMTP和IMAP/SMTP服务。
系统中配置阿里云企业邮箱相关信息。
复制
mail:username: XXXXXXXXXXXpassword: XXXXXXXXXXdefault-encoding: UTF-8host: smtp.mxhichina.comport: 25protocol: smtpproperties: mail:smtp: auth: true ssl:enable: false
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
复制
# 异步发送邮件,核心线程池数配置 mail-task:execution: pool:core-size: 5max-size: 10queue-capacity: 5keep-alive: 60 thread-name-prefix: mail-send-task-
1.
2.
3.
4.
5.
6.
7.
8.
9.
选择需要测试的邮箱服务器
填写测试邮箱发送的内容
查看邮箱发送日志