發送郵件應該是網站的必備拓展功能之一,注冊驗證、忘記密碼或者是給用戶發送營銷信息。
一、郵件協議
在收發郵件的過程中,需要遵守相關的協議,其中主要有:
- 發送電子郵件的協議:
SMTP; - 接收電子郵件的協議:
POP3和IMAP。
1.1 什么是SMTP?
SMTP全稱為Simple Mail Transfer Protocol(簡單郵件傳輸協議),它是一組用於從源地址到目的地址傳輸郵件的規范,通過它來控制郵件的中轉方式。SMTP認證要求必須提供賬號和密碼才能登陸服務器,其設計目的在於避免用戶受到垃圾郵件的侵擾。
1.2 什么是IMAP?
IMAP全稱為Internet Message Access Protocol(互聯網郵件訪問協議),IMAP允許從郵件服務器上獲取郵件的信息、下載郵件等。IMAP與POP類似,都是一種郵件獲取協議。
1.3 什么是POP3?
POP3全稱為Post Office Protocol 3(郵局協議),POP3支持客戶端遠程管理服務器端的郵件。POP3常用於離線郵件處理,即允許客戶端下載服務器郵件,然后服務器上的郵件將會被刪除。目前很多POP3的郵件服務器只提供下載郵件功能,服務器本身並不刪除郵件,這種屬於改進版的POP3協議。
1.4 IMAP和POP3協議有什么不同呢?
兩者最大的區別在於,IMAP允許雙向通信,即在客戶端的操作會反饋到服務器上,例如在客戶端收取郵件、標記已讀等操作,服務器會跟着同步這些操作。而對於POP協議雖然也允許客戶端下載服務器郵件,但是在客戶端的操作並不會同步到服務器上面的,例如在客戶端收取或標記已讀郵件,服務器不會同步這些操作。
二、初始化配置
2.1 開啟郵件服務
本文僅以
163郵箱為例。
2.2 pom.xml
正常我們會用
JavaMail相關api來寫發送郵件的相關代碼,但現在Spring Boot提供了一套更簡易使用的封裝。
spring-boot-starter-mail:Spring Boot郵件服務;spring-boot-starter-thymeleaf:使用Thymeleaf制作郵件模版。
<!-- test 包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--mail -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!--使用 Thymeleaf 制作郵件模板 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>1.8.4</scope>
</dependency>
2.3 application.yml
spring-boot-starter-mail 的配置由 MailProperties 配置類提供。
針對不同的郵箱的配置略有不同,以下是QQ郵箱和163郵箱的配置。
server:
port: 8081
#spring:
# mail:
# # QQ 郵箱 https://service.mail.qq.com/cgi-bin/help?subtype=1&&no=1001256&&id=28
# host: smtp.qq.com
# # 郵箱賬號
# username: van93@qq.com
# # 郵箱授權碼(不是密碼)
# password: password
# default-encoding: UTF-8
# properties:
# mail:
# smtp:
# auth: true
# starttls:
# enable: true
# required: true
spring:
mail:
# 163 郵箱 http://help.mail.163.com/faqDetail.do?code=d7a5dc8471cd0c0e8b4b8f4f8e49998b374173cfe9171305fa1ce630d7f67ac2cda80145a1742516
host: smtp.163.com
# 郵箱賬號
username: 17098705205@163.com
# 郵箱授權碼(不是密碼)
password: password
default-encoding: UTF-8
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
2.4 郵件信息類
來保存發送郵件時的郵件主題、郵件內容等信息
@Data
public class Mail {
/**
* 郵件id
*/
private String id;
/**
* 郵件發送人
*/
private String sender;
/**
* 郵件接收人 (多個郵箱則用逗號","隔開)
*/
private String receiver;
/**
* 郵件主題
*/
private String subject;
/**
* 郵件內容
*/
private String text;
/**
* 附件/文件地址
*/
private String filePath;
/**
* 附件/文件名稱
*/
private String fileName;
/**
* 是否有附件(默認沒有)
*/
private Boolean isTemplate = false;
/**
* 模版名稱
*/
private String emailTemplateName;
/**
* 模版內容
*/
private Context emailTemplateContext;
}
三、發送郵件的實現
3.1 檢查輸入的郵件配置
校驗郵件收信人、郵件主題和郵件內容這些必填項
private void checkMail(Mail mail) {
if (StringUtils.isEmpty(mail.getReceiver())) {
throw new RuntimeException("郵件收信人不能為空");
}
if (StringUtils.isEmpty(mail.getSubject())) {
throw new RuntimeException("郵件主題不能為空");
}
if (StringUtils.isEmpty(mail.getText()) && null == mail.getEmailTemplateContext()) {
throw new RuntimeException("郵件內容不能為空");
}
}
3.2 將郵件保存到數據庫
發送結束后將郵件保存到數據庫,便於統計和追查郵件問題。
private Mail saveMail(Mail mail) {
// todo 發送成功/失敗將郵件信息同步到數據庫
return mail;
}
3.3 發送郵件
- 發送純文本郵件
public void sendSimpleMail(Mail mail){
checkMail(mail);
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setFrom(sender);
mailMessage.setTo(mail.getReceiver());
mailMessage.setSubject(mail.getSubject());
mailMessage.setText(mail.getText());
mailSender.send(mailMessage);
saveMail(mail);
}
- 發送郵件並攜帶附件
public void sendAttachmentsMail(Mail mail) throws MessagingException {
checkMail(mail);
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setFrom(sender);
helper.setTo(mail.getReceiver());
helper.setSubject(mail.getSubject());
helper.setText(mail.getText());
File file = new File(mail.getFilePath());
helper.addAttachment(file.getName(), file);
mailSender.send(mimeMessage);
saveMail(mail);
}
- 發送模版郵件
public void sendTemplateMail(Mail mail) throws MessagingException {
checkMail(mail);
// templateEngine 替換掉動態參數,生產出最后的html
String emailContent = templateEngine.process(mail.getEmailTemplateName(), mail.getEmailTemplateContext());
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setFrom(sender);
helper.setTo(mail.getReceiver());
helper.setSubject(mail.getSubject());
helper.setText(emailContent, true);
mailSender.send(mimeMessage);
saveMail(mail);
}
四、測試及優化
4.1 單元測試
- 測試附件郵件時,附件放在
static文件夾下; - 測試模版郵件時,模版放在
file文件夾下。
@RunWith(SpringRunner.class)
@SpringBootTest
public class MailServiceTest {
@Resource
MailService mailService;
/**
* 發送純文本郵件
*/
@Test
public void sendSimpleMail() {
Mail mail = new Mail();
// mail.setReceiver("17098705205@163.com");
mail.setReceiver("van93@qq.com");
mail.setSubject("測試簡單郵件");
mail.setText("測試簡單內容");
mailService.sendSimpleMail(mail);
}
/**
* 發送郵件並攜帶附件
*/
@Test
public void sendAttachmentsMail() throws MessagingException {
Mail mail = new Mail();
// mail.setReceiver("17098705205@163.com");
mail.setReceiver("van93@qq.com");
mail.setSubject("測試附件郵件");
mail.setText("附件郵件內容");
mail.setFilePath("file/dusty_blog.jpg");
mailService.sendAttachmentsMail(mail);
}
/**
* 測試模版郵件郵件
*/
@Test
public void sendTemplateMail() throws MessagingException {
Mail mail = new Mail();
// mail.setReceiver("17098705205@163.com");
mail.setReceiver("van93@qq.com");
mail.setSubject("測試模版郵件郵件");
//創建模版正文
Context context = new Context();
// 設置模版需要更換的參數
context.setVariable("verifyCode", "6666");
mail.setEmailTemplateContext(context);
// 模版名稱(模版位置位於templates目錄下)
mail.setEmailTemplateName("emailTemplate");
mailService.sendTemplateMail(mail);
}
}
4.2 優化
因為平時發送郵件還有抄送/密送等需求,這里,封裝一個實體和工具類,便於直接調用郵件服務。
- 郵件信息類
@Data
public class MailDomain {
/**
* 郵件id
*/
private String id;
/**
* 郵件發送人
*/
private String sender;
/**
* 郵件接收人 (多個郵箱則用逗號","隔開)
*/
private String receiver;
/**
* 郵件主題
*/
private String subject;
/**
* 郵件內容
*/
private String text;
/**
* 抄送(多個郵箱則用逗號","隔開)
*/
private String cc;
/**
* 密送(多個郵箱則用逗號","隔開)
*/
private String bcc;
/**
* 附件/文件地址
*/
private String filePath;
/**
* 附件/文件名稱
*/
private String fileName;
/**
* 是否有附件(默認沒有)
*/
private Boolean isTemplate = false;
/**
* 模版名稱
*/
private String emailTemplateName;
/**
* 模版內容
*/
private Context emailTemplateContext;
/**
* 發送時間(可指定未來發送時間)
*/
private Date sentDate;
}
- 郵件工具類
@Component
public class EmailUtil {
@Resource
private JavaMailSender mailSender;
@Resource
TemplateEngine templateEngine;
@Value("${spring.mail.username}")
private String sender;
/**
* 構建復雜郵件信息類
* @param mail
* @throws MessagingException
*/
public void sendMail(MailDomain mail) throws MessagingException {
//true表示支持復雜類型
MimeMessageHelper messageHelper = new MimeMessageHelper(mailSender.createMimeMessage(), true);
//郵件發信人從配置項讀取
mail.setSender(sender);
//郵件發信人
messageHelper.setFrom(mail.getSender());
//郵件收信人
messageHelper.setTo(mail.getReceiver().split(","));
//郵件主題
messageHelper.setSubject(mail.getSubject());
//郵件內容
if (mail.getIsTemplate()) {
// templateEngine 替換掉動態參數,生產出最后的html
String emailContent = templateEngine.process(mail.getEmailTemplateName(), mail.getEmailTemplateContext());
messageHelper.setText(emailContent, true);
}else {
messageHelper.setText(mail.getText());
}
//抄送
if (!StringUtils.isEmpty(mail.getCc())) {
messageHelper.setCc(mail.getCc().split(","));
}
//密送
if (!StringUtils.isEmpty(mail.getBcc())) {
messageHelper.setCc(mail.getBcc().split(","));
}
//添加郵件附件
if (mail.getFilePath() != null) {
File file = new File(mail.getFilePath());
messageHelper.addAttachment(file.getName(), file);
}
//發送時間
if (StringUtils.isEmpty(mail.getSentDate())) {
messageHelper.setSentDate(mail.getSentDate());
}
//正式發送郵件
mailSender.send(messageHelper.getMimeMessage());
}
/**
* 檢測郵件信息類
* @param mail
*/
private void checkMail(MailDomain mail) {
if (StringUtils.isEmpty(mail.getReceiver())) {
throw new RuntimeException("郵件收信人不能為空");
}
if (StringUtils.isEmpty(mail.getSubject())) {
throw new RuntimeException("郵件主題不能為空");
}
if (StringUtils.isEmpty(mail.getText()) && null == mail.getEmailTemplateContext()) {
throw new RuntimeException("郵件內容不能為空");
}
}
/**
* 將郵件保存到數據庫
* @param mail
* @return
*/
private MailDomain saveMail(MailDomain mail) {
// todo 發送成功/失敗將郵件信息同步到數據庫
return mail;
}
}
具體的測試詳見Github 示例代碼,這里就不貼出來了。
五、 總結及延伸
5.1 異步發送
很多時候郵件發送並不是我們主業務必須關注的結果,比如通知類、提醒類的業務可以允許延時或者失敗。這個時候可以采用異步的方式來發送郵件,加快主交易執行速度,在實際項目中可以采用MQ發送郵件相關參數,監聽到消息隊列之后啟動發送郵件。
5.2 發送失敗情況
因為各種原因,總會有郵件發送失敗的情況,比如:郵件發送過於頻繁、網絡異常等。在出現這種情況的時候,我們一般會考慮重新重試發送郵件,會分為以下幾個步驟來實現:
- 接收到發送郵件請求,首先記錄請求並且入庫;
- 調用郵件發送接口發送郵件,並且將發送結果記錄入庫;
- 啟動定時系統掃描時間段內,未發送成功並且重試次數小於
3次的郵件,進行再次發送。
5.3 其他問題
郵件端口問題和附件大小問題。
