邮箱服务
邮箱服务的一些概念
- 为什么要用邮箱服务
互联网发展到现在,大家都知道发送邮件应该是网站的必备功能之一:用户注册发送邮箱验证、忘记密码、监控提醒以及发送营销信息等,使用邮箱服务也可以推送一些信息给用户。 - 什么是SMTP
SMTP全称为Simple Mail Transfer Protocol(简单邮件传输协议),它是一组用于从源地址到目的地址传输邮件的规范,通过它来控制邮件的中转方式。SMTP认证要求必须提供账号和密码才能登陆服务器,其设计目的在于避免用户受到垃圾邮件的侵扰。 - 什么是POP3
POP3全称为Post Office Protocol 3(邮局协议),POP3支持客户端远程管理服务器端的邮件。POP3常用于“离线”邮件处理,即允许客户端下载服务器邮件,然后服务器上的邮件将会被删除。目前很多POP3的邮件服务器只提供下载邮件功能,服务器本身并不删除邮件,这种属于改进版的POP3协议。 - 什么是IMAP
IMAP全称为Internet Message Access Protocol(互联网邮件访问协议),IMAP允许从邮件服务器上获取邮件的信息、下载邮件等。IMAP与POP类似,都是一种邮件获取协议。 - IMAP和POP3协议有什么不同呢?
两者最大的区别在于,IMAP允许双向通信,即在客户端的操作会反馈到服务器上,例如在客户端收取邮件、标记已读等操作,服务器会跟着同步这些操作。而对于POP协议虽然也允许客户端下载服务器邮件,但是在客户端的操作并不会同步到服务器上面的,例如在客户端收取或标记已读邮件,服务器不会同步这些操作。
开启邮件服务
示范两个邮箱
1. 登陆网易邮箱163,在设置中打开并勾选POP3/SMTP/IMAP服务,然后会得到一个授权码,这个邮箱和授权码将用作登陆认证。
2. 使用qq邮箱,开启POP3/SMTP服务服务
springboot优雅的配置和使用邮箱服务
项目目录如下
1. 建立邮箱服务数据库
为什么要建立数据库呢?
(当然是为了之后写项目时邮箱服务的扩展性)
create database heartemail; use heartemail; -- ---------------------------- -- Table structure for email_config -- ---------------------------- DROP TABLE IF EXISTS `email_config`; CREATE TABLE `email_config` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', `from_user` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '收件人', `host` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮件服务器SMTP地址', `pass` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码', `port` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '端口', `user` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '发件者用户名', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '邮箱配置' ROW_FORMAT = Compact; -- ---------------------------- -- Table structure for email_content -- ---------------------------- DROP TABLE IF EXISTS `email_content`; CREATE TABLE `email_content` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号', `fromuser` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '发送者', `touser` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '接收者', `mail_title` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '邮件标题', `mail_content` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '邮件内容', `createtime` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '发送时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
2. pom.xml 项目依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!--邮件依赖--> <dependency> <groupId>javax.mail</groupId> <artifactId>mail</artifactId> <version>1.4.7</version> </dependency> <!--工具包--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.3.4</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.6</version> </dependency> <!--单元测试--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.3</version> </dependency> </dependencies>
3. application.yml(配置文件)
spring: freemarker: suffix: .ftl datasource: driver-class-name: com.mysql.jdbc.Driver username: root password: 123456 url: jdbc:mysql://localhost:3306/heartemail?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&zeroDateTimeBehavior=convertToNull #配置 Jpa jpa: properties: hibernate: ddl-auto: none dialect: org.hibernate.dialect.MySQL5InnoDBDialect open-in-view: true
4. EmailConfig.java、EmailContent.java、EmailVO.java(实体类)
@Entity @Data @Table(name = "email_config") public class EmailConfig implements Serializable { @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; /*邮件服务器SMTP地址*/ @Column(name = "host") private String host; /*邮件服务器 SMTP 端口*/ @Column(name = "port") private String port; /*发件者用户名*/ @Column(name = "user") private String user; /*密码*/ @Column(name = "pass") private String pass; /*收件人*/ @Column(name = "from_user") private String fromUser; }
@Entity @Data @Table(name = "email_content") public class EmailContent { @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; /*邮件发送者*/ @Column(name = "fromuser") private String fromuser; /*邮件接收者*/ @Column(name = "touser") private String touser; /*邮件标题*/ @Column(name = "mail_title") private String mailTitle; /*邮件内容*/ @Column(name = "mail_content") private String mailContent; /*邮件发送时间*/ @Column(name = "createtime") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private String createtime; }
@Data @NoArgsConstructor public class EmailVO { /** * 收件人,支持多个收件人 */ private List<String> tos; /*邮箱标题*/ private String subject; /*邮箱内容*/ private String content; }
5. EncryptUtils.java(加密工具类)
package cn.kt.heartmail.utils; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.DESKeySpec; import javax.crypto.spec.IvParameterSpec; import java.nio.charset.StandardCharsets; /** * Created by tao. * Date: 2021/7/21 10:40 * 描述: */ public class EncryptUtils { private static final String STR_PARAM = "Passw0rd"; private static Cipher cipher; private static final IvParameterSpec IV = new IvParameterSpec(STR_PARAM.getBytes(StandardCharsets.UTF_8)); private static DESKeySpec getDesKeySpec(String source) throws Exception { if (source == null || source.length() == 0) { return null; } cipher = Cipher.getInstance("DES/CBC/PKCS5Padding"); String strKey = "Passw0rd"; return new DESKeySpec(strKey.getBytes(StandardCharsets.UTF_8)); } /** * 对称加密 */ public static String desEncrypt(String source) throws Exception { DESKeySpec desKeySpec = getDesKeySpec(source); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); SecretKey secretKey = keyFactory.generateSecret(desKeySpec); cipher.init(Cipher.ENCRYPT_MODE, secretKey, IV); return byte2hex( cipher.doFinal(source.getBytes(StandardCharsets.UTF_8))).toUpperCase(); } /** * 对称解密 */ public static String desDecrypt(String source) throws Exception { byte[] src = hex2byte(source.getBytes(StandardCharsets.UTF_8)); DESKeySpec desKeySpec = getDesKeySpec(source); SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES"); SecretKey secretKey = keyFactory.generateSecret(desKeySpec); cipher.init(Cipher.DECRYPT_MODE, secretKey, IV); byte[] retByte = cipher.doFinal(src); return new String(retByte); } private static String byte2hex(byte[] inStr) { String stmp; StringBuilder out = new StringBuilder(inStr.length * 2); for (byte b : inStr) { stmp = Integer.toHexString(b & 0xFF); if (stmp.length() == 1) { // 如果是0至F的单位字符串,则添加0 out.append("0").append(stmp); } else { out.append(stmp); } } return out.toString(); } private static byte[] hex2byte(byte[] b) { int size = 2; if ((b.length % size) != 0) { throw new IllegalArgumentException("长度不是偶数"); } byte[] b2 = new byte[b.length / 2]; for (int n = 0; n < b.length; n += size) { String item = new String(b, n, 2); b2[n / 2] = (byte) Integer.parseInt(item, 16); } return b2; } }
6. EmailRepository.java(邮箱服务dao层)
package cn.kt.heartmail.repository; import cn.kt.heartmail.domian.EmailConfig; import org.springframework.data.jpa.repository.JpaRepository; /** * Created by tao. * Date: 2021/7/21 10:39 * 描述: */ public interface EmailRepository extends JpaRepository<EmailConfig,Long> { }
- EmailService.java、EmailServiceImpl.java(邮箱服务service层,邮箱服务的核心代码)
package cn.kt.heartmail.service; import cn.kt.heartmail.domian.EmailConfig; import cn.kt.heartmail.domian.vo.EmailVO; /** * Created by tao. * Date: 2021/7/21 10:35 * 描述: */ public interface EmailService { /** * 更新邮件配置 * * @param emailConfig 邮箱配置 * @param old / * @return / * @throws Exception / */ EmailConfig config(EmailConfig emailConfig, EmailConfig old) throws Exception; /** * 查询配置 * * @return EmailConfig 邮件配置 */ EmailConfig find(); /** * 发送邮件 * * @param emailVo 邮件发送的内容 * @param emailConfig 邮件配置 * @throws Exception / */ void send(EmailVO emailVo, EmailConfig emailConfig); }
package cn.kt.heartmail.service.impl; import cn.hutool.extra.mail.Mail; import cn.hutool.extra.mail.MailAccount; import cn.kt.heartmail.domian.EmailConfig; import cn.kt.heartmail.domian.vo.EmailVO; import cn.kt.heartmail.repository.EmailRepository; import cn.kt.heartmail.service.EmailService; import cn.kt.heartmail.utils.EncryptUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Optional; /** * Created by tao. * Date: 2021/7/21 10:37 * 描述: */ @Service @RequiredArgsConstructor @Slf4j public class EmailServiceImpl implements EmailService { private final EmailRepository emailRepository; @Override @Transactional(rollbackFor = Exception.class) public EmailConfig config(EmailConfig emailConfig, EmailConfig old) throws Exception { emailConfig.setId(1L); if (!emailConfig.getPass().equals(old.getPass())) { // 对称加密 emailConfig.setPass(EncryptUtils.desEncrypt(emailConfig.getPass())); } return emailRepository.save(emailConfig); } @Override public EmailConfig find() { Optional<EmailConfig> emailConfig = emailRepository.findById(1L); return emailConfig.orElseGet(EmailConfig::new); } @Override @Transactional(rollbackFor = Exception.class) public void send(EmailVO emailVo, EmailConfig emailConfig) { if (emailConfig == null) { log.error("请先配置,再操作"); } // 封装 MailAccount account = new MailAccount(); account.setUser(emailConfig.getUser()); account.setHost(emailConfig.getHost()); account.setPort(Integer.parseInt(emailConfig.getPort())); account.setAuth(true); try { // 对称解密 account.setPass(EncryptUtils.desDecrypt(emailConfig.getPass())); } catch (Exception e) { log.error(e.getMessage()); } account.setFrom(emailConfig.getUser() + "<" + emailConfig.getFromUser() + ">"); // ssl方式发送 account.setSslEnable(true); // 使用STARTTLS安全连接 account.setStarttlsEnable(true); String content = emailVo.getContent(); // 发送 try { int size = emailVo.getTos().size(); Mail.create(account) .setTos(emailVo.getTos().toArray(new String[size])) .setTitle(emailVo.getSubject()) .setContent(content) .setHtml(true) //关闭session .setUseGlobalSession(false) .send(); } catch (Exception e) { log.error(e.getMessage()); } } }
邮箱服务测试
接下来可以进行邮箱服务测试了
稍微看一下代码,可以发现在逻辑代码中存数据库的邮箱配置只做了一条数据,只是为了这次的测试方便,如果在实际场景的有需要配置多个邮箱服务的邮箱账号,可以随时自己做一下处理,扩展性很高。
1. 添加邮箱服务数据
由于数据库中数据是加密的,所以需要自己写一个测试代码对数据库添加数据
@Autowired private EmailService emailService; @Test void config() throws Exception { EmailConfig emailConfig = new EmailConfig(); //EmailConfig emailConfig = emailService.find(); //选择qq邮箱服务 emailConfig.setHost("smtp.qq.com"); //发件者用户名 qq邮箱 emailConfig.setUser("***@qq.com"); //邮箱服务授权码 emailConfig.setPass("zjtyma*****jdfc"); //收件人 emailConfig.setFromUser("***@qq.com"); emailService.config(emailConfig, emailConfig); }
生成一条数据后
2. 可以进行发送邮件的测试
代码如下:
@Test void send() { EmailConfig emailConfig = emailService.find(); EmailVO emailVO = new EmailVO(); ArrayList<String> list = new ArrayList<>(); list.add("kt1205529635@aliyun.com"); emailVO.setTos(list); emailVO.setContent("我是测试邮件内容"); emailVO.setSubject("Hello,Email"); emailService.send(emailVO, emailConfig); }
发送邮件成功后
结果如下
3. 自定义邮件内容模板
邮件内容模板我选择freemarket,通过自己的测试,只要是静态的html样式,文字,图片在邮件中都可以显示出来(动态获取的就无法渲染)
代码如下:
在 resources/template 目录下创建code.ftl,内容如下
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <style> @page { margin: 0; } </style> </head> <body style="margin: 0px; padding: 0px; font: 100% SimSun, Microsoft YaHei, Times New Roman, Verdana, Arial, Helvetica, sans-serif; color: #000;"> <div style="height: auto; width: 820px; min-width: 820px; margin: 0 auto; margin-top: 20px; border: 1px solid #eee;"> <div style="padding: 10px;padding-bottom: 0px;"> <p style="margin-bottom: 10px;padding-bottom: 0px;">尊敬的用户,您好:</p> <p style="text-indent: 2em; margin-bottom: 10px;">您正在申请邮箱验证,您的验证码为:</p> <p style="text-align: center; font-family: Times New Roman; font-size: 22px; color: #C60024; padding: 20px 0px; margin-bottom: 10px; font-weight: bold; background: #ebebeb;">${code}</p> <div class="foot-hr hr" style="margin: 0 auto; z-index: 111; width: 800px; margin-top: 30px; border-top: 1px solid #DA251D;"> </div> <div style="text-align: center; font-size: 12px; padding: 20px 0px; font-family: Microsoft YaHei;"> Copyright ©${.now?string["yyyy年MM月dd日"]} 如我西沉 All Rights Reserved. </div> </div> </div> </body> </html>
在测试代码中进行读取模板文件,动态传值验证码 code,并且进行渲染。
@Test void sendCCode() { TemplateEngine engine = TemplateUtil.createEngine(new TemplateConfig("template", TemplateConfig.ResourceMode.CLASSPATH)); Template template = engine.getTemplate("/code.ftl"); String code = "888888"; EmailConfig emailConfig = emailService.find(); EmailVO emailVO = new EmailVO(); ArrayList<String> list = new ArrayList<>(); list.add("kt1205529635@aliyun.com"); emailVO.setTos(list); emailVO.setContent(template.render(Dict.create().set("code", code))); emailVO.setSubject("如我西沉的验证码"); emailService.send(emailVO, emailConfig); }
结果如下
总结
其实这次使用邮箱服务最主要的是使用了 hutool 工具包和 mail 依赖项,使得我们只需要简单的配置一下就可以做到发邮件得到功能。但为什么需要写这么繁琐的代码呢?原因是这样维护和开发起来很方便,如果需要多个邮箱账号服务,收件人是多个,使用起来根本不需要改什么代码,这就是封装的好处,还有一点就是使用了数据库,如果需要做一个很大的邮箱服务系统,这套代码同适用。
细心的老铁会发现我们建立了两张表,为什么只使用了一张 email_config 邮箱服务的配置表,另外一张表 email_content 我想用来存每次发送邮件的邮件信息的,可以在每次发送邮件前,将本次要发送的邮件内容做一次数据库存储,可以数据备份,方便之后的数据可视化,主要还是为了扩展性。(提示一下:可以做一个给心爱之人的每日邮箱哦 ^ _ ^)
大佬,为什么我的数据库中PORT获取不到呢
可以加我联系方式,具体看一下哦