美文网首页Java学习笔记
JavaMail开发webMail不完全经验——之邮件接收和解析

JavaMail开发webMail不完全经验——之邮件接收和解析

作者: 阿库纳玛塔塔 | 来源:发表于2017-08-23 12:47 被阅读528次

虽然我是个菜逼,但是我还是打算写下这一篇博客,只是作为我自己在开发中遇到问题的一些记录,也许下次遇到相同问题的时候,我还可以回看一下。当然,如果该文章帮助到你了,我也非常开心。

最近在做公司的项目,这边需求要一个邮箱,可以代管理各种邮箱。这个任务扔给了我,经过这么长时间的研究,现在大概做出来了一个简单的邮箱,这里给大家分享一下,我开发这个邮件的过程吧,大神勿喷,看看就行,也欢迎大家提出宝贵的意见,可以让我来完善一下。

一、JavaMail初识

JavaMail是Sun官方提供的一套标准开发包,可以代收发邮件,支持一些常用协议,比如:SMTP、POP3、IMAP和MIME等。开发人员使用JavaMail编写邮件处理软件时,不需要考虑邮件协议的低层实现,只需要调用JavaMail开发包中的API即可,也有第三方的整合包,能更方便的使用JavaMail,这里推荐一个叫Jodd的还不错,将邮件需要做的处理封装得很好了。

下面简单介绍一下JavaMail中主要用到的几个类:

Message类

Message类是处理邮件时使用最多的类之一,在JavaMail中一个Message实例就是一封邮件。

Transport类

Transport类就是发送邮件的API类,它的实例对象代表发送邮件协议的发送对象,创建了Message实例后调用transport的发送方法就能发送邮件出去。

Store类

Store类是接受邮件的API类,它的实例表示接收邮件协议的接受对象,调用store的接收方法就可以从指定的服务器获得邮件数据。

Session类

Session类顾名思义就是会话类,做过Servlet开发的人应该用到过HttpRequestSession,类似与这种会话。而JavaMail中的Session定义类一系列邮件收取客户端和邮件服务器建立网络连接的会话信息,比如邮件服务器的主机、端口号、所用协议等。之前提到的Store类和Transport类都需要Session类来进行构建。

二、JavaMail接收邮件

进入主题,webMail最主要的功能就是收发和管理邮件。接收邮件作为webMail的核心功能之一,这里我们重点描述一下如何利用JavaMail来接收邮件。

首先我们定义一个邮件头实体,因为我们WebMail第一步需要展示的就是邮件的头信息,并不需要我们去解析邮件的内容,所以我把头信息放在了一个实体当中,里面主要为了存放一些邮件的基本信息,比如:主题、收件人、发件人、抄送人、密送人等等等等,这里就不多啰嗦了。

public class MailHeader {
    //邮件序号(第几封邮件)
    private int num;
  //发件人地址
    private String fromAddress;
    //邮件主题
  private String subject;
    //是否已读
  private boolean isSeen;
    //发送时间
    private String sendTime;
        ....
}

set/get方法省略。。。

接着还需要一个实体类,这个实体类主要包含邮箱信息

public class BoxInfo {
      //未读邮件数量
    private Integer unReadCount;
    //删除邮件数量
    private Integer deleteCount;
    //新邮件数量
    private Integer newCount;
    //邮件总数
    private Integer totalCount;
    //邮件
    private Object messages;
    //当前页数
    private Integer pageNo = 1;
    //当页数据容量
    private Integer pageSize = 10;
    //总页数
    private Integer pageTotal;

        ....
}

以上工作完成后,现在我们可以开始写邮件接收了,我写了一个邮件处理的工具类,里面封装了很多处理邮件的方法,首先我们来看第一个使用IMAP协议接收邮件的方法:

根据之前我讲的JavaMail中的几个核心类,给大家梳理一下接收邮件的主要步骤:
1. 获取邮件会话Session类
2. 根据Session和接收协议,获取接收对象Store类
3. 根据邮箱账号和密码(授权码),用Store类连接邮件服务器
4. 根据Store类获取具体邮箱的文件夹对象Folder
5. 打开Folder类,就可以获取到具体的MimeMessage类型的邮件了
代码:

public static BoxInfo receive(UserConfig userConfig,String folderName,int pageNo,int pageSize){
    BoxInfo boxInfo = new BoxInfo();
    try{
        Properties props = new Properties();
        props.setProperty("mail.imap.host", config.getHost());
        props.setProperty("mail.imap.port", config.getPort());
        //步骤1
        Session session = Session.getInstance(props);
        //步骤2
        IMAPStore store = (IMAPStore) session.getStore(config.getProtocol());
        //步骤3
        store.connect(config.getUserAddress(),config.getPassword());
        //步骤4
        //注释一(代码下面有注释解释)
        IMAPFolder folder = getFolder(folderName,store);
        //步骤5
        //注释二(代码下面有注释解释)
        folder.open(Folder.READ_WRITE);
        //开始解析邮箱信息
        boxInfo.setUnReadCount(folder.getUnreadMessageCount());
        boxInfo.setDeleteCount(folder.getDeleteMessageCount());
        boxInfo.setNewCount(folder.getNewMessageCount());
        boxInfo.setTotalCount(folder.getTotalMessageCount());
        boxInfo.setPageNo(pageNo);
        boxInfo.setPageSize(pageSize);
        int totalPage = 0;
        //注释三(代码下面有注释解释)
        if (folder.getMessageCount()%pageSize>0){
            totalPage = folder.getMessageCount()/pageSize+1;
        } eles {
            totalPage = folder.getMessageCount()/pageSize;
        }
        boxInfo.setPageTotal(totalPage);
        List<MailHeader> mails = new ArrayList<>();
        Message[] messages = null;
        int totalCount = folder.getMessageCount();
        //注释四(代码下面有注释解释)
        if ((totalCount-pageNo*pageSize)>0){
            messages = folder.getMessages(totalCount-pageNo*pageSize+1,totalCount-(pageNo-1)*pageSize);
        } eles {
            messages = folder.getMessages(1,totalCount-(pageNo-1)*pageSize);
        }
        //注释五(代码下面有注释解释)
        for(int i = messages.length;i>0;i--){
            MailHeader header = new MailHeader();
            Message msg = message[i-1];
            header.setNum(pageSize-i)
            header.setIsSeen(msg.getFlags().contains(Flags.Flag.SEEN))
            header.setFromAddress(getFromByAddress(msg.getFrom()));
            header.setSubject(msg.getSubject());
            header.setSendTime(new SimpleDateFormat("yyyy年MM月dd日 E HH:mm ").format(msg.getReceivedDate()));
            mails.add(header);
        }
        boxInfo.setMessages(mails);
        folder.close(true);
        store.close();
    }catch(Exception e){
        //自己处理异常
        ...
    }
    return boxInfo;
}

代码中间有几个地方可能需要解释一下当时这样做的原因,下面是每个注释位置的解释:
1. 此处folderName为邮箱内文件夹的名称,比如INBOX(收件箱),后面我会给出几种常用邮箱支持的FolderName。
2. 这个地方的打开方式有多种,其实这个部分应该用只读方式打开比较合理。Folder.READ_WRITE是可编辑的方式,即可以修改Folder中的数据。
3. 这个部分还是比较好理解,自己做分页的同学都知道,这样来判断总页数,就不多说了。
4. 这个地方可能很多同学就不解了,因为邮件服务返回给我们Folder,获取出来的Message[]是按时间升序排列的,我们显示的时候肯定是需要按照时间降序来给出的,所以这个地方根据pageNo、pageSize和邮件总数,我们可以计算出邮件实际位置来获取响应的邮件,即代码中的判断。
5. 通过注释二中的解释,相信大家已经明白这个地方我进行倒序循环的原因了吧,获取到的几封显示邮件也是按照时间升序排列的,所以这里我们倒序循环取出来的就是按照时间降序的邮件了。

特别注意一点:该方式每次收取邮件都会从邮件服务器上拉取邮件,并且每次拉取下来的邮件都需要进行解析才能进一步使用,这样会极大拉低系统性能,所以最好在收信的时候,重新开一个线程处理拉取和解析邮件的逻辑,然后将解析出来的邮件本地化(存数据库or存文件),用户一进入邮箱直接从数据库中查询邮件,并启用新线程同步拉取邮件服务器中的邮件,直接判断是否有新邮件,并更新数据库中的数据。如果再走缓存来干这个事,效率应该会更高。存文件的话主要想法是可以集成Solr(搜索引擎),对邮件进行全文搜索的时候效率和性能都会更好。各路大神如果看到这里有更好的解决方式,欢迎(请求、乞求、跪求!!!)你给我私信或者在文章下评论留下你的想法,合适的话小子一定修改并感谢各位🙏!

这里我给出几个常用邮箱IMAP协议所支持的Folder:

  • QQ邮箱
    INBOX
    Sent Messages
    Drafts
    Deleted Messages
    Junk
  • 126邮箱
    INBOX
    草稿箱
    已发送
    已删除
    垃圾邮件
    病毒邮件
    广告邮件
    订阅邮件
  • 新浪邮箱
    INBOX
    草稿夹
    已发送
    已删除
    垃圾邮件
    订阅邮件
    星标邮件
    商讯信息
    网站通知
    其它邮件
  • Gmail
    Drafts
    INBOX
    Notes
    [Gmail]
    垃圾邮件
    已删除邮件
    已加星标
    已发邮件
    所有邮件
    草稿
    重要
    工作邮件
    收据
    旅行相关
    私人邮件

三、邮件内容和附件解析

解析完文件头之后大家就可以在你们的webMail中显示邮件列表了,但是我们还需要�对邮件正文和附件进行解析,下面我来介绍一下我这拙劣的解析方式。

解析邮件之前,我们首先要弄清楚,这封邮件的内容可能会存在两种格式,一种是text类型的纯文本内容,一种是html类型的邮件,针对两种不同的格式,我们在页面上需要作出不同的显示方式,切忌两种格式都输出,这会是一种噩梦,让你自己都不知道你邮件显示的是什么内容。

public static void getHtmlContent(Part msg,StringBuffer content){
    boolean isContainTextAttach = msg.getContentType().indexOf("name") > 0;
    if (msg.isMimeType("text/html")){
        content.append(msg.getContent().toString);
    } else if (msg.isMimeType("message/rfc822")){
        getHtmlContent((Part) msg.getContent,content);
    } else if (msg.isMimeType("multipart/*")){
        Multipart multipart = (Multipart) msg.getContent();
        int partCount = multipart.getCount();
        for (int i = 0; i < partCount; i++) {
            BodyPart bodyPart = multipart.getBodyPart(i);
            getHtmlContent(bodyPart,content);
        }
    }
}

这个方法可以将邮件中的Html内容解析出来,这里可能需要解释一下这个方法的意思。首先大家需要知道一封邮件在邮件系统中是怎样存储的。

图有点拙劣,因为我找了很久都没找到合适的,只有在我当时开发时看的书上给大家截取这么一张,见谅。

这个就明确表示了邮件存储时Mime消息体的结构,每个消息单元都需要以一个分隔符开始和一个分隔符结束。这个就和POST请求Resource中的结构一样的,对这方面有研究的同学应该很容易理解。

看懂这个之后,就能理解为什么这个方法需要用递归来解析邮件html内容了,因为第一次传进来的就是第1部分,第1部分里面可能还会有很多部分,所以需要我们层层判断和解析。

邮件内容解析完成之后,我们需要对邮件附件进行解析了。邮件的附件可以直接以输出流的方式输出,也可以存在本机,我这边是存在本机提供下载的。

public static void saveAttachment(Part part,String dir){
    if (part.isMimeType("multipart/*")) {
        Multipart multipart = (Multipart) part.getContent();
        //复杂体邮件包含多个邮件体
        int partCount = multipart.getCount();
        for (int i=0; i<partCount;i++){
            //获得复杂体邮件中其中一个邮件体
            BodyPart bodyPart = multipart.getBodyPart(i);
            //某一个邮件体也有可能是由多个邮件体组成的复杂体
            String disp = bodyPart.getDisposition();
            if (disp != null && (disp.equalsIgnoreCase(Part.ATTACHMENT) || disp.equalsIgnoreCase(Part.INLINE))) {
                InputStream is = bodyPart.getInputStream();
                saveFile(is, destDir, decodeText(bodyPart.getFileName()));
            } else if (bodyPart.isMimeType("multipart/*")){
                saveAttachment(bodyPart,destDir);
            } else {
                String contentType = bodyPart.getContentType();
                if (contentType.indexOf("name") != -1 || contentType.indexOf("application") != -1) {
                    saveFile(bodyPart.getInputStream(), destDir, decodeText(bodyPart.getFileName()));
                }
            }
        }
    } else if (part.isMimeType("message/rfc822")){
        saveAttachment((Part) part.getContent(),destDir);
    }
}

这个方法主要判断是否为一个复杂邮件体(附件都是存在复杂邮件体中的),一个复杂邮件体中也可能会包含多个邮件体,所以要判断。当最后一层不是由多个邮件体构成的复杂体时,就可以直接获取输入流,并存入文件,中间出现一个方法saveFile(),这个方法就是通过输入流在指定路径创建文件的。

public static void saveFile(InputStream is, String destDir, String fileName){
    BufferedInputStream bis = new BufferedInputStream(is);
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(new File(destDir + fileName)));
    int len = -1;
    while ((len = bis.read()) != -1) {
        bos.write(len);
        bos.flush();
    }
    bos.close();
    bis.close();
}

到这里邮件系统第一部分完成了,可以接收邮件和解析邮件,然后发送邮件也很简单,请听下回分解!

相关文章

网友评论

    本文标题:JavaMail开发webMail不完全经验——之邮件接收和解析

    本文链接:https://www.haomeiwen.com/subject/tiyddxtx.html