背景
在从品牌网站抓取零件信息的时候,登录过程中会出现邮箱验证码。 目前的解决办法是再爬取邮件的网址,从中提取出验证码。这种办法虽然可行,但是最近在通过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中,Session、Message和Store是处理电子邮件的核心组件,每个都扮演着不同的角色。下面是对这些概念的详细解释:
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对象通常是通过Store或Transport对象创建的,或者从已经存在的邮件中获取的。一旦你有了Message对象,就可以使用它提供的方法来读取或修改邮件的内容。
3. Store
Store是Java Mail API中用于连接到邮件服务器并访问邮件存储(如收件箱、发件箱、已发送邮件等)的对象。它提供了访问和操作存储在邮件服务器上的邮件的方法。Store可以是用于读取邮件的(如IMAP或POP3协议的Store),也可以是用于写入邮件的(尽管这通常不是Store的直接用途,因为发送邮件通常是通过Transport对象完成的)。
Store对象是通过调用Session的getStore(String protocol)方法获得的,其中protocol参数指定了要使用的邮件协议(如"imap"、"pop3"等)。一旦你有了Store对象,就可以使用它来连接到邮件服务器,并打开一个邮件存储(如通过调用Store.connect(String host, String user, String password)方法)。之后,你可以使用Store提供的方法来访问和操作邮件。
总结来说,Session是Java Mail API中用于维护邮件会话状态的对象,Message代表一封电子邮件,而Store则用于连接到邮件服务器并访问邮件存储。这些组件共同工作,使得Java应用程序能够发送和接收电子邮件。
邮件客户端
框架类图
框架类图描述
用的设计模式是:模板方法。 在父类AbstractMailClient中定义了整个邮箱客户端收取邮件的主要方法。 将各个邮箱的初始化放在具体的邮箱子类进行操作。
具体代码
公司里用的是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邮箱客户端的配置教程见官方文档
测试
先给目标邮箱发一封邮件。
测试代码
public class MailClientTest {
public static void main(String[] args) {
QqMail qqMail = new QqMail();
System.out.println(qqMail.getMessageContent());
}
}
测试结果
我们可以看到在代码里成功获取到了验证码,不需要通过人机交互啥的,很完美。
总结
在登录品牌网站的时候登录需要用到邮箱验证码。之前邮箱验证码是通过抓取网页数据来获得的,但是现在这个邮箱出现了人机交互,经过调研现在用javax.mail的邮箱客户端来接收邮件。 目前在生产环境上使用一个多礼拜了,一切正常。