37DATA

JavaMail接收邮件

1、邮件协议介绍

SMTP SMTP 的全称是“Simple Mail Transfer Protocol”,即简单邮件传输协议。它是一组用于从源地址到目的地址传输邮件的规范,通过它来控制邮件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。SMTP 服务器就是遵循 SMTP 协议的发送邮件服务器。SMTP 认证,简单地说就是要求必须在提供了账户名和密码之后才可以登录 SMTP 服务器,这就使得那些垃圾邮件的散播者无可乘之机。增加 SMTP 认证的目的是为了使用户避免受到垃圾邮件的侵扰。
POP3
POP3是Post Office Protocol 3的简称,即邮局协议的第3个版本,它规定怎样将个人计算机连接到Internet的邮件服务器和下载电子邮件的电子协议。它是因特网电子邮件的第一个离线协议标准,POP3允许用户从服务器上把邮件存储到本地主机(即自己的计算机)上,同时删除保存在邮件服务器上的邮件,而POP3服务器则是遵循POP3协议的接收邮件服务器,用来接收电子邮件的。
IMAP
IMAP全称是Internet Mail Access Protocol,即交互式邮件存取协议,它是跟POP3类似邮件访问标准协议之一。不同的是,开启了IMAP后,您在电子邮件客户端收取的邮件仍然保留在服务器上,同时在客户端上的操作都会反馈到服务器上,如:删除邮件,标记已读等,服务器上的邮件也会做相应的动作。所以无论从浏览器登录邮箱或者客户端软件登录邮箱,看到的邮件以及状态都是一致的。 个人邮箱的协议配置页面:

简单来说,SMTP是用于发送的,POP3/IMAP是用于接收。如果需要变更服务器上的邮件,就需要IMAP协议,例如删除、标记、迁移文件夹等操作。 同时每个协议都有加密连接方式,对应不同的端口,如图:

2、JavaMail介绍 收发邮件的Java包一般是下面几种:
a、使用javax.mail的坐标依赖包(导包时看清楚是不是javax.mail的)    
   <!--JavaMail基本包-->        <dependency>            <groupId>javax.mail</groupId>            <artifactId>mail</artifactId>            <version>1.4.7</version>        </dependency>        <!--邮件发送的扩展包-->        <dependency>            <groupId>javax.activation</groupId>            <artifactId>activation</artifactId>            <version>1.1.1</version>        </dependency>
        注:坐标必须为两个,因为javax.mail没有携带依赖包javax.activation(发送)
b、使用Jakarta Mail发送邮件(和javax.mail使用方式基本一样,不过它内部携带了javax.activation依赖包)    
   <dependency>       <groupId>com.sun.mail</groupId>       <artifactId>jakarta.mail</artifactId>       <version>1.6.7</version>   </dependency>
c、使用还有一种方式是使用SpringBoot集成的功能:
<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-mail</artifactId></dependency>
目前我使用的是javax.mail,因为项目本身已安装。不过建议使用Jakarta Mail的包,因为javax.mail貌似已经停更。 下面主要针对javax.mail接收邮件进行讲解。

3、接收邮件

3.0、邮件接收过程

创建一个 Session 对象。 Session 对象创建一个 Store 对象,用来保存邮件。 Store 对象连接邮件服务器。 Store 对象创建一个 Message 对象 ( 也就是邮件内容 ) 。 Store 对象获取邮箱的邮件。

3.1、配置

    private final String protocol;    private final String host;    private final String port;    private final String user;    private final String password;    public MailReceiveClient(String protocol, String host, String port, String user, String password) {        this.protocol = protocol;        this.host = host;        this.port = port;        this.user = user;        this.password = password;    }

3.2、使用Session类定义配置信息

javax.mail.Session类用于定义整个应用程序所需的环境信息,以及收集客户端与邮件服务器建立网络连接的会话信息,例如邮件服务器的主机名、端口号、采用的邮件发送和接收协议等。Session 对象根据这些信息构建用于邮件收发的Transport和Store对象,以及为客户端创建Message对象时提供信息支持。
    Properties properties = new Properties();properties.put("mail.store.protocol", protocol);    properties.put("mail." + protocol + ".host", host);    properties.put("mail." + protocol + ".port", port);    Session emailSession = Session.getInstance(properties);
这里的 protocol 值得是协议,对应的是"POP3"或者"IMAP"

3.3、调用Store类接收

javax.mail.Store类是接收邮件的核心API类,它的实例对象代表实现了某个邮件接收协议的邮件接收对象,例如POP3协议,客户端程序接收邮件时,只需要使用邮件接收API得到Store对象,然后调用Store对象的接收方法,就可以从指定的POP3服务器获得邮件数据,并把这些邮件数据封装到表示邮件的 Message 对象中
    // 从Session实例中获取Store对象    emailStore = emailSession.getStore();    emailStore.connect(user, password);    // 打开文件夹,这里默认打开收件箱    // 注意,如果想打开其他文件夹,必须都在inbox前缀下:[INBOX.Junk, INBOX.Drafts, INBOX.Sent, INBOX.Trash, INBOX.Finished]    emailFolder = emailStore.getFolder("INBOX");     emailFolder.open(Folder.READ_WRITE);

3.4、Message类创建和解析邮件内容

javax.mail.Message类是创建和解析邮件的核心API,这是一个抽象类,通常使用它的子类javax.mail.internet.MimeMessage类。它的实例对象表示一份电子邮件。客户端程序发送邮件时,首先使用创建邮件的JavaMail API创建出封装了邮件数据的Message对象,然后把这个对象传递给邮件发送API(Transport 类) 发送,客户端程序接收邮件时,邮件接收API把接收到的邮件数据封装在Message类的实例中,客户端程序在使用邮件解析API从这个对象中解析收到的邮件数据。 代码:
private List<Mail> getNewMails(Folder emailFolder) throws MessagingException {        List<Mail> mails = new ArrayList<>();        for (Message message : emailFolder.getMessages()) {            if (!message.getFlags().contains(Flags.Flag.SEEN)) {                message.setFlags(new Flags(Flags.Flag.SEEN), true);                Mail mail = MailMapper.map(message, user);                mails.add(mail);            }        }        return mails;    }
Flag.seen表示已读。
定义Mail的Bean类,包含以下内容: Subject 邮件主题 Content 邮件内容 From 发送者 Recipient 接收者 代码:
public final class Mail {    private final String subject;    private final String content;    private final String from;    private final String recipient;    public Mail(String subject, String content, String from, String recipient) {        this.subject = subject;        this.content = content;        this.from = from;        this.recipient = recipient;    }    public String getSubject() {        return subject;    }    public String getContent() {        return content;    }    public String getFrom() {        return from;    }    public String getRecipient() {        return recipient;    }}

Mapper方法(定义解析内容的方法): 代码:
import org.owasp.encoder.Encode;import javax.mail.Address;import javax.mail.Message;import javax.mail.MessagingException;import java.io.IOException;import java.util.Arrays;public final class MailMapper {    private MailMapper() {}    public static Mail map(Message message, String user) {        String subject = Encode.forHtml(getSubject(message));        String content = Encode.forHtml(getContent(message));        String from = Encode.forHtml(getFrom(message));        String recipient = Encode.forHtml(getRecipient(message, user));        return new Mail(subject, content, from, recipient);    }    private static String getSubject(Message message) {        try {            return message.getSubject();        } catch (MessagingException e) {            throw new RuntimeException(e);        }    }    private static String getContent(Message message) {        try {            Object content = message.getContent();            if (content == null) {                return null;            }            return content.toString();        } catch (IOException | MessagingException e) {            throw new RuntimeException(e);        }    }    private static String getFrom(Message message) {        try {            Address[] from = message.getFrom();            if (from.length == 0 || from[0] == null) {                return null;            }            return from[0].toString();        } catch (MessagingException e) {            throw new RuntimeException(e);        }    }    private static String getRecipient(Message message, String user) {        try {            return Arrays.stream(message.getAllRecipients())                    .map(Address::toString)                    .filter(it -> it.startsWith(user))                    .findFirst()                    .orElse(user);        } catch (MessagingException e) {            throw new RuntimeException(e);        }    }}

4、获取附件

上面内容只是获取文本内容,如果需要获取附件的话看下面。 首先看Message类结构:
  • MimeMessage 类:代表整封邮件。
  • MimeBodyPart 类:代表邮件的一个 MIME 信息。
  • MimeMultipart 类:代表一个由多个 MIME 信息组合成的组合 MIME 信息。
MIME(Multipurpose Internet Mail Extensions) :即多用途因特网邮件扩展标准。它不是邮件传输协议。但对传输内容的消息、附件及其它的内容定义了格式。 解析内容参考: 邮件的主题为: 测试接收邮件(无附件)①    发件人地址为: =?gb18030?B?sai/vLTL0KO1xLH4?= <xiaofeng6699@ vip.qq.com >
邮件的内容为:
Received: from xmbg7.mail.qq.com (unknown [101.91.43.54])
by mx47 (Coremail) with SMTP id YcCowAAHhSLsVZxhew5GCw--.44623S3;
Tue, 23 Nov 2021 10:46:04 +0800 (CST)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=vip.qq.com;
s=s201512; t=1637635564;
bh=hx6m189GKLvHwYK7EPgjUY6W63aDJzkYLaZobsCH5fw=;
h=From:To:Subject:Date;
b=enOyaq4hkcY0sVHuLe3O6f0wz9gctOI9knFJ438sldmBa4gFpX7I6Ucv9Npe0BtTz
+89KfHw6wUpfiBmgo119MRTkR8m1gE8BXyFCUgdq2qaYHPkf0sNAaZDtDGan7rRQuG
oQmP+mXeqP3/KHtJNAMYEAJQF03qYipFKcPXA964=
X-QQ-FEAT: zaIfg0hwV2qA1LHh5sNQ3yDC93UmKYlF+NlagoYC+rE=
X-QQ-SSF: 000100000000004000000000000000Z
X-QQ-XMAILINFO: NBrlLnjHQiaZ3bWTg2+kaq2WcXqqqVbSme9UoRGcm/EacxcEzzBQa78nxQndrm  eF/UAg21TqvTk30iPgxBkFSCSf9NIRTG3wpo2TUtMU4b/2LEU+Be3z3i/hOQaoe09Y2PYIfBfkona FEzXSl8LNQHYStRGXPoD/TwYeiQfXWnMIOnEU3EgJpJ5uznkMMFN6wldar9WyhOYPE3h2nLLSuSN4RT4Prw9m7uF/IJoSE5jMEI2hO3EE7YetoenF7EULwI+s5KyKyH9Y+hMDbVsZv5mnQ+aBLJJSsBJbaRb4Z7Di6PZfKS9MN2IQvcKNzNlgQ5mIhSiwLIlukwMz5W+XnOVp7gLIn4HXK5b4WmqclfbrHV0b1qSM/HdMWzjT29cV1ZM7A2+uiCPFR9Tf1V0EDOhJGN+GqCgV4Z8ETjrWz47C4E3B993M+HiNQj1PY03UhD+8DsrsGqZqVTKoRP3SQKrZSygkyrYpBkevCoRex26JH67dHQxRXUGgCci1UZ8IA9/kMa28JQ+hfM0Cly0ZQtW32QAJSPUcHdrhxkfWQo3mfeVHg6NEp18arwXhEPp4Io8PTcmCGq6G0k5ZsjMVXaUtNDRUFEVQVo2jxint/dyQoGoxsNVkUdqYijW+VTMSdMkC4wnuEqjZ5a+8aiD1uMGm/eEkwW2qF4tZi7XZ5MKHKvvze1HSPsol0cuP6og8hbc490IH1Lz4Slz/HSTWL95IwkqvniKJtVKldTpa+0GYEY1dPIxKAmwahttxUgWv39mZ4VJgxL5Ozb+IX4tZ4JyTsjx4sk8RCHkEQVHsXZn+nSrQwPvlmMlaQ5sp/N7WJt7yf2TpaHTWhtTek3P08MNRmkPqMgJSKCrn+3t/TJq8HdAKQi2NY0B0Mnseh5S7pWEu3i5xh1Rh
X-HAS-ATTACH: no
X-QQ-BUSINESS-ORIGIN: 2
X-Originating-IP: 122.97.149.60
X-QQ-STYLE:
X-QQ-mid: webmail623t1637635564t3630643
From: "=?gb18030?B?sai/vLTL0KO1xLH4?=" <[email protected]>
To: "=?gb18030?B?YW50bGFkZGll?=" <[email protected]>
Subject: =?gb18030?B?suLK1L3TytXTyrz+KM7euL28/imi2Q==?=
Mime-Version: 1.0
Content-Type: multipart/alternative;
boundary="----=_NextPart_619C55EB_107CACC0_425E3EAC"
Content-Transfer-Encoding: 8Bit
Date: Tue, 23 Nov 2021 10:46:03 +0800
X-Priority: 3
Message-ID: <[email protected]>
X-QQ-MIME: TCMime 1.0 by Tencent
X-Mailer: QQMail 2.x
X-QQ-Mailer: QQMail 2.x
X-CM-TRANSID:YcCowAAHhSLsVZxhew5GCw--.44623S3
Authentication-Results: mx47; spf=pass [email protected]
om; dkim=pass [email protected]
X-Coremail-Antispam: 1Uf129KBjDUn29KB7ZKAUJUUUUU529EdanIXcx71UUUUU7v73
VFW2AGmfu7bjvjm3AaLaJ3UbIYCTnIWIevJa73UjIFyTuYvjxU4vPfDUUUU

This is a multi-part message in MIME format.

------=_NextPart_619C55EB_107CACC0_425E3EAC
Content-Type: text/plain;
charset="gb18030"
Content-Transfer-Encoding: base64 SSdtIGdvaW5nIHRvIGRvIGEgbWFpbGJveCBhY2NlcHRhbmNlIHRlc3QuztLSqtf20ru49tPK
z+S908rVsuLK1A== ------=_NextPart_619C55EB_107CACC0_425E3EAC
Content-Type: text/html;
charset="gb18030"
Content-Transfer-Encoding: base64 PG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNo
YXJzZXQ9R0IxODAzMCI+PHNwYW4gc3R5bGU9ImZvbnQtZmFtaWx5OiBQaW5nRmFuZ1NDLVJl
Z3VsYXIsICZxdW90O01pY3Jvc29mdCBZYWhlaSZxdW90OywgJnF1b3Q7XFw1RkFFyO3Rxbra
JnF1b3Q7LCBzYW5zLXNlcmlmOyBmb250LXNpemU6IDE2cHg7Ij5JJ20gZ29pbmcgdG8gZG8g
YSBtYWlsYm94IGFjY2VwdGFuY2UgdGVzdC48L3NwYW4+PGRpdj48Zm9udCBmYWNlPSJQaW5n
RmFuZ1NDLVJlZ3VsYXIsIE1pY3Jvc29mdCBZYWhlaSwgXFw1RkFFyO3RxbraLCBzYW5zLXNl
cmlmIj48c3BhbiBzdHlsZT0iZm9udC1zaXplOiAxNnB4OyI+ztLSqtf20ru49tPKz+S908rV
suLK1Dwvc3Bhbj48L2ZvbnQ+PC9kaXY+

------=_NextPart_619C55EB_107CACC0_425E3EAC--


获取附件的代码:
for (Message message : messages) {    // 处理邮件内容    if (message.isMimeType("multipart/*")) {        Multipart multipart = (Multipart) message.getContent();        // 遍历内容主体        for (int i = 0; i < multipart.getCount(); i++) {            BodyPart bodyPart = multipart.getBodyPart(i);            if (Part.ATTACHMENT.equalsIgnoreCase(bodyPart.getDisposition())) {                // 获取附件文件名,注意使用decodeText防止中文乱码                String fileName = MimeUtility.decodeText(bodyPart.getFileName());                // 保存附件到本地                saveAttachment(bodyPart.getInputStream(), fileName);            }        }    }}
保存本地方法,分块读取并写入:
public void saveAttachment(InputStream inputStream, String fileName) throws SecurityException, IOException {    File file = new File(fileName);    FileOutputStream outputStream = new FileOutputStream(file);    int blockSize = 4096;    byte[] buffer = new byte[blockSize];    int bytesRead;    while ((bytesRead = inputStream.read(buffer)) != -1) {        outputStream.write(buffer, 0, bytesRead);    }    outputStream.close();    inputStream.close();}
5、总结几个坑
5.1 检查好邮箱服务设置,尤其端口号和是否加密,大部分服务并有没有开通IMAP协议
5.2 记得捕获异常,并用finally断开连接 5.3 在结束后记得也要关闭文件夹和连接 emailFolder.close(false); // 迁移文件夹的时候这里一定要false,否则失效 emailStore.close(); 5.4 注意读取邮件的顺序,并且读取没有索引,每次都是从头开始,所以我们程序在读取完后都会迁移到新的文件夹 5.5 Store类实例可以重复使用,避免重复连接。