From b2ee7c3477d5269cc39ec457c0367572956bf53f Mon Sep 17 00:00:00 2001 From: cnouguier Date: Mon, 17 Jun 2024 07:40:38 +0200 Subject: [PATCH] wip: Provide hooks to manage IMAP connection #277 --- lib/hooks/hooks.imap.js | 119 ++++++++++++++++++++++++++++++++++------ 1 file changed, 103 insertions(+), 16 deletions(-) diff --git a/lib/hooks/hooks.imap.js b/lib/hooks/hooks.imap.js index 830b137c..5e53fb74 100644 --- a/lib/hooks/hooks.imap.js +++ b/lib/hooks/hooks.imap.js @@ -1,7 +1,9 @@ import _ from 'lodash' +import path from 'path' +import fs from 'fs' import { ImapFlow } from 'imapflow' import makeDebug from 'debug' -import { callOnHookItems } from '../utils.js' +import { callOnHookItems, getStoreFromHook } from '../utils.js' const debug = makeDebug('krawler:hooks:imap') @@ -46,39 +48,124 @@ export function disconnectIMAP (options = {}) { // List mailboxes export function listIMAPMailboxes (options = {}) { - async function listMailboxes (item, hook) { + return callOnHookItems(options)(async (item, hook) => { const client = _.get(hook.data, options.clientPath || 'client') - if (_.isNil(client)) throw new Error('You must provide client parameters to use the \'listIMAP\' hook') + if (_.isNil(client)) throw new Error('You must provide client parameters to use the \'listIMAPMailboxes\' hook') - debug(`[IMAP] listing mailboxes for ${hook.data.id}`) + debug(`[IMAP] ${hook.data.id}: listing mailboxes`) const mailboxes = await client.list(options) - console.log(mailboxes) _.set(hook, options.dataPath || 'result.data', mailboxes) - return hook - } - return callOnHookItems(options)(listMailboxes) + }) } -// List mailboxes +// List messages for a given mailbox export function fetchIMAPMessages (options = {}) { - async function fetchMessage (item, hook) { + return callOnHookItems(options)(async (item, hook) => { const client = _.get(hook.data, options.clientPath || 'client') - if (_.isNil(client)) throw new Error('You must provide client parameters to use the \'listIMAP\' hook') + if (_.isNil(client)) throw new Error('You must provide client parameters to use the \'fetchIMAPMessages\' hook') - debug(`[IMAP] fetching messages for ${hook.data.id}`) + debug(`[IMAP] ${hook.data.id}: fetching messages`) let lock = await client.getMailboxLock(options.mailbox) try { let messages = [] for await (const message of client.fetch(options.range, options.query, _.omit(options, ['mailbox', 'range', 'query', 'clientPath']))) { messages.push(message) } - console.log(messages) _.set(hook, options.dataPath || 'result.data', messages) } finally { // Make sure lock is released, otherwise next `getMailboxLock()` never returns lock.release(); } - return hook - } - return callOnHookItems(options)(fetchMessage) + }) +} + +// Downalod message attachment for a given mailbox +export function downloadIMAPAttachments (options = {}) { + return callOnHookItems(options)(async (item, hook) => { + const client = _.get(hook.data, options.clientPath || 'client') + if (_.isNil(client)) throw new Error('You must provide client parameters to use the \'downloadIMAPContent\' hook') + const store = await getStoreFromHook(hook, 'downloadIMAPAttachment', options) + + debug(`[IMAP] ${hook.data.id}: download attachments`) + let lock = await client.getMailboxLock(options.mailbox) + try { + let attachment = [] + const message = await client.fetchOne(options.range, { bodyStructure: true }) + const parts = _.get(message.bodyStructure, 'childNodes') + const filteredParts = _.filter(parts, part => part.type === options.type) + for (const part of filteredParts) { + // see example https://github.com/postalsys/imapflow/blob/master/examples/example.js#L242 + let {meta, content} = await client.download(message.seq, part.part, _.omit(options, ['mailbox', 'range', 'part', 'type', 'clientPath'])) + if (content) { + let stream = fs.createWriteStream(path.join(store.path, meta.filename)) + await new Promise(resolve => { + content.pipe(stream); + stream.once('finish', () => { + debug(`[IMAP] written attachement ${meta.filename}`) + resolve() + }) + }) + attachment.push(meta.filename) + } + } + if (attachment.length === 1) attachment = attachment[0] + else if (attachment.length === 0) attachment = null + _.set(hook, options.dataPath || 'result.data', attachment) + console.log(hook) + } finally { + // Make sure lock is released, otherwise next `getMailboxLock()` never returns + lock.release() + } + }) +} + +// Flag messages +export function flagIMAPMessages (options = {}) { + return callOnHookItems(options)(async (item, hook) => { + const client = _.get(hook.data, options.clientPath || 'client') + if (_.isNil(client)) throw new Error('You must provide client parameters to use the \'deleteIMAPMessages\' hook') + + debug(`[IMAP] ${hook.data.id}: flag messages`) + let lock = await client.getMailboxLock(options.mailbox) + try { + await client.messageFlagsAdd(options.range, options.flags, _.omit(options, ['mailbox', 'range', 'flags', 'clientPath'])) + } finally { + // Make sure lock is released, otherwise next `getMailboxLock()` never returns + lock.release() + } + }) +} + +// Unflag messages +export function unflagIMAPMessages (options = {}) { + return callOnHookItems(options)(async (item, hook) => { + const client = _.get(hook.data, options.clientPath || 'client') + if (_.isNil(client)) throw new Error('You must provide client parameters to use the \'deleteIMAPMessages\' hook') + + debug(`[IMAP] ${hook.data.id}: unflag messages`) + let lock = await client.getMailboxLock(options.mailbox) + try { + await client.messageFlagsRemove(options.range, options.flags, _.omit(options, ['mailbox', 'range', 'flags', 'clientPath'])) + } finally { + // Make sure lock is released, otherwise next `getMailboxLock()` never returns + lock.release() + } + }) +} + +// Delete messages +export function deleteIMAPMessages (options = {}) { + return callOnHookItems(options)(async (item, hook) => { + const client = _.get(hook.data, options.clientPath || 'client') + if (_.isNil(client)) throw new Error('You must provide client parameters to use the \'deleteIMAPMessages\' hook') + + debug(`[IMAP] ${hook.data.id}: delete messages`) + let lock = await client.getMailboxLock(options.mailbox) + try { + await client.messageDelete(options.range, _.omit(options, ['mailbox', 'range', 'clientPath'])) + } finally { + // Make sure lock is released, otherwise next `getMailboxLock()` never returns + lock.release(); + } + }) } \ No newline at end of file