一、实战
1 邮箱准备工作,下面以qq邮箱做示例,其他邮箱同理
1.1 登录邮箱进入找到设置
1.2 找到邮箱中的POP3/IMAP/SMTP服务设置,并开启POP3/SMTP,IMAP/SMTP服务
此时,邮箱设置已经完成。
1.3 最后查看qq邮箱POP3和SMTP服务器
注:如需了解POP3、SMTP、IMAP协议,可以查看第三部分。
2 代码实战
2.1 搭建spring boot项目,引入spring-boot-starter-mail依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
2.2 配置application.properties中的邮件配置
#邮箱服务器地址
spring.mail.host=smtp.qq.com
#用户名
spring.mail.username=XXXXX@qq.com
#密码
spring.mail.password=XXXXXXX
# hudmmejunplchdea
spring.mail.default-encoding=UTF-8
spring.mail.port=587
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
#以谁来发送邮件
mail.fromMail.addr=XXXXXX@qq.com
2.3 编写发送邮件服务及其实现类(通过spring boot mail 提供的JavaMailSender)
package com.ganhuojun.demo.service;
import java.io.ByteArrayOutputStream;
public interface MailService {
/**
* 发送邮件
* @param to 目的地
* @param subject 主题
* @param content 内容
* @param os 附件
* @param attachmentFilename 附件名
* @throws Exception
*/
public void sendMail(String to, String subject, String content, ByteArrayOutputStream os, String attachmentFilename, Boolean isHtml) throws Exception;
public void sendMail(String[] to, String subject, String content, ByteArrayOutputStream os, String attachmentFilename, Boolean isHtml) throws Exception;
}
实现类
package com.ganhuojun.demo.service.impl;
import com.ganhuojun.demo.service.MailService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.InputStreamSource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import javax.mail.internet.MimeMessage;
import java.io.ByteArrayOutputStream;
@Component
public class MailServiceImpl implements MailService {
private static Logger logger = LoggerFactory.getLogger(MailServiceImpl.class);
@Autowired
private JavaMailSender mailSender;
@Value("${mail.fromMail.addr}")
private String from;
@Override
public void sendMail(String to, String subject, String content, ByteArrayOutputStream os, String attachmentFilename, Boolean isHtml) throws Exception {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, isHtml);
if (null != os) {
//附件
InputStreamSource inputStream = new ByteArrayResource(os.toByteArray());
helper.addAttachment(attachmentFilename, inputStream);
}
mailSender.send(message);
logger.info("简单邮件已经发送。");
} catch (Exception e) {
logger.error("发送简单邮件时发生异常!", e);
throw new Exception("发送简单邮件时发生异常!",e);
}
}
@Override
public void sendMail(String[] to, String subject, String content, ByteArrayOutputStream os, String attachmentFilename, Boolean isHtml) throws Exception {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, isHtml);
if (null != os) {
//附件
InputStreamSource inputStream = new ByteArrayResource(os.toByteArray());
helper.addAttachment(attachmentFilename, inputStream);
}
mailSender.send(message);
logger.info("简单邮件已经发送。");
} catch (Exception e) {
logger.error("发送简单邮件时发生异常!", e);
throw new Exception("发送简单邮件时发生异常!",e);
}
}
}
2.4 编写测试用例
package com.ganhuojun.demo.service;
import com.ganhuojun.demo.DemoApplication;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.web.WebAppConfiguration;
@SpringBootTest(classes = DemoApplication.class)
@WebAppConfiguration
public class MailServiceTest {
@Autowired
MailService mailService;
@Test
public void sendMail(){
try {
mailService.sendMail("xxxx@qq.com","标题","邮件内容",null,null,false);
}catch (Exception e){
System.out.println("fail");
}
System.out.println("success");
}
}
2.5 运行会发现已经收到邮件
二、源码解读
很多人都会有个疑问,我们知道spring-boot-starter-mail,但是我不知道从哪去入手源码,下面我们从原理上为大家分析一下。
1 了解spring boot的starter机制
如果大家了解spring boot的starter立刻就知道其运行原理。本文不对starter机制详细介绍,如果有需要了解的可以自行查询。我们简单描述下:
spring boot项目在Application类上都会@SpringBootApplication注解,该注解会自动引入@EnableAutoConfiguration注解,从而开启spring boot的默认配置,@EnableAutoConfiguration在spring-boot-autoconfigure包中。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
由于@EnableAutoConfiguration注解使用了@Import({AutoConfigurationImportSelector.class}),如果查看源码,可以最终查看到其读取了META-INF/spring.factories中的数据
源码如下:
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
if (result != null) {
return result;
} else {
try {
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
LinkedMultiValueMap result = new LinkedMultiValueMap();
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
int var10 = var9.length;
for(int var11 = 0; var11 < var10; ++var11) {
String factoryImplementationName = var9[var11];
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
} catch (IOException var13) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
}
}
}
我们查看spring-boot-autoconfigure包中spring.factories文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
……
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
……
此时豁然开朗,原来我们spring boot mail的入口就是MailSenderAutoConfiguration这个类,下面我们重点分析。
2 邮件配置类MailSenderAutoConfiguration
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({MimeMessage.class, MimeType.class, MailSender.class})
@ConditionalOnMissingBean({MailSender.class})
@Conditional({MailSenderAutoConfiguration.MailSenderCondition.class})
@EnableConfigurationProperties({MailProperties.class})
@Import({MailSenderJndiConfiguration.class, MailSenderPropertiesConfiguration.class})
public class MailSenderAutoConfiguration {
public MailSenderAutoConfiguration() {
}
static class MailSenderCondition extends AnyNestedCondition {
MailSenderCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@ConditionalOnProperty(
prefix = "spring.mail",
name = {"jndi-name"}
)
static class JndiNameProperty {
JndiNameProperty() {
}
}
@ConditionalOnProperty(
prefix = "spring.mail",
name = {"host"}
)
static class HostProperty {
HostProperty() {
}
}
}
}
@ConditionalOnClass,@ConditionalOnMissingBean,@Conditional这些配置均表示MailSenderAutoConfiguration创建的条件(引入spring-boot-starter-mail会满足其条件)
@EnableConfigurationProperties({MailProperties.class})此处即导入mail的配置,我们看下MailProperties.class
@ConfigurationProperties(
prefix = "spring.mail"
)
public class MailProperties {
private static final Charset DEFAULT_CHARSET;
/**
* SMTP server host. For instance, `smtp.example.com`.
*/
private String host;
/**
* SMTP server port.
*/
private Integer port;
/**
* Login user of the SMTP server.
*/
private String username;
/**
* Login password of the SMTP server.
*/
private String password;
/**
* Protocol used by the SMTP server.
*/
private String protocol = "smtp";
/**
* Default MimeMessage encoding.
*/
private Charset defaultEncoding = DEFAULT_CHARSET;
/**
* Additional JavaMail Session properties.
*/
private Map<String, String> properties = new HashMap<>();
/**
* Session JNDI name. When set, takes precedence over other Session settings.
*/
private String jndiName;
// 此处getter和setter省略
}
这里解释了我们实战中的配置文件为什么都是spring.mail开头,属性有哪些,各个字段什么含义,源码中都可以看到。
那我们发送邮件底层使用的接口JavaMailSender,是在哪交给spring管理的呢?
当然是@Import({ MailSenderJndiConfiguration.class, MailSenderPropertiesConfiguration.class })为我们做的,核心如下:
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.mail", name = "host")
class MailSenderPropertiesConfiguration {
@Bean
@ConditionalOnMissingBean(JavaMailSender.class)
JavaMailSenderImpl mailSender(MailProperties properties) {
JavaMailSenderImpl sender = new JavaMailSenderImpl();
applyProperties(properties, sender);
return sender;
}
// 此处忽略其他代码
……
}
从而为我们构建了JavaMailSender
3 了解JavaMailSenderImpl的send方法
public void send(MimeMessage mimeMessage) throws MailException {
this.send(mimeMessage);
}
public void send(MimeMessage... mimeMessages) throws MailException {
this.doSend(mimeMessages, (Object[])null);
}
此处send方法重载,如何理解,第一个send会不会死循环?
答案是肯定的,那我们怎么理解此处这样写的用意?
debug后发现上面send会跳转到下面的send方法中,百思不解。
考虑到源码中可能有注释进行解释,我点击了右上角的download sources
然后,当我们下载一下源码后此处代码会变成,瞬间柳暗花明
@Override
public void send(MimeMessage mimeMessage) throws MailException {
send(new MimeMessage[] {mimeMessage});
}
@Override
public void send(MimeMessage... mimeMessages) throws MailException {
doSend(mimeMessages, null);
}
因此我们得出结论,idea默认查看的class文件和源码还是有所区别,建议查看源码的时候全部download sources
最后发送都是通过doSend方法进行,最终底层通sun公司底层的SMTPTransport进行传输,不再分析,有兴趣的可以咨询研究。
三、POP3、SMTP、IMAP的区别
POP3是Post Office Protocol 3的简称,即邮局协议的第3个版本,它规定怎样将个人计算机连接到Internet的邮件服务器和下载电子邮件的电子协议。它是因特网电子邮件的第一个离线协议标准,POP3允许用户从服务器上把邮件存储到本地主机(即自己的计算机)上,同时删除保存在邮件服务器上的邮件,而POP3服务器则是遵循POP3协议的接收邮件服务器,用来接收电子邮件的。
SMTP是“Simple Mail Transfer Protocol”,即简单邮件传输协议。它是一组用于从源地址到目的地址传输邮件的规范,通过它来控制邮件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。SMTP 服务器就是遵循 SMTP 协议的发送邮件服务器。
IMAP全称是Internet Mail Access Protocol,即交互式邮件存取协议,它是跟POP3类似邮件访问标准协议之一。不同的是,开启了IMAP后,您在电子邮件客户端收取的邮件仍然保留在服务器上,同时在客户端上的操作都会反馈到服务器上,如:删除邮件,标记已读等,服务器上的邮件也会做相应的动作。所以无论从浏览器登录邮箱或者客户端软件登录邮箱,看到的邮件以及状态都是一致的。
简单的来说POP3是收取邮件的,SMTP是发送邮件的,因此第二部分我们实现了发送邮件只需要配置SMTP服务器即可。