Reputation: 11157
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: '[email protected]',
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?
Upvotes: 10
Views: 27433
Reputation: 21
Since everything is updated instead of using pipe method you could directly use this approach to write your file. `
const Imap = require("imap")
const fs = require("fs")
const { simpleParser } = require("mailparser")
const imapConfig = {
user: "your mail",
password: "your password",
host: "imap.gmail.com",
port: 993,
tls: true,
tlsOptions: { rejectUnauthorized: false },
}
const getEmails = () => {
try {
const imap = new Imap(imapConfig)
imap.once("ready", () => {
imap.openBox("INBOX", false, () => {
//to fetch latest 25 emails
const currentDate = new Date()
const daysAgo = 25
const sinceDate = new Date(currentDate)
sinceDate.setDate(currentDate.getDate() - daysAgo)
imap.search([["SINCE", sinceDate]], (err, result) => {
if (err) {
console.error("Error while searching for messages:", err)
imap.end()
return
}
if (result.length === 0) {
console.log("No messages to fetch.")
imap.end()
return
}
const mails = imap.fetch(result, { bodies: "" })
console.log("Fetching emails...")
mails.on("message", (msg) => {
let attachments = []
msg.on("body", (stream) => {
simpleParser(stream, async (err, parsed) => {
if (err) {
console.log("Error while parsing", err)
} else {
attachments = parsed.attachments || []
// console.log("Subject:", parsed.subject)
// console.log("Text body:", parsed.text)
// console.log("attachments:", attachments)
if (attachments.length) {
attachments.forEach((attachment, index) => {
if (attachment?.contentType === "application/pdf") {
console.log("pdf file found", attachment.content)
let fileName = attachment?.filename
fileName = fileName.replace(".docx", "")
console.log("fileName", fileName)
const fileStream = fs.createWriteStream(
`./attachments/attachment_${index}_${fileName}`,
{ encoding: "binary" }
)
fileStream.write(attachment.content)
// Close the file stream
fileStream.end()
fileStream.on("finish", () => {
console.log(` Done writing to file ${fileName}`)
})
}
})
}
}
})
})
msg.once("attributes", (attrs) => {
const { uid } = attrs
// imap.addFlags(uid, ["\\Seen"], () => {
// console.log("Here Marked as read")
// })
})
})
mails.once("error", (ex) => {
return Promise.reject(ex)
})
mails.once("end", () => {
console.log("Done fetching all messages!")
imap.end()
})
})
})
})
imap.once("error", (err) => {
console.log("error:", err)
})
imap.once("end", () => {
console.log("Connection ended")
})
imap.connect()
} catch (error) {
console.log("Error while getting the email:-", error)
}
}
getEmails()
`
Upvotes: 1
Reputation: 9
The below explanation is for downloading attachments from Emails, also there a flag (markAsRead) set it true to read only UNREAD mails, ans false for downloading all attachments.
Fetching only unread/unseen emails: you have to wrap the fetching call in the callback for a search one, like this:
imap.search(
['UNSEEN'],
function(err, results) {
// current code
}
);
Marking emails as read: was a tricky one for me, at line 53, the call to open the inbox is something like this: imap.openBox('INBOX', true, function(err, box) { the second parameter (the true value) is for opening the inbox in readonly mode. You need to change it to false and then, add a field markSeen: true in the second parameter:
var f = imap.seq.fetch('1:*', {
bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)'],
struct: true,
markSeen: true // <---- this is new
});
So, this is the script I'm using right now, changes are:
Mark mails as read: if the config option imapOptions.markAsRead is set to true it will mark processed mails as read. Filename Format: there is a config option (downloads.filenameFormat) that can be used to rename files. It's really simple. If you set it to $FILENAME or just remove it, it will keep the original file name. I included it because people are sending files named the same, but with different content and I need to keep them all. Logs: I've added logs using the simple-node-logger package. The scripts uses two levels: debug which shows everything from the original script and info which are simpler logs. Also uses the error level in case you only need that.
const config = require('./config.json');
const markAsRead = (config.imapOptions && config.imapOptions.markAsRead) ? config.imapOptions.markAsRead : false;
const fs = require('fs');
const { Base64Decode } = require('base64-stream')
const Imap = require('imap');
const imap = new Imap(config.imap);
// Simple logger:
const logger = require('simple-node-logger').createSimpleLogger( config.logs?.simpleNodeLogger || { logFilePath:'mail-downloader.log', timestampFormat:'YYYY-MM-DD HH:mm:ss.SSS' } );
logger.setLevel(config.logs?.level || 'debug');
// var emailDate;
// var emailFrom;
function formatFilename(filename, emailFrom, emailDate) {
// defaults to current filename:
let name = filename;
// if custom config is present:
if (config.downloads) {
// if format provided, use it to build filename:
if (config.downloads.filenameFormat) {
name = config.downloads.filenameFormat;
// converts from field from "Full Name <[email protected]>" into "fullname":
name = name.replace('$FROM', emailFrom.replace(/.*</i, '').replace('>', '').replace(/@.*/i, ''));
// parses text date and uses timestamp:
name = name.replace('$DATE', new Date(emailDate).getTime());
name = name.replace('$FILENAME', filename);
}
// if directory provided, use it:
if (config.downloads.directory) name = `${config.downloads.directory}/${name}`;
}
// return formatted filename:
return name;
}
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(struct[i].disposition.type.toLowerCase()) > -1) {
attachments.push(struct[i]);
}
}
}
return attachments;
}
function buildAttMessageFunction(attachment, emailFrom, emailDate) {
const filename = attachment.params.name;
const 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;
logger.debug(prefix + 'Streaming this attachment to file', filename, info);
var writeStream = fs.createWriteStream(formatFilename(filename, emailFrom, emailDate));
writeStream.on('finish', function() {
logger.debug(prefix + 'Done writing to file %s', filename);
});
//so we decode during streaming using
if (encoding.toLowerCase() === 'base64') {
//the stream is base64 encoded, so here the stream is decode on the fly and piped to the write stream (file)
stream.pipe(new Base64Decode()).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() {
logger.debug(prefix + 'Finished attachment %s', filename);
logger.info(`Attachment downloaded: ${filename}`)
});
};
}
imap.once('ready', function() {
logger.info('Connected');
imap.openBox('INBOX', !markAsRead, function(err, box) {
if (err) throw err;
imap.search(
['UNSEEN'],
function(err, results) {
if (err) throw err;
if (!results.length) {
// if now unread messages, log and end connection:
logger.info('No new emails found');
imap.end();
} else {
logger.info(`Found ${results.length} unread emails`)
// if unread messages, fetch and process:
var f = imap.fetch(results, {
bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)'],
struct: true,
markSeen: markAsRead
});
f.on('message', function (msg, seqno) {
logger.debug('Message #%d', seqno);
const prefix = '(#' + seqno + ') ';
var emailDate;
var emailFrom;
msg.on(
'body',
function(stream, info) {
var buffer = '';
stream.on('data', function(chunk) {
buffer += chunk.toString('utf8');
});
stream.once('end', function() {
const parsedHeader = Imap.parseHeader(buffer);
logger.debug(prefix + 'Parsed header: %s', parsedHeader);
// set to global vars so they can be used later to format filename:
emailFrom = parsedHeader.from[0];
emailDate = parsedHeader.date[0];
logger.info(`Email from ${emailFrom} with date ${emailDate}`);
});
}
);
msg.once(
'attributes',
function(attrs) {
const attachments = findAttachmentParts(attrs.struct);
logger.debug(prefix + 'Has attachments: %d', attachments.length);
logger.info(`Email with ${attachments.length} attachemnts`);
for (var i = 0, len=attachments.length ; i < len; ++i) {
const attachment = attachments[i];
logger.debug(prefix + 'Fetching attachment %s', attachment.params.name);
var f = imap.fetch(attrs.uid , {
bodies: [attachment.partID],
struct: true
});
//build function to process attachment message
f.on('message', buildAttMessageFunction(attachment, emailFrom, emailDate));
}
}
);
msg.once(
'end',
function() {
logger.debug(prefix + 'Finished email');
}
);
});
f.once('error', function(err) {
logger.error('Fetch error: ' + err);
});
f.once('end', function() {
logger.info('Done fetching all messages!');
imap.end();
});
}
}
);
});
});
imap.once('error', function(err) {
logger.error(err);
});
imap.once('end', function() {
logger.info('Connection ended');
});
imap.connect();
and this is the config file with the new options:
{
"imap": {
"user": "[email protected]",
"password": "myPassword",
"host": "myImapServer",
"port": 993,
"tls": true
},
"imapOptions": {
"markAsRead": false
},
"downloads": {
"directory": "./downloads",
"filenameFormat": "$DATE_$FROM_$FILENAME"
},
"logs": {
"level": "info",
"simpleNodeLogger": {
"logFilePath": "mail-downloader.log",
"timestampFormat": "YYYY-MM-DD HH:mm:ss.SSS"
}
}
}
Cheers!
Upvotes: 0
Reputation: 85
you can also use this its work for me.for gmail.
const IMAP = require("imap");
const MailParser = require("mailparser").MailParser;
const moment = require('moment');
var fs = require('fs'), fileStream;
module.exports.imapEmailDownload = function () {
return new Promise(async (resolve, reject) => {
try {
const imapConfig = {
user: '[email protected]',
password: 'XXX@126',
host: 'imap.gmail.com',
port: '993',
tls: true,
tlsOptions: {
secureProtocol: 'TLSv1_method'
}
}
const imap = IMAP(imapConfig);
imap.once("ready", execute);
imap.once("error", function (err) {
console.error("Connection error: " + err.stack);
});
imap.connect();
function execute() {
imap.openBox("INBOX", false, function (err, mailBox) {
if (err) {
console.error(err);
return;
}
imap.search([["ON", moment().format('YYYY-MM-DD')]], function (err, results) {
if (!results || !results.length) { console.log("No unread mails"); imap.end(); return; }
/* mark as seen
imap.setFlags(results, ['\\Seen'], function(err) {
if (!err) {
console.log("marked as read");
} else {
console.log(JSON.stringify(err, null, 2));
}
});*/
var f = imap.fetch(results, { bodies: "" });
f.on("message", processMessage);
f.once("error", function (err) {
return Promise.reject(err);
});
f.once("end", function () {
imap.end();
});
});
});
}
function processMessage(msg, seqno) {
var parser = new MailParser({ streamAttachments: true });
parser.on("headers", function (headers) {
});
parser.on('data', data => {
if (data.type === 'text') {
console.log(seqno);
console.log(data.text); /* data.html*/
}
});
let data = ""
msg.on("body", function (stream) {
stream.on("data", function (chunk) {
data = data + chunk.toString("utf8");
parser.write(chunk.toString("utf8"));
});
stream.on("end", (chunk) => {
})
});
parser.on('attachment', async function (attachment, mail) {
let filepath = './download/';
let output = fs.createWriteStream(filepath + attachment.fileName);
attachment.stream.pipe(output).on("end", function () {
console.log("All the data in the file has been read");
}).on("close", function (err) {
console.log("Stream has been cloesd.");
});
});
msg.once("end", function () {
// console.log("Finished msg #" + seqno);
parser.end();
});
}
resolve();
} catch (error) {
console.log("error", error);
reject(error);
}
});};
Upvotes: 3
Reputation: 2086
based Christiaan Westerbeek
changed: 1. use =>, forEach; 2. the 2nd fetch don't need "struct".
Problem:
In some cases, the attachment's filename SHOLUD be attachment.disposition.params['filename*']. Please see "RFC2231 MIME Parameter Value and Encoded Word Extensions" & here.
const fs = require('fs')
const base64 = require('base64-stream')
const Imap = require('imap')
const imap = new Imap({
user: '[email protected]',
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()
Upvotes: 1
Reputation: 11157
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: '[email protected]',
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();
Upvotes: 31