I’m using node-imap and I can’t find a straightforward code example of how to save attachments from emails fetched using node-imap to disk using fs.
I’ve read the documentation a couple of times. It appears to me I should do another fetch with a reference to the specific part of a message being the attachment. I started of with the 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's 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 output with the attachment 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 do I read that file and save it to disk using node’s fs module?
Advertisement
Answer
I figured it out thanks to help of @arnt and mscdex. Here’s a complete and working script that streams all attachments as files to disk while base64 decoding them on the fly. Pretty 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();