用 javax.mail 发送邮件

工作中遇到了用 Java 通过 SMTP 发送邮件的需求,调研了一圈发现基本上用的都是 javax.mail 来实现的,这个玩意儿是上古年间 Java EE 的时候搞的一个 Java 的组件之一,API 设计得令人非常难以理解。我在使用中遇到的最大的问题是,有很多配置项你可以从很多地方设置,但是其实在真正发邮件的时候,你不知道程序会使用哪个地方配置的信息。比如说 SMTP 服务器的 Host,你可以在 Properties 里传入,也可以在 connect 的时候传入,但是没有任何的文档说明在两个地方都配置了的情况下程序会使用哪一处(其实我最终也没搞明白)。

总之,如果想要一个简单而现代化的解决方案,可以使用一个基于 javax.mail 开发的库 Simple Java Mail 。但是如果不想引入多余的依赖,那就复制我这一段例程改改用吧。

package asdf;

import org.springframework.util.ResourceUtils;

import javax.activation.DataHandler;
import javax.mail.Message;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.util.ByteArrayDataSource;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Date;
import java.util.Properties;

public class email {

    public static void main(String[] args) throws Exception {
        String host = "smtp.exmail.qq.com";
        int port = 465;

        Properties p = new Properties();
        p.put("mail.smtp.auth", "true");
        p.put("mail.transport.protocol", "smtp");
        p.setProperty("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
        p.setProperty("mail.smtp.socketFactory.fallback", "false");
        p.setProperty("mail.smtp.starttls.enable", "true");

        Session session = Session.getInstance(p);

        MimeMessage msg = new MimeMessage((Session) null);
        msg.setFrom(new InternetAddress("[email protected]")); // 发件人
        msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse("[email protected]"));
        msg.setSentDate(new Date());
        msg.setSubject("test " + new Date().toString());

        // multipart 可以包含许多个 MimeBodyPart,本例程里包含了一个 content 和一个 attachment
        Multipart multipart = new MimeMultipart();

        MimeBodyPart contentPart = new MimeBodyPart();
        contentPart.setContent("test email send", "text/html; charset=gbk");
        multipart.addBodyPart(contentPart);

        MimeBodyPart filePart = new MimeBodyPart();
        ByteArrayDataSource ds = new ByteArrayDataSource(readFile("a.png"), "application/octet-stream");
        filePart.setDataHandler(new DataHandler(ds));
        filePart.setFileName("a.png");
        multipart.addBodyPart(filePart);

        msg.setContent(multipart);

        // 这才是真正的 connection/session,如果想要复用这个对象,可以使用 apache commons-pool2 把它池化
        Transport transport = session.getTransport();
        transport.connect(host, port, "[email protected]", "a strong pwd");
        transport.sendMessage(msg, InternetAddress.parse("[email protected]"));
    }

    public static byte[] readFile(String filename) throws IOException {
        File file = ResourceUtils.getFile("classpath:"+filename);
        return Files.readAllBytes(file.toPath());
    }

}

例程中有些地方在此加以说明。

首先是检查 Transport 对应的连接的存活性。当我们使用 apache commons-pool2 将这个连接池化的时候,在 borrow 一个连接时,一般需要检测其存活性,如果连接已经断开,则需要重新创建连接,并销毁当前对象。其实检测连接存活性只需要调用 transport.isConnected() 即可,我们在 idea 中按住 Ctrl+Alt 并点击 Transport,选择 com.sun.mail.smtp.SMTPTransport 类,找到 isConnected 方法,会发现这个方法会调用 SMTP 中的 NOOP 指令以检测连接是否存活。

再者是发送附件时可以使用的 DataSource 其实很多,DataSource 其实是 Java EE 中的一个接口。我们在互联网上搜索 Java 发邮件的方法时,很大一部分回答会告诉我们调用 attachmentPart.attachFile(new File("C:\Document1.txt")); 或者使用 new FileDataSource(filename) 这样的代码去从文件中读取一个附件。这让人觉得要发送附件就必须从磁盘上的一个文件里读取数据,从而让人写出类似“先把用户上传的文件保存到磁盘上,然后再读取到内存里并发送邮件”的代码。其实根本没必要这么做,只需要使用 javax.mail 里实现的一个 ByteArrayDataSource ,直接把内存中的 Bytes 输入到邮件对象中即可。

此条目发表在搬砖杂记分类目录,贴了, , , 标签。将固定链接加入收藏夹。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据