Java Mail 电子邮件

JavaEE 提供了 JavaMail 包(javax.mail),方便开发者构建简单易用的 Email 收发客户端软件。注意,JavaMail API 不涉及 Email 服务端的相关知识,可以将 JavaMail 看作是开发 Java 平台的 MUA(电子邮件用户代理)的一套 API。

JavaMail 简介

因为是 JavaEE 规范的包,因此必须自己下载 JavaMail 相关的 JAR 包。JavaMail API 相关的两个 JAR 文件:

  • javax.mail:JavaMail 核心包,点我下载
  • javax.activation:JavaBeans Activation Framework(JAF),处理附件时会用到,点我下载

下载后,我们需要将这两个 JAR 包(JAF 解压后才有)添加到 CLASSPATH 搜索路径中,为了方便日后添加其它第三方 API,建议专门创建一个文件夹,存放所有 JAR 依赖包,这里假设存放目录为 /usr/local/java-lib,然后修改 CLASSPATH 环境变量就可以将该目录下的所有 JAR 包添加到搜索路径中:export CLASSPATH=/usr/local/java-lib/*:$CLASSPATH

学习 JavaMail 之前,我们先来学习 Email 的相关知识。电子邮件说白了就两个部分:发送系统接收系统。其中与发送相关的协议主要是 SMTP(简单邮件传输协议),与接收相关的协议有两个:POP3(邮局协议第三版)、IMAP(互联网邮件访问协议)。在 Linux 系统上,常用的 SMTP 服务器为 Postfix,常用的 IMAP/POP3 服务器为 Dovecot。

特别注意,SMTP 服务与 IMAP/POP3 服务不是互相依赖的,你可以在不同的服务器上安装它们两个,也可以只安装其中一种(比如只安装 SMTP 服务器,单纯用于发送电子邮件)。不过,大部分情况下它们两个都是安装在同一台服务器上的。

说完协议,我们再来解释与 Mail 相关的名词:

  • MUA(Mail User Agent,邮件用户代理):主要有两个用途,发送邮件、接收邮件(包括管理邮件,下同)。常见的 MUA 有 Outlook Express、Foxmail,当然现在也有很多不需要安装软件的 MUA,只需要打开浏览器就能进行邮件的收发,称为 webmail,常见的有 QQ 邮箱、163 邮箱、Gmail 邮箱。
  • MTA(Mail Transfer Agent,邮件传输代理):即 SMTP 服务器,常用的 MTA 有 Postfix、SendMail。
  • MDA(Mail Delivery Agent,邮件投递代理):MDA 其实是 MTA 的一个小程序(组件),其实际功能与网络系统中的路由器类似。MTA 收到一封邮件后,首先交给 MDA,MDA 根据该邮件的目的收信域做出判断:如果该邮件是发往本域的,则将其存放至事先指定的文件夹中(俗称信箱);如果该邮件是发往外域的,则将其交还给 MTA,MTA 接着发送出去。
  • MRA(Mail Retrieval Agent,邮件接收代理):即 IMAP/POP3 服务器,常用的 MRA 有 Dovecot。注意,MRA 实际上并未参与任何电子邮件的收发工作,收和发都是 MTA + MDA 负责的,MRA 只是负责管理用户邮箱中的电子邮件的,比如查看邮件、删除邮件、标记已读等。

早期互联网上的 SMTP 服务器都是 open-relay 的,什么是 open-relay?MDA 如果发现邮件不是发往本域的,会将它交还给 MTA,而 MTA 会无条件的将该邮件发送出去(或者称为 relay,转发)。因为是无条件 relay 的,因此,所有人都可以使用此 SMTP 服务器来发送邮件,而不需要任何验证。这在互联网发展初期并没有什么问题,毕竟用户很少。但是如果放到现在,那就非常糟糕了,带来的直接后果就是垃圾邮件满天飞,服务器不堪重负,因为谁都可以使用这台 SMTP 服务器发送邮件,某些人当然不会放过这样的好机会。

所以,现在的绝大多数 SMTP 服务器都是默认关闭 open-relay 的,你要想使用此 SMTP 服务器来发送邮件,就必须申请必要的邮箱帐户,只有已注册的账户才可以使用它发送邮件,其它发送者的邮件是不会被接收的。比如 Gmail 邮箱,使用它的第一步就是获取一个 Gmail 账户(Google 帐号)。

POP3 和 IMAP 协议都是与 MRA 相关的,POP 协议先于 IMAP 出现,我们现在使用的 POP 协议通常为第三版,简称 POP3。

  • POP3 基本工作原理:MUA 连接到 MRA 后,首先将邮箱中邮件下载到本地,然后删除邮箱中的邮件(现可配置保留邮件副本),之后的所有操作都是在本机上进行的。因为是剪切到本地的(不考虑保留副本),因此在其他地方再使用 POP3 是无法获取这些邮件的。也因此 POP3 可以很好的支持离线操作,这也符合互联网初期环境的需求,毕竟那时候的带宽很小,不过现在,我们可能不会喜欢这种工作方式,比较笨重。
  • IMAP 基本工作原理:MUA 连接到 MRA 后,不是直接下载邮箱中的邮件,而是采取了一种更为简单灵活的方式,即发送命令,MRA 根据 MUA 发来的命令,进行不同的操作,同时,获取的邮件也不会在本机存储,所有的操作(读取,删除,归档)都是在服务器上完成的。因此,我们可以按需的读取邮件,而不需要一次性获取所有邮件,不过 IMAP 服务器会比 POP3 服务器消耗更多存储空间,因为所有的邮件都是存储在服务器上的。

因此,如果你需要离线阅读邮件(比如邮件服务器在公司内网,你回家后想要查看邮件),建议选择 POP3,否则选择 IMAP。

回到 JavaMail API,文章开头我们说了,此 API 不负责 Mail 服务端的开发,因此,它是用来开发 MUA 的一套 API,而 MUA 的主要功能就两个:发送邮件,接收邮件。本文主要围绕 普通邮件带附件的邮件带图片的 HTML 邮件 展开讲解。

JavaMail 发送

发送 简单电子邮件 用到的类:

  • java.util.Properties:属性表,用于存储 SMTP 服务器相关的信息。如 SMTP 服务器地址、端口、认证方式。
  • javax.mail.Authenticator:认证类,用于存储 SMTP 服务器的登录凭证。常见的认证方式为:用户名 + 密码。
  • javax.mail.Session:邮件会话类,这名字比较抽象,其实就是 Properties + Authenticator 的配置集合而已。
  • javax.mail.Message:邮件信息类,我们所说的电子邮件就是指它了,如设置收件人、邮件标题、邮件主体内容等。
  • javax.mail.Transport:邮件发送类,编写完 Message 后,需要使用 Transport 的 send() 方法将邮件发送出去。

我以 xxx@zfl9.com 的身份,发送一封邮件给 xxx@gmail.com,例子如下:

编译运行后,登录 xxx@gmail.com 账户,不出意外的话应该会收到一封新邮件,如下:
SendMail

那么,要如何发送一封 带附件的电子邮件 呢?基本用法和上面的类似,只是 Content 不再是 String(纯文本),而是 Multipart。Multipart 本质上是一个数组(Part 集合),元素类型为 BodyPart。BodyPart 和 Message 都是 Part 接口的实现类,也可以将 Multipart 看作是多个 Message 的集合。邮件正文是一个 BodyPart,邮件附件也是一个 BodyPart(可以有多个附件,这里以一个附件举例)。

编译运行,如果没有报错,登录 Gmail,可以看到一份新邮件,如下:
SendMailWithFile

但是,我们大多数邮件都是 HTML 格式的,如果我想在 HTML 文档中插入一张图片(使用 <img> 标签),并且让这张图片显示在正文部分,而不是出现在附件部分,该怎么做呢?以上面的带附件的电子邮件为例,邮件 Content 是一个包含两个 BodyPart 的 Multipart。其中一个 BodyPart 是邮件正文,另一个则是邮件附件(刚好也是一张图片)。其它地方基本不用改动,只需将 setFileName() 改为 addHeader("Content-ID", "<otokaze.jpg>"),给这个附件分配一个 Content-ID,然后我们就可以在 img 标签中引用它了。如下:

打开 Gmail 邮箱,可以看到下面这样一封新邮件,注意与带普通附件的区别,这里的图片是显示在正文部分的,附件没有显示:
SendMailWithImage

JavaMail 接收

在学习 JavaMail 接收电子邮件相关 API 之前,我们先来了解一些与电子邮件接收相关的知识。

我们的邮件都是存储在服务器上的某个文件夹下的,存储邮件的地方也称为 邮箱,在 JavaMail 中,使用 Store 对象表示邮箱。在 Store 中,有一个特殊文件夹,也即 Root 根目录,可以通过 store.getDefaultFolder() 方法获取它。而我们常说的 收件箱草稿箱垃圾箱 都是根目录下的一个文件夹。在 JavaMail 中,使用 Folder 对象表示一个文件夹,Root 目录也是一个文件夹,其他文件夹都是它的子文件夹。

我们常用的文件夹就是 收件箱 了,不过一般收件箱的文件夹名称并不是“收件箱”,而是 INBOX(不区分大小写),如果想获取某个账户下的所有文件夹,可以使用下面这个程序:

例子,获取收件箱中的电子邮件(未读邮件):

那么,我们该如何删除电子邮件呢?上面的程序是通过 message.setFlag(Flags.Flag.SEEN, true); 方法来将邮件标记为已读的,其实删除邮件也是通过这种方法,只不过 flag 不同而已,例子如下:

JavaMail 参考