How to read and save attachments using node-imap

I am using node-imap and I cannot find a simple example code on how to save attachments from emails received with node-imap to disk using fs.

I've read the documentation a couple of times. I think I have to make another choice by referencing the specific part of the message that is the attachment. I started with a basic example:

var Imap = require('imap'),
    inspect = require('util').inspect;

var imap = new Imap({
  user: 'mygmailname@gmail.com',
  password: 'mygmailpassword',
  host: 'imap.gmail.com',
  port: 993,
  tls: true
});

function openInbox(cb) {
  imap.openBox('INBOX', true, cb);
}

imap.once('ready', function() {
  openInbox(function(err, box) {
    if (err) throw err;
    var f = imap.seq.fetch('1:3', {
      bodies: 'HEADER.FIELDS (FROM TO SUBJECT DATE)',
      struct: true
    });
    f.on('message', function(msg, seqno) {
      console.log('Message #%d', seqno);
      var prefix = '(#' + seqno + ') ';
      msg.on('body', function(stream, info) {
        var buffer = '';
        stream.on('data', function(chunk) {
          buffer += chunk.toString('utf8');
        });
        stream.once('end', function() {
          console.log(prefix + 'Parsed header: %s', inspect(Imap.parseHeader(buffer)));
        });
      });
      msg.once('attributes', function(attrs) {
        console.log(prefix + 'Attributes: %s', inspect(attrs, false, 8));

        //Here were I imagine to need to do another fetch for the content of the message part...

      });
      msg.once('end', function() {
        console.log(prefix + 'Finished');
      });
    });
    f.once('error', function(err) {
      console.log('Fetch error: ' + err);
    });
    f.once('end', function() {
      console.log('Done fetching all messages!');
      imap.end();
    });
  });
});

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

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

imap.connect();

      

And this example works. This is the result with the nesting part:

 [ { partID: '2',
     type: 'application',
     subtype: 'octet-stream',
     params: { name: 'my-file.txt' },
     id: null,
     description: null,
     encoding: 'BASE64',
     size: 44952,
     md5: null,
     disposition:
      { type: 'ATTACHMENT',
        params: { filename: 'my-file.txt' } },
     language: null } ],

      

How can I read this file and save it to disk using the node fs module?

+3


source to share


2 answers


I figured it out thanks to the help of @arnt and mscdex . Here's a complete and working script that transfers all attachments as files to disk, while base64 decrypts them on the fly. Fairly scalable in terms of memory usage.



var inspect = require('util').inspect;
var fs      = require('fs');
var base64  = require('base64-stream');
var Imap    = require('imap');
var imap    = new Imap({
  user: 'mygmailname@gmail.com',
  password: 'mygmailpassword',
  host: 'imap.gmail.com',
  port: 993,
  tls: true
  //,debug: function(msg){console.log('imap:', msg);}
});

function toUpper(thing) { return thing && thing.toUpperCase ? thing.toUpperCase() : thing;}

function findAttachmentParts(struct, attachments) {
  attachments = attachments ||  [];
  for (var i = 0, len = struct.length, r; i < len; ++i) {
    if (Array.isArray(struct[i])) {
      findAttachmentParts(struct[i], attachments);
    } else {
      if (struct[i].disposition && ['INLINE', 'ATTACHMENT'].indexOf(toUpper(struct[i].disposition.type)) > -1) {
        attachments.push(struct[i]);
      }
    }
  }
  return attachments;
}

function buildAttMessageFunction(attachment) {
  var filename = attachment.params.name;
  var encoding = attachment.encoding;

  return function (msg, seqno) {
    var prefix = '(#' + seqno + ') ';
    msg.on('body', function(stream, info) {
      //Create a write stream so that we can stream the attachment to file;
      console.log(prefix + 'Streaming this attachment to file', filename, info);
      var writeStream = fs.createWriteStream(filename);
      writeStream.on('finish', function() {
        console.log(prefix + 'Done writing to file %s', filename);
      });

      //stream.pipe(writeStream); this would write base64 data to the file.
      //so we decode during streaming using 
      if (toUpper(encoding) === 'BASE64') {
        //the stream is base64 encoded, so here the stream is decode on the fly and piped to the write stream (file)
        stream.pipe(base64.decode()).pipe(writeStream);
      } else  {
        //here we have none or some other decoding streamed directly to the file which renders it useless probably
        stream.pipe(writeStream);
      }
    });
    msg.once('end', function() {
      console.log(prefix + 'Finished attachment %s', filename);
    });
  };
}

imap.once('ready', function() {
  imap.openBox('INBOX', true, function(err, box) {
    if (err) throw err;
    var f = imap.seq.fetch('1:3', {
      bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)'],
      struct: true
    });
    f.on('message', function (msg, seqno) {
      console.log('Message #%d', seqno);
      var prefix = '(#' + seqno + ') ';
      msg.on('body', function(stream, info) {
        var buffer = '';
        stream.on('data', function(chunk) {
          buffer += chunk.toString('utf8');
        });
        stream.once('end', function() {
          console.log(prefix + 'Parsed header: %s', Imap.parseHeader(buffer));
        });
      });
      msg.once('attributes', function(attrs) {
        var attachments = findAttachmentParts(attrs.struct);
        console.log(prefix + 'Has attachments: %d', attachments.length);
        for (var i = 0, len=attachments.length ; i < len; ++i) {
          var attachment = attachments[i];
          /*This is how each attachment looks like {
              partID: '2',
              type: 'application',
              subtype: 'octet-stream',
              params: { name: 'file-name.ext' },
              id: null,
              description: null,
              encoding: 'BASE64',
              size: 44952,
              md5: null,
              disposition: { type: 'ATTACHMENT', params: { filename: 'file-name.ext' } },
              language: null
            }
          */
          console.log(prefix + 'Fetching attachment %s', attachment.params.name);
          var f = imap.fetch(attrs.uid , { //do not use imap.seq.fetch here
            bodies: [attachment.partID],
            struct: true
          });
          //build function to process attachment message
          f.on('message', buildAttMessageFunction(attachment));
        }
      });
      msg.once('end', function() {
        console.log(prefix + 'Finished email');
      });
    });
    f.once('error', function(err) {
      console.log('Fetch error: ' + err);
    });
    f.once('end', function() {
      console.log('Done fetching all messages!');
      imap.end();
    });
  });
});

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

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

imap.connect();

      

+12


source


founded by Christiaan Westerbeek

changed: 1. use =>, forEach; 2. 2nd choice doesn't need "struct".

Problem:



In some cases, the filename SHOLUD attachment will be attachment.disposition.params ['filename *']. See "RFC2231 MIME Parameter Value and Word Coded Extensions" here too .

const fs = require('fs')
const base64 = require('base64-stream')
const Imap = require('imap')

const imap = new Imap({
  user: 'XXX@126.com',
  password: 'XXXXX',
  host: 'imap.126.com',
  port: 993,
  tls: true /*,
  debug: (msg) => {console.log('imap:', msg);} */
});

function toUpper(thing) { return thing && thing.toUpperCase ? thing.toUpperCase() : thing }

function findAttachmentParts(struct, attachments) {
  attachments = attachments ||  []
  struct.forEach((i) => {
    if (Array.isArray(i)) findAttachmentParts(i, attachments)
    else if (i.disposition && ['INLINE', 'ATTACHMENT'].indexOf(toUpper(i.disposition.type)) > -1) {
      attachments.push(i)
    }
  })
  return attachments
}

imap.once('ready', () => {
  // A4 EXAMINE "INBOX"
  imap.openBox('INBOX', true, (err, box) => { 
    if (err) throw err;
    // A5 FETCH 1:3 (UID FLAGS INTERNALDATE BODYSTRUCTURE BODY.PEEK[HEADER.FIELDS (SUBJECT DATE)])
    const f = imap.seq.fetch('1:3', {
      bodies: ['HEADER.FIELDS (SUBJECT)'],
      struct: true  // BODYSTRUCTURE
    }) 
    f.on('message', (msg, seqno) => {
      console.log('Message #%d', seqno)
      const prefix = `(#${seqno})`
      var header = null
      msg.on('body', (stream, info) => {
        var buffer = ''
        stream.on('data', (chunk) => { buffer += chunk.toString('utf8') });
        stream.once('end', () => { header = Imap.parseHeader(buffer) })
      });
      msg.once('attributes', (attrs) => {
        const attachments = findAttachmentParts(attrs.struct);
        console.log(`${prefix} uid=${attrs.uid} Has attachments: ${attachments.length}`);
        attachments.forEach((attachment) => {
        /* 
          RFC2184 MIME Parameter Value and Encoded Word Extensions
                  4.Parameter Value Character Set and Language Information
          RFC2231 Obsoletes: 2184
          {
            partID: "2",
            type: "image",
            subtype: "jpeg",
            params: {
    X         "name":"________20.jpg",
              "x-apple-part-url":"8C33222D-8ED9-4B10-B05D-0E028DEDA92A"
            },
            id: null,
            description: null,
            encoding: "base64",
            size: 351314,
            md5: null,
            disposition: {
              type: "inline",
              params: {
    V           "filename*":"GB2312''%B2%E2%CA%D4%B8%BD%BC%FE%D2%BB%5F.jpg"
              }
            },
            language: null
          }   */            
          console.log(`${prefix} Fetching attachment $(attachment.params.name)`)
          console.log(attachment.disposition.params["filename*"])
          const filename = attachment.params.name  // need decode disposition.params['filename*'] !!!
          const encoding = toUpper(attachment.encoding)
          // A6 UID FETCH {attrs.uid} (UID FLAGS INTERNALDATE BODY.PEEK[{attachment.partID}])
          const f = imap.fetch(attrs.uid, { bodies: [attachment.partID] })
          f.on('message', (msg, seqno) => {
            const prefix = `(#${seqno})`
            msg.on('body', (stream, info) => {
              const writeStream = fs.createWriteStream(filename);
              writeStream.on('finish', () => { console.log(`${prefix} Done writing to file ${filename}`) })
              if (encoding === 'BASE64') stream.pipe(base64.decode()).pipe(writeStream)
              else stream.pipe(writeStream)
            })
            msg.once('end', () => { console.log(`${prefix} Finished attachment file${filename}`) })
          })
          f.once('end', () => { console.log('WS: downloder finish') })
        })
      })
      msg.once('end', () => { console.log(`${prefix} Finished email`); })
    });
    f.once('error', (err) => { console.log(`Fetch error: ${err}`) })
    f.once('end', () => {
      console.log('Done fetching all messages!')
      imap.end()
    })
  })
})
imap.once('error', (err) => { console.log(err) })
imap.once('end', () => { console.log('Connection ended') })
imap.connect()

      

0


source







All Articles