官网文档:39. Sending Email (spring.io)。
Sending Email
Spring框架提供了JavaMailSender实例,用于发送邮件。
如果SpringBoot项目中包含了相关的启动器,那么就会自动装配一个Bean实例到项目中。
在SpringBoot项目中引入如下Email启动器,
<!-- spring-boot-starter-mail-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
核心接口和类
核心接口:MailSender
org.springframework.mail是邮件发送依赖的顶级包,其中提供了邮件发送的核心接口MailSender,该接口提供了两个方法,分别用于指定逐个邮件发送、批量邮件发送。
package org.springframework.mail;
/**
* This interface defines a strategy for sending simple mails. Can be
* implemented for a variety of mailing systems due to the simple requirements.
* For richer functionality like MIME messages, consider JavaMailSender.
*
* <p>Allows for easy testing of clients, as it does not depend on JavaMail's
* infrastructure classes: no mocking of JavaMail Session or Transport necessary.
*/
public interface MailSender {
/**
* Send the given simple mail message.-发送简单邮件
* @param simpleMessage the message to send
* @throws MailParseException in case of failure when parsing the message
* @throws MailAuthenticationException in case of authentication failure
* @throws MailSendException in case of failure when sending the message
*/
void send(SimpleMailMessage simpleMessage) throws MailException;
/**
* Send the given array of simple mail messages in batch.-批量发送一组简单邮件
* @param simpleMessages the messages to send
* @throws MailParseException in case of failure when parsing a message
* @throws MailAuthenticationException in case of authentication failure
* @throws MailSendException in case of failure when sending a message
*/
void send(SimpleMailMessage... simpleMessages) throws MailException;
}
可以看到,其实JavaMailSender接口也作为它的子接口存在,并且内置了一个实现类JavaMailSenderImpl可以供我们直接使用。
邮件消息接口:MailMessage
邮件消息接口:规定一封电子邮件对应的组成元素,内置了简单邮件消息和媒体邮件消息两个实现子类。
package org.springframework.mail;
import java.util.Date;
/**
* This is a common interface for mail messages, allowing a user to set key
* values required in assembling a mail message, without needing to know if
* the underlying message is a simple text message or a more sophisticated
* MIME message.
*
* <p>Implemented by both SimpleMailMessage and MimeMessageHelper,
* to let message population code interact with a simple message or a
* MIME message through a common interface.
*/
public interface MailMessage {
void setFrom(String from) throws MailParseException;
void setReplyTo(String replyTo) throws MailParseException;
void setTo(String to) throws MailParseException;
void setTo(String... to) throws MailParseException;
void setCc(String cc) throws MailParseException;
void setCc(String... cc) throws MailParseException;
void setBcc(String bcc) throws MailParseException;
void setBcc(String... bcc) throws MailParseException;
void setSentDate(Date sentDate) throws MailParseException;
void setSubject(String subject) throws MailParseException;
void setText(String text) throws MailParseException;
}
简单邮件对象:SimpleMailMessage
可以用来囊括简单邮件信息的类是SimpleMailMessage类,邮件主体内容以简单文本形式为主。
A simple value object that encapsulates the properties of a simple mail such as
from
andto
(plus many others) is theSimpleMailMessage
class.
该类的源码如下,
package org.springframework.mail;
import java.io.Serializable;
import java.util.Date;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Models a simple mail message, including data such as the from, to, cc, subject,
* and text fields.
*
* <p>Consider {@code JavaMailSender} and JavaMail {@code MimeMessages} for creating
* more sophisticated messages, for example messages with attachments, special
* character encodings, or personal names that accompany mail addresses.
*
* @author Dmitriy Kopylenko
* @author Juergen Hoeller
* @since 10.09.2003
* @see MailSender
* @see org.springframework.mail.javamail.JavaMailSender
* @see org.springframework.mail.javamail.MimeMessagePreparator
* @see org.springframework.mail.javamail.MimeMessageHelper
* @see org.springframework.mail.javamail.MimeMailMessage
*/
@SuppressWarnings("serial")
public class SimpleMailMessage implements MailMessage, Serializable {
@Nullable
private String from;
@Nullable
private String replyTo;
@Nullable
private String[] to;
@Nullable
private String[] cc;
@Nullable
private String[] bcc;
@Nullable
private Date sentDate;
@Nullable
private String subject;
@Nullable
private String text;
/**
* Create a new {@code SimpleMailMessage}.
*/
public SimpleMailMessage() {
}
/**
* Copy constructor for creating a new {@code SimpleMailMessage} from the state
* of an existing {@code SimpleMailMessage} instance.
*/
public SimpleMailMessage(SimpleMailMessage original) {
Assert.notNull(original, "'original' message argument must not be null");
this.from = original.getFrom();
this.replyTo = original.getReplyTo();
this.to = copyOrNull(original.getTo());
this.cc = copyOrNull(original.getCc());
this.bcc = copyOrNull(original.getBcc());
this.sentDate = original.getSentDate();
this.subject = original.getSubject();
this.text = original.getText();
}
@Override
public void setFrom(String from) {
this.from = from;
}
@Nullable
public String getFrom() {
return this.from;
}
@Override
public void setReplyTo(String replyTo) {
this.replyTo = replyTo;
}
@Nullable
public String getReplyTo() {
return this.replyTo;
}
@Override
public void setTo(String to) {
this.to = new String[] {to};
}
@Override
public void setTo(String... to) {
this.to = to;
}
@Nullable
public String[] getTo() {
return this.to;
}
@Override
public void setCc(String cc) {
this.cc = new String[] {cc};
}
@Override
public void setCc(String... cc) {
this.cc = cc;
}
@Nullable
public String[] getCc() {
return this.cc;
}
@Override
public void setBcc(String bcc) {
this.bcc = new String[] {bcc};
}
@Override
public void setBcc(String... bcc) {
this.bcc = bcc;
}
@Nullable
public String[] getBcc() {
return this.bcc;
}
@Override
public void setSentDate(Date sentDate) {
this.sentDate = sentDate;
}
@Nullable
public Date getSentDate() {
return this.sentDate;
}
@Override
public void setSubject(String subject) {
this.subject = subject;
}
@Nullable
public String getSubject() {
return this.subject;
}
@Override
public void setText(String text) {
this.text = text;
}
@Nullable
public String getText() {
return this.text;
}
/**
* Copy the contents of this message to the given target message.
* @param target the {@code MailMessage} to copy to
*/
public void copyTo(MailMessage target) {
Assert.notNull(target, "'target' MailMessage must not be null");
if (getFrom() != null) {
target.setFrom(getFrom());
}
if (getReplyTo() != null) {
target.setReplyTo(getReplyTo());
}
if (getTo() != null) {
target.setTo(copy(getTo()));
}
if (getCc() != null) {
target.setCc(copy(getCc()));
}
if (getBcc() != null) {
target.setBcc(copy(getBcc()));
}
if (getSentDate() != null) {
target.setSentDate(getSentDate());
}
if (getSubject() != null) {
target.setSubject(getSubject());
}
if (getText() != null) {
target.setText(getText());
}
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (!(other instanceof SimpleMailMessage)) {
return false;
}
SimpleMailMessage otherMessage = (SimpleMailMessage) other;
return (ObjectUtils.nullSafeEquals(this.from, otherMessage.from) &&
ObjectUtils.nullSafeEquals(this.replyTo, otherMessage.replyTo) &&
ObjectUtils.nullSafeEquals(this.to, otherMessage.to) &&
ObjectUtils.nullSafeEquals(this.cc, otherMessage.cc) &&
ObjectUtils.nullSafeEquals(this.bcc, otherMessage.bcc) &&
ObjectUtils.nullSafeEquals(this.sentDate, otherMessage.sentDate) &&
ObjectUtils.nullSafeEquals(this.subject, otherMessage.subject) &&
ObjectUtils.nullSafeEquals(this.text, otherMessage.text));
}
@Override
public int hashCode() {
int hashCode = ObjectUtils.nullSafeHashCode(this.from);
hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.replyTo);
hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.to);
hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.cc);
hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.bcc);
hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.sentDate);
hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(this.subject);
return hashCode;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("SimpleMailMessage: ");
sb.append("from=").append(this.from).append("; ");
sb.append("replyTo=").append(this.replyTo).append("; ");
sb.append("to=").append(StringUtils.arrayToCommaDelimitedString(this.to)).append("; ");
sb.append("cc=").append(StringUtils.arrayToCommaDelimitedString(this.cc)).append("; ");
sb.append("bcc=").append(StringUtils.arrayToCommaDelimitedString(this.bcc)).append("; ");
sb.append("sentDate=").append(this.sentDate).append("; ");
sb.append("subject=").append(this.subject).append("; ");
sb.append("text=").append(this.text);
return sb.toString();
}
@Nullable
private static String[] copyOrNull(@Nullable String[] state) {
if (state == null) {
return null;
}
return copy(state);
}
private static String[] copy(String[] state) {
return state.clone();
}
}
媒体邮件对象:MimeMailMessage
MemeMailMessage表示媒体邮件对象,可以实现自定义邮件模板的动态填充和邮件发送。
/*
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.mail.javamail;
import java.util.Date;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import org.springframework.mail.MailMessage;
import org.springframework.mail.MailParseException;
/**
* Implementation of the MailMessage interface for a JavaMail MIME message,
* to let message population code interact with a simple message or a MIME
* message through a common interface.
*
* <p>Uses a MimeMessageHelper underneath. Can either be created with a
* MimeMessageHelper instance or with a JavaMail MimeMessage instance.
*
* @author Juergen Hoeller
* @since 1.1.5
* @see MimeMessageHelper
* @see javax.mail.internet.MimeMessage
*/
public class MimeMailMessage implements MailMessage {
private final MimeMessageHelper helper;
/**
* Create a new MimeMailMessage based on the given MimeMessageHelper.
* @param mimeMessageHelper the MimeMessageHelper
*/
public MimeMailMessage(MimeMessageHelper mimeMessageHelper) {
this.helper = mimeMessageHelper;
}
/**
* Create a new MimeMailMessage based on the given JavaMail MimeMessage.
* @param mimeMessage the JavaMail MimeMessage
*/
public MimeMailMessage(MimeMessage mimeMessage) {
this.helper = new MimeMessageHelper(mimeMessage);
}
/**
* Return the MimeMessageHelper that this MimeMailMessage is based on.
*/
public final MimeMessageHelper getMimeMessageHelper() {
return this.helper;
}
/**
* Return the JavaMail MimeMessage that this MimeMailMessage is based on.
*/
public final MimeMessage getMimeMessage() {
return this.helper.getMimeMessage();
}
@Override
public void setFrom(String from) throws MailParseException {
try {
this.helper.setFrom(from);
}
catch (MessagingException ex) {
throw new MailParseException(ex);
}
}
@Override
public void setReplyTo(String replyTo) throws MailParseException {
try {
this.helper.setReplyTo(replyTo);
}
catch (MessagingException ex) {
throw new MailParseException(ex);
}
}
@Override
public void setTo(String to) throws MailParseException {
try {
this.helper.setTo(to);
}
catch (MessagingException ex) {
throw new MailParseException(ex);
}
}
@Override
public void setTo(String... to) throws MailParseException {
try {
this.helper.setTo(to);
}
catch (MessagingException ex) {
throw new MailParseException(ex);
}
}
@Override
public void setCc(String cc) throws MailParseException {
try {
this.helper.setCc(cc);
}
catch (MessagingException ex) {
throw new MailParseException(ex);
}
}
@Override
public void setCc(String... cc) throws MailParseException {
try {
this.helper.setCc(cc);
}
catch (MessagingException ex) {
throw new MailParseException(ex);
}
}
@Override
public void setBcc(String bcc) throws MailParseException {
try {
this.helper.setBcc(bcc);
}
catch (MessagingException ex) {
throw new MailParseException(ex);
}
}
@Override
public void setBcc(String... bcc) throws MailParseException {
try {
this.helper.setBcc(bcc);
}
catch (MessagingException ex) {
throw new MailParseException(ex);
}
}
@Override
public void setSentDate(Date sentDate) throws MailParseException {
try {
this.helper.setSentDate(sentDate);
}
catch (MessagingException ex) {
throw new MailParseException(ex);
}
}
@Override
public void setSubject(String subject) throws MailParseException {
try {
this.helper.setSubject(subject);
}
catch (MessagingException ex) {
throw new MailParseException(ex);
}
}
@Override
public void setText(String text) throws MailParseException {
try {
this.helper.setText(text);
}
catch (MessagingException ex) {
throw new MailParseException(ex);
}
}
}
异常接口:MailException
This package also contains a hierarchy of checked exceptions that provide a higher level of abstraction over the lower level mail system exceptions, with the root exception being MailException.
Mail是邮件发送的异常处理根接口,源码如下,
@SuppressWarnings("serial")
public abstract class MailException extends NestedRuntimeException {
/**
* Constructor for MailException.
* @param msg the detail message
*/
public MailException(String msg) {
super(msg);
}
/**
* Constructor for MailException.
* @param msg the detail message
* @param cause the root cause from the mail API in use
*/
public MailException(@Nullable String msg, @Nullable Throwable cause) {
super(msg, cause);
}
}
当然,它也内置了一系列子接口,
基本含义如下,
MailSendException:邮件发送失败异常
MailParseException:非法的邮件配置属性
MailPreparationException:邮件模板渲染出错时,会抛出此类异常。
MailAuthenticationException:认证失败异常。
application.yml配置
要使用JavaMailSender实现邮件发送,那么就需要先对其进行配置。通过上述内容的初步了解,基本上可以确定我们要配置的参数就和JavaMailSenderImpl实现子类相关了,其成员属性如下,和SpringBoot官网配置文档给出的配置参数也是一一对应的。
我所做的配置如下,
spring:
mail:
protocol: smtp
host: smtp.qq.com
default-encoding: UTF-8
username: email-account
password: key/password
properties:
mail:
debug: true #开启日志打印
smtp:
auth: true
starttls:
enable: true
required: true
邮件发送
上面了解到两种邮件形式,所以,接下来我们尝试一下,如何发送简单文本邮件,以及自定义模板的邮件。
SimpleMailMessage发送
简单邮件的发送示例代码如下,其中:JavaMailSender 实例即为文章开头部分提到的,由Spring框架自动注入的Bean实例。
@Component
public class EmailSendServiceImpl implements EmailSenderService {
@Autowired
private JavaMailSender mailSender;
@Override
public R sendSimpleEmail(String from, String to, String subject, String text) {
try {
//创建邮件对象
SimpleMailMessage mailMessage = new SimpleMailMessage();
//设置邮件属性
mailMessage.setFrom(from);//发件人
mailMessage.setTo(to);//收件人
mailMessage.setSubject(subject);//主题
mailMessage.setText(text);//文本内容
mailMessage.setSentDate(new Date());//发送日期
//发送邮件
this.mailSender.send(mailMessage);
//返回消息
return R.ok("邮件发送成功!");
} catch (MailException e) {
e.printStackTrace();
return R.fail("邮件发送失败!");
}
}
}
MimeMailMessage发送
为了方便向邮件中写入自定义内容,所以自定义邮件模板需要借助后端的模板引擎来实现,此处我们选择thymeleaf模板引擎。以下为此时的依赖,
<!-- spring-boot-starter-mail-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
thymeleaf配置
如上图所示,thymeleaf模板引擎内置了一套配置规则,最简单的方式就是将其原模原样的写到application.yml配置文件中即可。
以下为我的配置项,
spring:
thymeleaf:
prefix: classpath:/templates/
suffix: .html
mode: HTML
邮件模板定制
邮件模板定制,其实就是写一个前端页面,然后借助thymeleaf模板引擎的表达式语法,实现自定义内容的动态填充即可。
关于Thymeleaf的表达式语法,详情可参阅官网文档:Tutorial: Using Thymeleaf
但是说实话,对于简单的文本替换,以下的简单表达式语法基本就能满足了。
关于邮件模板的定制,如果要求特别高的话,可以自己从零开始写;但是,如果要快速实现的话,也可以借助在线生成工具,这里比较推荐:拉易网提供的精美邮件定制服务,里面内置了一些可以直接使用的邮件模板,也可以在此基础上进行个性化定制,零代码自动生成。
最终将定制好的模板下载下来即可。
如何使用邮件模板
那么我们下载下来的邮件模板如何使用呢?
其实和我们的Thymeleaf模板引擎配置相关,毕竟是要将这个模板交给Thymeleaf模板引擎进行值的动态替换和渲染的。
这里对应于thymeleaf的prefix配置项,我的email.html邮件模板就放在templates路径下。
另外,为了实现动态替换文本,例如:我这里想要实现邮件验证码,那么,我就要通过表达式语法,提供一个文本字符串的变量verifyCode,以便于在后面发送邮件时,将这个变量替换为任何我们随机生成的验证码字符串。
示例代码
终于来到代码环节了,但是前面的环节也确实缺一不可。
@Component
public class EmailSendServiceImpl implements EmailSenderService {
@Autowired
private JavaMailSender mailSender;
@Autowired
private TemplateEngine templateEngine;
@Override
public R sendMimeEmail(String from, String to, String subject) {
//create a new JavaMail MimeMessage
MimeMessage mimeMailMessage = this.mailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = null;
try {
/**
* mimeMessage – the mime message to work on
* multipart – whether to create a multipart message that supports alternative texts, inline elements and attachments (corresponds to MULTIPART_MODE_MIXED_RELATED)
*/
mimeMessageHelper = new MimeMessageHelper(mimeMailMessage, true);
//setting basic params
mimeMessageHelper.setFrom(from);
mimeMessageHelper.setTo(to);
mimeMessageHelper.setSubject(subject);
//create html-text based on thymeleaf template
Context context = new Context();
context.setVariable("verifyCode","456935");
String process = templateEngine.process("email", context);
//设置邮件内容
/**
* process:the text for the message
* html:whether to apply content type "text/html" for an HTML mail, using default content type ("text/plain") else
*/
mimeMessageHelper.setText(process,true);
//发送邮件
this.mailSender.send(mimeMailMessage);
return R.ok("邮件发送成功!");
} catch (MessagingException e) {
e.printStackTrace();
return R.fail("邮件发送失败!");
}
}
}
* 模板变量替换:Context探究
先看一下源码注释,
IContext接口的非web(场景)实现,适用于非web场景。
我们继续查看Context父类AbstractContext实现的IContext接口,可以看到,有一个我们刚才用于动态替换thymeleaf模板变量的方法,
注意到它还提供了一个重载方法,可以实现多个模板变量值的设置,都是以key-value键值对的形式出现。