nodejs 爬取 MIME 邮件 踩坑

1. 依赖

  • node-imap
  • mailparser-mit

    请务必使用mailparser-mit而不是mailparser依赖,后者实测没用,只提供一个SimpleParser,不能解析MIME

2. 概念

  • box :

    box是邮箱的结构,收件箱发件箱都是box,虽然收件箱大多约定俗成叫INBOX
    但是不同邮箱对其他box的定义不同,比如发件箱可能是SentSENTSent Messages

  • flags :

    打开邮箱之后,邮件的状态有对应的flag,比如已读未读之类的,可以在box的flags中找到,
    例如Answered, Flagged, Deleted, Draft, Seen ,这些并不是全部,具体的根据你打开的box

  • search :

    邮箱打开后,如果邮件量比较大,逐条查看并且过滤是很费事的,构造一定的查询条件是必要的。
    根据node-imap的文档上的search描述,你可以构建一些简单的search条件,他们之间的默认关系是且(AND)。

    但很不幸的告诉你,这东西十分难用,你要做好心理准备,因为你的大部分组合条件不会生效。

    当你使用OR使一个条件有多个可能的值时,
    你会发现这东西十分难用,需要套娃来保证[‘OR’,’value1‘,[‘OR’,’value2‘,[‘OR’,’value4‘,’value5‘]]] 这种方式嵌套多个

    还不如按照时间进行分页启动多个任务进行拉取,在获取邮件后写规则筛选

  • stream :

    node-imap的邮件读取可以使用流的方式,并且mailparser对象在初始化的时候可以设置参数使他以允许以流的方式给你附件,
    但是我在实测之后发现这个流没法用,大多文件都是空的,或者长度不足。建议从end事件回调里的邮件里取附件列表的Buffer对象

  • Buffer :

    Buffer对象的写入文件时默认的encodingutf8,但邮件的附件并不一定是,需要读取附件的transferEncoding属性

3. example code

  • 工具类 假设在 项目目录/utils/mail-tool/index.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    const Imap = require('imap');
    const MailParser = require("mailparser-mit").MailParser;
    const fs = require("fs");
    const Path = require("path")
    const pathPrefix = Path.join(__dirname, '../uploads/')

    exports.mailTool = {
    downloadAttachment: (user, start, end) => {
    return new Promise((resolve, reject) => {
    //构建一个Imap
    let imap = new Imap({
    user: user.user, //你的邮箱账号
    password: user.pwd, //你的邮箱密码
    host: 'imap.exmail.qq.com', //邮箱服务器的主机地址 以腾讯企业邮箱为例子
    port: 993, //邮箱服务器的端口地址
    tls: true, //使用安全传输协议
    tlsOptions: {rejectUnauthorized: false} //禁用对证书有效性的检查
    });

    //imap 就绪事件 的 回调
    imap.once('ready', function () {
    //首先获取BoxList,这关系到你要读取哪个邮箱,此处回调函数比较阴间,第一个参数是err,第二个才是boxes
    imap.getBoxes('', (err, res) => {
    console.log("获取所有信箱")
    console.log(Object.keys(res))
    })

    //打开一个box,收件箱是INBOX,这里我是打开的发件箱
    imap.openBox('Sent Messages', true, function (err, box) {

    console.log("打开邮箱")

    if (err) throw err;
    //这里就是阴间的 search条件了。具体参考node-imap的文档,flag可选项在box对象的flags里看
    //已知 SINCE + BEFORE是能识别的,日期格式支持好多种,目前在用yyyy-MM-dd
    let searchParam = ['ALL', ['SINCE', start], ['BEFORE', end]]
    imap.search(searchParam, function (err, results) {
    if (err) throw err;
    const f = imap.fetch(results, {bodies: ''});//抓取邮件

    f.on('message', function (msg, seqNo) {

    //这里就是我们的Parser了,亲测streamAttachments开了读到的流是空的,建议别开
    let mailParser = new MailParser({
    // streamAttachments: true
    });

    msg.on('body', async function (stream, info) {
    stream.pipe(mailParser)

    //邮件头内容
    mailParser.on("headers", function (headers) {
    console.log("邮件头信息>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
    console.log("邮件主题: " + headers.subject);
    console.log("发件人: " + headers.from);
    console.log("收件人: " + headers.to);
    console.log("x-qq-mid: " + headers['x-qq-mid']);
    console.log("date:" + headers.date)
    });

    //邮件内容

    mailParser.on('end', async function (mail) {
    /*

    做一些判断,不满满足的就return

    */
    //从发件者中取第一位
    let sender = mail.from[0].address.match(/^.+(?=@)/).join('')
    let path = 'mail/' + sender
    let dir = await createDir(path)

    if (mail.attachments && mail.attachments.length > 0) {
    mail.attachments.forEach((data, i) => {
    //把主题和文件名拼起来,替换非法字符为_
    let filename = `${mail.subject}_${data.fileName}`.replace(/[\x00\/\\:*?"<>|]/g, '_')
    //把buffer存文件,注意使用附件文件的transferEncoding
    writeFile(dir, filename, data.content, data.transferEncoding)
    })
    }
    })
    });
    msg.once('end', function () {
    console.log(seqNo + '完成');

    });
    });
    f.once('error', function (err) {
    console.log('抓取出现错误: ' + err);

    });
    f.once('end', function () {
    console.log('所有邮件抓取完成!');
    imap.end();
    });
    });
    });
    });

    imap.once('error', function (err) {
    console.log(err);
    reject()
    });

    imap.once('end', function () {
    console.log('Connection ended');
    resolve()
    });

    //开始启动imap链接
    imap.connect()

    })
    },
    getUserName(username = '') {
    return username.match(/^.+(?=@)/).join('')
    },

    }
    function createDir(path) {
    let dir = pathPrefix + path + "/"
    return new Promise((resolve, reject) => {
    const exists = fs.existsSync(dir);
    if (!exists) {
    fs.mkdir(dir, {recursive: true},function (err) {
    if (err) {
    reject(err);
    } else {
    console.log(`目录创建${dir}成功。`);
    resolve(dir)
    }
    });
    } else {
    resolve(dir)
    }
    })
    }

    function writeFile(path,name,file,type = 'binary'){
    return fs.writeFileSync(path + name,file,{encoding:type});
    }

  • 调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const mailTool = require('../../utils/mail-tool/').mailTool

    const user = {
    user:"gcnb@gcnb.com",
    pwd:"gcnb"
    }
    //异步无阻塞
    mailTool.downloadAttachment(user,'2019-05-01','2019-07-01').finally()
    mailTool.downloadAttachment(user,'2019-07-01','2019-09-01').finally()
    //同步阻塞
    await mailTool.downloadAttachment(user,'2019-05-01','2019-07-01')
    await mailTool.downloadAttachment(user,'2019-07-01','2019-09-01')

4. 后记

nodeJs还是挺方便的。