十分鍾實現發送郵件服務


發送郵件應該是網站的必備拓展功能之一,注冊驗證、忘記密碼或者是給用戶發送營銷信息。

一、郵件協議

在收發郵件的過程中,需要遵守相關的協議,其中主要有:

  1. 發送電子郵件的協議:SMTP
  2. 接收電子郵件的協議:POP3IMAP

1.1 什么是SMTP

SMTP全稱為Simple Mail Transfer Protocol(簡單郵件傳輸協議),它是一組用於從源地址到目的地址傳輸郵件的規范,通過它來控制郵件的中轉方式。SMTP認證要求必須提供賬號和密碼才能登陸服務器,其設計目的在於避免用戶受到垃圾郵件的侵擾。

1.2 什么是IMAP

IMAP全稱為Internet Message Access Protocol(互聯網郵件訪問協議),IMAP允許從郵件服務器上獲取郵件的信息、下載郵件等。IMAPPOP類似,都是一種郵件獲取協議。

1.3 什么是POP3

POP3全稱為Post Office Protocol 3(郵局協議),POP3支持客戶端遠程管理服務器端的郵件。POP3常用於離線郵件處理,即允許客戶端下載服務器郵件,然后服務器上的郵件將會被刪除。目前很多POP3的郵件服務器只提供下載郵件功能,服務器本身並不刪除郵件,這種屬於改進版的POP3協議。

1.4 IMAPPOP3協議有什么不同呢?

兩者最大的區別在於,IMAP允許雙向通信,即在客戶端的操作會反饋到服務器上,例如在客戶端收取郵件、標記已讀等操作,服務器會跟着同步這些操作。而對於POP協議雖然也允許客戶端下載服務器郵件,但是在客戶端的操作並不會同步到服務器上面的,例如在客戶端收取或標記已讀郵件,服務器不會同步這些操作。

二、初始化配置

2.1 開啟郵件服務

本文僅以QQ郵箱和163郵箱為例。

  1. QQ郵箱 開啟郵件服務文檔
  2. 163郵箱 開啟郵件服務文檔

2.2 pom.xml

正常我們會用JavaMail相關api來寫發送郵件的相關代碼,但現在Spring Boot提供了一套更簡易使用的封裝。

  1. spring-boot-starter-mail:Spring Boot 郵件服務;
  2. 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 單元測試

  1. 測試附件郵件時,附件放在static文件夾下;
  2. 測試模版郵件時,模版放在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 發送失敗情況

因為各種原因,總會有郵件發送失敗的情況,比如:郵件發送過於頻繁、網絡異常等。在出現這種情況的時候,我們一般會考慮重新重試發送郵件,會分為以下幾個步驟來實現:

  1. 接收到發送郵件請求,首先記錄請求並且入庫;
  2. 調用郵件發送接口發送郵件,並且將發送結果記錄入庫;
  3. 啟動定時系統掃描時間段內,未發送成功並且重試次數小於3次的郵件,進行再次發送。

5.3 其他問題

郵件端口問題和附件大小問題。

5.4 示例代碼地址

5.5 技術交流

  1. 風塵博客
  2. 風塵博客-掘金
  3. 風塵博客-博客園
  4. Github


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM