十分鐘實現傳送郵件服務
阿新 • • 發佈:2020-03-30
傳送郵件應該是網站的必備拓展功能之一,註冊驗證、忘記密碼或者是給使用者傳送營銷資訊。
## 一、郵件協議
在收發郵件的過程中,需要遵守相關的協議,其中主要有:
1. 傳送電子郵件的協議:`SMTP`;
1. 接收電子郵件的協議:`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 開啟郵件服務
> 本文僅以`QQ`郵箱和`163`郵箱為例。
1. [`QQ`郵箱 開啟郵件服務文件](https://service.mail.qq.com/cgi-bin/help?subtype=1&&no=1001256&&id=28)
1. [`163`郵箱 開啟郵件服務文件](http://help.mail.163.com/faqDetail.do?code=d7a5dc8471cd0c0e8b4b8f4f8e49998b374173cfe9171305fa1ce630d7f67ac2cda80145a1742516)
### 2.2 `pom.xml`
> 正常我們會用`JavaMail`相關`api`來寫傳送郵件的相關程式碼,但現在`Spring Boot`提供了一套更簡易使用的封裝。
1. `spring-boot-starter-mail`:`Spring Boot` 郵件服務;
2. `spring-boot-starter-thymeleaf`:使用 `Thymeleaf`製作郵件模版。
```xml
```
### 2.3 `application.yml`
`spring-boot-starter-mail` 的配置由 `MailProperties` 配置類提供。
針對不同的郵箱的配置略有不同,以下是`QQ`郵箱和`163`郵箱的配置。
```xml
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: [email protected]
# # 郵箱授權碼(不是密碼)
# 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: [email protected]
# 郵箱授權碼(不是密碼)
password: password
default-encoding: UTF-8
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
```
### 2.4 郵件資訊類
> 來儲存傳送郵件時的郵件主題、郵件內容等資訊
```java
@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 檢查輸入的郵件配置
> 校驗郵件收信人、郵件主題和郵件內容這些必填項
```java
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 將郵件儲存到資料庫
傳送結束後將郵件儲存到資料庫,便於統計和追查郵件問題。
```java
private Mail saveMail(Mail mail) {
// todo 傳送成功/失敗將郵件資訊同步到資料庫
return mail;
}
```
### 3.3 傳送郵件
- 傳送純文字郵件
```java
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);
}
```
- 傳送郵件並攜帶附件
```java
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);
}
```
- 傳送模版郵件
```java
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 單元測試
1. 測試附件郵件時,附件放在`static`資料夾下;
2. 測試模版郵件時,模版放在`file`資料夾下。
```java
@RunWith(SpringRunner.class)
@SpringBootTest
public class MailServiceTest {
@Resource
MailService mailService;
/**
* 傳送純文字郵件
*/
@Test
public void sendSimpleMail() {
Mail mail = new Mail();
// mail.setReceiver("[email protected]");
mail.setReceiver("[email protected]");
mail.setSubject("測試簡單郵件");
mail.setText("測試簡單內容");
mailService.sendSimpleMail(mail);
}
/**
* 傳送郵件並攜帶附件
*/
@Test
public void sendAttachmentsMail() throws MessagingException {
Mail mail = new Mail();
// mail.setReceiver("[email protected]");
mail.setReceiver("[email protected]");
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("[email protected]");
mail.setReceiver("[email protected]");
mail.setSubject("測試模版郵件郵件");
//建立模版正文
Context context = new Context();
// 設定模版需要更換的引數
context.setVariable("verifyCode", "6666");
mail.setEmailTemplateContext(context);
// 模版名稱(模版位置位於templates目錄下)
mail.setEmailTemplateName("emailTemplate");
mailService.sendTemplateMail(mail);
}
}
```
### 4.2 優化
因為平時傳送郵件還有抄送/密送等需求,這裡,封裝一個實體和工具類,便於直接呼叫郵件服務。
- 郵件資訊類
```java
@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;
}
```
- 郵件工具類
```java
@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 示例程式碼](https://github.com/vanDusty/SpringBoot-Home/tree/master/springboot-demo-list/send-mail),這裡就不貼出來了。
## 五、 總結及延伸
### 5.1 非同步傳送
很多時候郵件傳送並不是我們主業務必須關注的結果,比如通知類、提醒類的業務可以允許延時或者失敗。這個時候可以採用非同步的方式來發送郵件,加快主交易執行速度,在實際專案中可以採用`MQ`傳送郵件相關引數,監聽到訊息佇列之後啟動傳送郵件。
### 5.2 傳送失敗情況
因為各種原因,總會有郵件傳送失敗的情況,比如:郵件傳送過於頻繁、網路異常等。在出現這種情況的時候,我們一般會考慮重新重試傳送郵件,會分為以下幾個步驟來實現:
1. 接收到傳送郵件請求,首先記錄請求並且入庫;
1. 呼叫郵件傳送介面傳送郵件,並且將傳送結果記錄入庫;
1. 啟動定時系統掃描時間段內,未傳送成功並且重試次數小於`3`次的郵件,進行再次傳送。
### 5.3 其他問題
郵件埠問題和附件大小問題。
### 5.4 示例程式碼地址
- [Github 示例程式碼](https://github.com/vanDusty/SpringBoot-Home/tree/master/springboot-demo-list/send-mail)
- [Spring Boot 系列文章](https://github.com/vanDusty/SpringBoot-Home),歡迎關注[風塵部落格](https://www.dusty.vip)!
### 5.5 技術交流
1. [風塵部落格](https://www.dusty.vip)
1. [風塵部落格-掘金](https://juejin.im/user/5d5ea68e6fb9a06afa328f56/posts)
1. [風塵部落格-部落格園](https://www.cnblogs.com/vandusty/)
1. [Github](https://github.com/v