1. 程式人生 > >十分鐘實現傳送郵件服務

十分鐘實現傳送郵件服務

傳送郵件應該是網站的必備拓展功能之一,註冊驗證、忘記密碼或者是給使用者傳送營銷資訊。 ## 一、郵件協議 在收發郵件的過程中,需要遵守相關的協議,其中主要有: 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