SpringBoot+mail 輕鬆實現各類郵件自動推送

潘志的研发笔记發表於2024-07-02

一、簡介

在實際的專案開發過程中,經常需要用到郵件通知功能。例如,透過郵箱註冊,郵箱找回密碼,郵箱推送報表等等,實際的應用場景非常的多。

早期的時候,為了能實現郵件的自動傳送功能,通常會使用 JavaMail 相關的 api 來完成。後來 Spring 推出的 JavaMailSender 工具,進一步簡化了郵件的自動傳送過程,呼叫其 send 方法即可傳送郵件。再之後, Spring Boot 針對郵件推送功能推出了spring-boot-starter-mail工具包,開發者可以透過它來快速實現郵件傳送服務。

今天透過這篇文章,我們一起來學習如何在 Spring Boot 中快速實現一個自動傳送郵件的功能。

二、環境準備

在介紹郵件推送實現之前,我們需要先準備一臺郵件推送的伺服器,以便實現相關功能。

這裡以騰訊郵箱為例,將其作為郵件傳送的中轉平臺。

2.1、開啟 SMTP 服務

登陸騰訊郵箱,開啟【設定】-》【收發信設定】,開啟 SMTP 服務,最後點選【儲存更改】。

2.2、生成客戶端專用密碼

點選【設定】-》【賬戶】,進入頁面後點選【開啟安全登陸】,點選【生成新密碼】。

這個新密碼會用於郵箱的自動傳送,因此需要記錄下來,最後點選【儲存更改】。

2.3、相關擴充套件知識

  • 什麼是 SMTP?

SMTP(simple mail transfer protocol),也被稱為簡單郵件傳輸協議,主要用於傳送電子郵件的,透過它可以實現郵件的傳送或者中轉。遵循 SMTP 協議的伺服器,通常稱為傳送郵件伺服器。

  • 什麼是 POP3?

POP3(Post Office Protocol),一種郵局通訊協議。主要用於接受電子郵件的,POP3 允許使用者從伺服器上把郵件儲存到自己的計算機上,同時刪除儲存在郵件伺服器上的郵件。同理,遵循 POP3 協議的伺服器,通常稱為接收郵件伺服器。

  • 什麼是 IMAP?

IMAP(Internet Mail Access Protocol),一種互動式郵件存取協議。與 POP3 協議類似,主要用於接收電子郵件,稍有不同的是:IMAP 允許電子郵件客戶端收取的郵件仍然保留在伺服器上,同時在客戶端上的操作都會反饋到伺服器上,例如刪除郵件,標記已讀等,伺服器上的郵件也會做相應的動作。所以無論從瀏覽器登入郵箱或者客戶端軟體登入郵箱,看到的郵件以及狀態都是一致的。

總結下來就是:SMTP 負責傳送郵件,POP3/IMAP 負責接收郵件。

常見郵箱發、收伺服器如下!

三、郵件推送實現

用於傳送郵件的伺服器、賬戶和密碼準備好了之後,就可以正式使用了。下面我們以 Spring Boot 的 2.1.0版本為基礎,實現過程如下。

2.1、新增依賴包

pom.xml檔案中,新增spring-boot-starter-mail依賴包。

<!--mail 支援-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

2.2、新增相關配置

application.properties中新增郵箱相關配置。

# 配置郵件傳送主機地址
spring.mail.host=smtp.exmail.qq.com
# 配置郵件傳送服務埠號
spring.mail.port=465
# 配置郵件傳送服務協議
spring.mail.protocol=smtp
# 配置郵件傳送者使用者名稱或者賬戶
spring.mail.username=xxx@qq.com
# 配置郵件傳送者密碼或者授權碼
spring.mail.password=xxxxxxx
# 配置郵件預設編碼
spring.mail.default-encoding=UTF-8
# 配置smtp相關屬性
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.ssl.enable=true
spring.mail.properties.mail.smtp.ssl.required=true

2.3、簡單傳送一封郵件

透過單元測試來實現一封簡單郵件的傳送,示例如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class MailSimpleTest {

    @Autowired
    private JavaMailSender mailSender;

    @Test
    public void sendSimpleMail() throws Exception {
        SimpleMailMessage message = new SimpleMailMessage();
        // 配置傳送者郵箱
        message.setFrom("xxxx@qq.com");
        // 配置接受者郵箱
        message.setTo("xxxxxx@qq.com");
        // 配置郵件主題
        message.setSubject("主題:簡單郵件");
        // 配置郵件內容
        message.setText("測試郵件內容");
        // 傳送郵件
        mailSender.send(message);
    }
}

執行單元測試之後,如果不出意外的話,接受者會收到這樣的一封郵件。

至此,郵件傳送成功!

2.4、傳送 HTML 格式郵件

在實際的業務開發中,郵件的內容通常會要求豐富,比如會傳送一些帶有圖片的內容,包括字型大小,各種超連結等,這個時候如何實現呢?

實際上,郵件內容支援 HTML 格式,因此可以藉助頁面模板引擎來實現絢麗多彩的內容。

下面我們以freemarker模板引擎為例,傳送一封內容為 HTML 格式的郵件。

2.4.1、引入 freemarker 依賴包

首先,在pom.xml檔案中,新增freemarker依賴包。

<!--freemarker 支援-->
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.23</version>
</dependency>
2.4.2、編寫郵件頁面模板

然後,在resources/templates目錄下,建立一個demo.ftl檔案,示例如下!

<html>
<head>
	<meta charset="utf-8">
	<title></title>
</head>
<body>
<div>您好:${userName}</div>
<div>這是html文字內容</div>
<img src="https://rescdn.qqmail.com/zh_CN/htmledition/images/logo/logo_0_0@2X1f1937.png" />
</body>
</html>
2.4.3、編寫一個郵件推送服務

雖然採用 Spring Boot 提供的自動配置屬性來實現郵件推送,可以極大的簡化開發過程。而實際開發的時候,通常更推薦自定義一個郵件統一推送服務,這樣更便於靈活的控制程式碼實現以及排查相關問題。

郵件統一傳送服務,示範如下。

@Component
public class MailPushService {

    private final Logger LOGGER = LoggerFactory.getLogger(MailPushService.class);

    @Value("${mail.host}")
    private String host;

    @Value("${mail.port}")
    private String port;

    @Value("${mail.protocol}")
    private String protocol;

    @Value("${mail.username}")
    private String username;

    @Value("${mail.password}")
    private String password;

    @Value("${mail.fromEmail}")
    private String fromEmail;

    @Value("${mail.fromPersonal}")
    private String fromPersonal;

    @Autowired
    private JavaMailSender mailSender;


    /**
     * 傳送郵件(簡單模式)
     * @param toEmail
     * @param subject
     * @param content
     */
    public void sendMail(String toEmail, String subject,String content)  {
        try {
            final Properties props = new Properties();
            //伺服器
            props.put("mail.smtp.host", host);
            //埠
            props.put("mail.smtp.port", port);
            //協議
            props.setProperty("mail.transport.protocol", protocol);
            //使用者名稱
            props.put("mail.user", username);
            //密碼
            props.put("mail.password", password);
            //使用smtp身份驗證
            props.put("mail.smtp.auth", "true");

            //開啟安全協議
            MailSSLSocketFactory sf = new MailSSLSocketFactory();
            sf.setTrustAllHosts(true);
            props.put("mail.smtp.ssl.enable", "true");
            props.put("mail.smtp.ssl.socketFactory", sf);
            Authenticator authenticator = new Authenticator() {
                @Override
                protected PasswordAuthentication getPasswordAuthentication() {
                    return new PasswordAuthentication(props.getProperty("mail.user"),
                            props.getProperty("mail.password"));
                }
            };

            Session session = Session.getDefaultInstance(props, authenticator);
            session.setDebug(true);
            MimeMessage mimeMessage = new MimeMessage(session);
            mimeMessage.setFrom(new InternetAddress(fromEmail, MimeUtility.encodeText(fromPersonal)));
            mimeMessage.setRecipient(MimeMessage.RecipientType.TO, new InternetAddress(toEmail));
            mimeMessage.setSubject(subject);
            mimeMessage.setContent(content, "text/html;charset=UTF-8");

            //儲存資訊
            mimeMessage.saveChanges();
            //傳送訊息
            Transport.send(mimeMessage);
            LOGGER.info("簡單郵件已經傳送。");
        } catch (Exception e) {
            LOGGER.error("傳送簡單郵件時發生異常!", e);
        }
    }
}

程式碼中相關自定義的全域性引數配置如下:

mail.host=smtp.exmail.qq.com
mail.port=465
mail.protocol=smtp
mail.username=xxx@qq.com
mail.password=xxxxxx
mail.fromEmail=xxxxxx@qq.com
mail.fromPersonal=傳送者暱稱
2.4.4、測試服務的正確性

最後,編寫一個單元測試來驗證服務的正確性,示例如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class MailTest {

    @Autowired
    private MailPushService mailPushService;

    @Test
    public void testSendHtmlMail() throws Exception {
        String sendHtml = buildHtmlContent("張三");
        mailPushService.sendMail("xxxxx@qq.com","簡單標題", sendHtml);
    }

    /**
     * 封裝html頁面
     * @return
     * @throws Exception
     */
    private static String buildHtmlContent(String userName) throws Exception {
        Configuration configuration = new Configuration(Configuration.VERSION_2_3_23);
        configuration.setDefaultEncoding(Charset.forName("UTF-8").name());
        configuration.setClassForTemplateLoading(MailTest.class, "/templates");
        // 獲取頁面模版
        Template template = configuration.getTemplate("demo.ftl");
        // 動態變數替換
        Map<String,Object> map = new HashMap<>();
        map.put("userName", userName);
        String htmlStr = FreeMarkerTemplateUtils.processTemplateIntoString(template,map);
        return htmlStr;
    }

}

執行單元測試之後,如果沒有報錯,接受者會收到這樣的一封郵件。

2.5、傳送帶附件的郵件

某些業務場景,使用者希望傳送的郵件中能帶上附件,比如上文中,在傳送 HTML 格式的郵件時,同時也帶上檔案附件,這個時候如何實現呢?

2.5.1、編寫帶附件的郵件傳送

此時可以在郵件推送服務中,新增一個支援帶附件的方法,實現邏輯如下。

/**
 * 傳送郵件(複雜模式)
 * @param toEmail    接受者郵箱
 * @param subject    主題
 * @param sendHtml   內容
 * @param attachment 附件
 */
public void sendMail(String toEmail, String subject, String sendHtml, File attachment) {
    try {
        //設定了附件名過長問題
        System.setProperty("mail.mime.splitlongparameters", "false");
        final Properties props = new Properties();
        //伺服器
        props.put("mail.smtp.host", host);
        //埠
        props.put("mail.smtp.port", port);
        //協議
        props.setProperty("mail.transport.protocol", protocol);
        //使用者名稱
        props.put("mail.user", username);
        //密碼
        props.put("mail.password", password);
        //使用smtp身份驗證
        props.put("mail.smtp.auth", "true");

        //開啟安全協議
        MailSSLSocketFactory sf = new MailSSLSocketFactory();
        sf.setTrustAllHosts(true);
        props.put("mail.smtp.ssl.enable", "true");
        props.put("mail.smtp.ssl.socketFactory", sf);
        Authenticator authenticator = new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(props.getProperty("mail.user"),
                        props.getProperty("mail.password"));
            }
        };

        Session session = Session.getDefaultInstance(props, authenticator);
        session.setDebug(true);
        MimeMessage mimeMessage = new MimeMessage(session);
        // 傳送者郵箱
        mimeMessage.setFrom(new InternetAddress(fromEmail, MimeUtility.encodeText(fromPersonal)));
        // 接受者郵箱
        mimeMessage.setRecipient(MimeMessage.RecipientType.TO, new InternetAddress(toEmail));
        // 郵件主題
        mimeMessage.setSubject(subject);
        // 定義郵件內容
        Multipart multipart = new MimeMultipart();

        // 新增郵件正文
        BodyPart contentPart = new MimeBodyPart();
        contentPart.setContent(sendHtml, "text/html;charset=UTF-8");
        multipart.addBodyPart(contentPart);

        // 新增附件
        if (attachment != null) {
            BodyPart attachmentBodyPart = new MimeBodyPart();
            // MimeUtility.encodeWord可以避免檔名亂碼
            FileDataSource fds=new FileDataSource(attachment);
            attachmentBodyPart.setDataHandler(new DataHandler(fds));
            attachmentBodyPart.setFileName(MimeUtility.encodeText(fds.getName()));
            multipart.addBodyPart(attachmentBodyPart);
        }

        // 將multipart物件放到message中
        mimeMessage.setContent(multipart);

        //儲存資訊
        mimeMessage.saveChanges();
        //傳送訊息
        Transport.send(mimeMessage);
        LOGGER.info("郵件已經傳送。");
    } catch (Exception e) {
        LOGGER.error("傳送郵件時發生異常!", e);
    }
}
2.5.2、測試服務的正確性

最後,編寫一個單元測試來驗證服務的正確性,示例如下:

@Test
public void doSendHtmlEmail() throws Exception {
    // 獲取正文內容
    String sendHtml = buildHtmlContent("張三");

    // 獲取附件
    File file = new File( "~/doc/Java開發手冊.pdf");
    // 傳送郵件
    mailPushService.sendMail("xxxxx@qq.com","帶附件的郵件推送", sendHtml, file);
}

執行單元測試之後,如果沒有報錯,接受者會收到這樣的一封郵件。

三、小結

最後總結一下,郵件自動推送功能在實際的業務系統中應用非常廣,在傳送過程中也可能會因為網路問題出現各種失敗現象,因此推薦採用非同步的方式來傳送郵件,例如採用非同步程式設計或者訊息佇列來實現,以便加快主流程的執行速度。

想要獲取專案原始碼的小夥伴,可以訪問如下地址獲取!

https://gitee.com/pzblogs/spring-boot-example-demo

四、參考

1.https://blog.csdn.net/qq_26383975/article/details/121957917

1.http://www.ityouknow.com/springboot/2017/05/06/spring-boot-mail.html

相關文章