javax.mail-邮箱客户端

732 阅读6分钟

背景

在从品牌网站抓取零件信息的时候,登录过程中会出现邮箱验证码。 目前的解决办法是再爬取邮件的网址,从中提取出验证码。这种办法虽然可行,但是最近在通过selenium登录邮箱的过程中出现了人机交互。所以需要另寻一个方法来解决邮件验证码的问题了。

初始想法

通过selenium控制浏览器从品牌官网登录是行不通了,因为有人机交互的界面,不是单纯的验证码,类似那种图灵机问题,判断你是人还是机器。

所以selenium方案肯定要舍弃。

找一个邮件客户端,在代码里配置好邮件服务器的地址和传输协议,账号密码之类的,在代码里接收验证码邮件。

一切都显得那么的流畅。

javax.mail

通过网上资料的查询,历经一番折腾,发现java可以通过引入依赖:

<!--javax.mail 是 JavaMail API 的核心库,主要用于在 Java 应用程序中发送和接收电子邮件。-->
<dependency>
    <groupId>com.sun.mail</groupId>
    <artifactId>javax.mail</artifactId>
    <version>1.6.2</version>
</dependency>

来实现邮件客户端的功能,收发邮件。(本篇文章只涉及客户端的收件

javax.mail 的一些关键名词解释

在开始贴代码之前,我们先解释一下javax.mail这个依赖中的几个主要类的角色。

在Java Mail API中,SessionMessageStore是处理电子邮件的核心组件,每个都扮演着不同的角色。下面是对这些概念的详细解释:

1. Session

Session是Java Mail API中用于维护应用程序与邮件服务器之间通信状态的对象。它封装了与邮件服务器交互所需的所有信息,包括邮件服务器的地址、端口、认证信息等。通过Session,你可以创建Message对象、Store(用于接收邮件)和Transport(用于发送邮件)对象。

Session通常是通过调用Session.getDefaultInstance(Properties props, Authenticator authenticator)方法获得的,其中Properties对象包含了邮件服务器的配置信息(如SMTP服务器地址、IMAP服务器地址、端口号、是否使用SSL等),而Authenticator对象则用于处理认证过程(如果需要的话)。

2. Message

Message是Java Mail API中表示一封电子邮件的抽象。它提供了多种方法来访问邮件的各个方面,包括发件人、收件人、主题、正文、附件等。Message对象可以是MimeMessage(用于MIME类型的邮件,这是最常见的类型)的实例,也可以是其他特定类型的邮件实例,具体取决于邮件的MIME类型。

Message对象通常是通过StoreTransport对象创建的,或者从已经存在的邮件中获取的。一旦你有了Message对象,就可以使用它提供的方法来读取或修改邮件的内容。

3. Store

Store是Java Mail API中用于连接到邮件服务器并访问邮件存储(如收件箱、发件箱、已发送邮件等)的对象。它提供了访问和操作存储在邮件服务器上的邮件的方法。Store可以是用于读取邮件的(如IMAP或POP3协议的Store),也可以是用于写入邮件的(尽管这通常不是Store的直接用途,因为发送邮件通常是通过Transport对象完成的)。

Store对象是通过调用SessiongetStore(String protocol)方法获得的,其中protocol参数指定了要使用的邮件协议(如"imap"、"pop3"等)。一旦你有了Store对象,就可以使用它来连接到邮件服务器,并打开一个邮件存储(如通过调用Store.connect(String host, String user, String password)方法)。之后,你可以使用Store提供的方法来访问和操作邮件。

总结来说,Session是Java Mail API中用于维护邮件会话状态的对象,Message代表一封电子邮件,而Store则用于连接到邮件服务器并访问邮件存储。这些组件共同工作,使得Java应用程序能够发送和接收电子邮件。

邮件客户端

框架类图

image.png

框架类图描述

用的设计模式是:模板方法。 在父类AbstractMailClient中定义了整个邮箱客户端收取邮件的主要方法。 将各个邮箱的初始化放在具体的邮箱子类进行操作。

image.png

具体代码

公司里用的是IonosMail,由于这种邮箱是国外的并且涉及公司隐私,所以这里就贴一下我个人的qq邮箱配置方式。

顶级父类:

public abstract class AbstractMailClient {

    protected Store store;

    protected Folder emailFolder;

    public static final int RECENT_MAIL_CNT = 20;

    public AbstractMailClient() {
        init();
    }

    protected abstract void init();

    protected abstract String getSubject();

    public Message getMessageContent() {
        Message message = null;
        Message[] recentMails = getRecentMail();
        if (recentMails != null && recentMails.length > 0) {
            for (int i = recentMails.length - 1; i >= 0; i--) {
                Message recentMail = recentMails[i];
                try {
                    if (recentMail.getSubject().equals(getSubject())) {
                        message = recentMail;
                        break;
                    }
                } catch (MessagingException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        if (message != null) {
            System.out.println("---------------------------------");
            try {
                System.out.println("Date: " + message.getReceivedDate());
                System.out.println("Subject: " + message.getSubject());
                System.out.println("From: " + message.getFrom()[0]);
                System.out.println("text: " + convertMessageToString(message));
            } catch (MessagingException | IOException e) {
                throw new RuntimeException(e);
            }
        }
        return message;
    }

    /**
     * 子类看情况重写
     *
     * @return 获取最近的几封
     */
    protected int getRecentMailCnt() {
        return RECENT_MAIL_CNT;
    }


    /**
     * 从邮箱中获取到最新的 x 封邮件
     *
     * @return 得到的邮件集合
     */
    protected Message[] getRecentMail() {
        int messageCount;
        try {
            messageCount = emailFolder.getMessageCount();
            int start = Math.max(1, messageCount - getRecentMailCnt() + 1);
            int end = messageCount;
            return emailFolder.getMessages(start, end);
        } catch (MessagingException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 将Message对象转换为字符串。
     *
     * @param message 要转换的Message对象。
     * @return 包含邮件内容的字符串,或者如果邮件为空或不支持的类型,则返回空字符串。
     * @throws MessagingException 如果邮件处理过程中发生错误。
     * @throws IOException 如果读取邮件内容时发生I/O错误。
     */
    public static String convertMessageToString(Part message) throws MessagingException, IOException {

        if (message == null) {
            return "";
        }

        StringBuilder sb = new StringBuilder();

        // 处理单部分邮件(如纯文本或HTML)
        if (message.isMimeType("text/*")) {
            String content = (String) message.getContent();
            sb.append(content);
        }
        // 处理多部分邮件:这里直接忽略附件
        else if (message.isMimeType("multipart/*")) {
            Multipart multipart = (Multipart) message.getContent();
            int count = multipart.getCount();
            for (int i = 0; i < count; i++) {
                BodyPart bodyPart = multipart.getBodyPart(i);
                if (bodyPart.isMimeType("text/*")) {
                    // 文本部分
                    String text = (String) bodyPart.getContent();
                    sb.append("Part ").append(i + 1).append(": ").append(text).append("\n");
                } else if (bodyPart.isMimeType("multipart/*")) {
                    // 递归处理嵌套的多部分
                    sb.append(convertMessageToString(bodyPart));
                }
            }
        }

        return sb.toString();
    }

}

具体的邮箱类:(这里是qq

public class QqMail extends AbstractMailClient{
    @Override
    protected void init() {
        // 这里我已经设置好了是qq的,直接用
        String host = "imap.qq.com";
        String mailStoreType = "imaps";
        // todo 这里要设置成自己的qq邮箱账号
        String username = "";
        // todo 这里要设置成自己的密码,是qq邮箱的授权码
        String password = "";
        // 设置邮件服务器的参数
        Properties properties = new Properties();
        properties.put("mail.imap.host", host);
        properties.put("mail.imap.port", "993");
        properties.put("mail.imap.starttls.enable", "true");

        // 获取默认的会话对象
        Session emailSession = Session.getInstance(properties);


        // 创建 IMAP store 对象并连接到邮件服务器
        try {
            store = emailSession.getStore(mailStoreType);
        } catch (NoSuchProviderException e) {
            throw new RuntimeException(e);
        }
        try {
            store.connect(host, username, password);
        } catch (MessagingException e) {
            throw new RuntimeException(e);
        }

        // 获取收件箱
        try {
            emailFolder = store.getFolder("INBOX");
        } catch (MessagingException e) {
            throw new RuntimeException(e);
        }
        try {
            emailFolder.open(Folder.READ_ONLY);
        } catch (MessagingException e) {
            throw new RuntimeException(e);
        }
    }

    // todo 由于我在获取验证码是按邮件主题去过滤获取的
    // todo 自己要测试的话需要自适应改动
    @Override
    protected String getSubject() {
        return "test qq mail client";
    }
}

关于qq邮箱客户端的配置教程见官方文档

SMTP/IMAP服务 (qq.com)

测试

先给目标邮箱发一封邮件。

image.png

测试代码

public class MailClientTest {
    public static void main(String[] args) {
        QqMail qqMail = new QqMail();
        System.out.println(qqMail.getMessageContent());
    }
}

测试结果

image.png

我们可以看到在代码里成功获取到了验证码,不需要通过人机交互啥的,很完美。

总结

在登录品牌网站的时候登录需要用到邮箱验证码。之前邮箱验证码是通过抓取网页数据来获得的,但是现在这个邮箱出现了人机交互,经过调研现在用javax.mail的邮箱客户端来接收邮件。 目前在生产环境上使用一个多礼拜了,一切正常。