Hi, I've often found similar issues to you in the past. Few developers bother to actually learn the complexities of IMAP so you end up with greatly simplified options.
Recently, I had a need to auto-process some inbound emails from a mailing list and because I also wanted to mark the email as read once processed AND move it out of the inbox to another folder, I needed to use something a bit more comprehensive.
I ended up adding these node.js packages to Node-RED via the function node:
IMAPFLOW does all the IMAP stuff and mailparser is needed to read and make sense of an email, cheerio extracts data from the HTML body of an email. Sanitize-html is for safety. url is a node.js native library.
You would likely need imapflow and something to create a well formed email from data.
Here is the code - obviously not what you are needing but gives you an idea about the possible complexities. Much of it deals with the HTML content which you probably don't need.
const outdoorActive = global.get('outdoorActive', 'file') ?? {}
// https://www.npmjs.com/package/imapflow
// https://imapflow.com/module-imapflow-ImapFlow.html
const { ImapFlow } = imapflow
// https://nodemailer.com/extras/mailparser/
const { simpleParser } = mailparser
// https://www.npmjs.com/package/sanitize-html
const sanitizeOpts = {
allowedTags: [
"address", "article", "aside", "footer", "header", "h1", "h2", "h3", "h4",
"h5", "h6", "hgroup", "main", "nav", "section", "blockquote", "dd", "div",
"dl", "dt", "figcaption", "figure", "hr", "li", "main", "ol", "p", "pre",
"ul", "a", "abbr", "b", "bdi", "bdo", "br", "cite", "code", "data", "dfn",
"em", "i", "kbd", "mark", "q", "rb", "rp", "rt", "rtc", "ruby", "s", "samp",
"small", "span", "strong", "sub", "sup", "time", "u", "var", "wbr", "caption",
"col", "colgroup", "table", "tbody", "td", "tfoot", "th", "thead", "tr",
"img", "html", "head", "body",
// "link", "meta",
],
allowedAttributes: {
'*': [
'id',
// 'class',
],
a: [ 'href', 'name', 'target' ],
img: [ 'src', 'srcset', 'alt', 'title', 'width', 'height', 'loading' ],
// meta: [ '*' ],
// link: [ 'href', 'type' ],
},
}
const client = new ImapFlow({
host: msg.host,
port: 993,
secure: true,
auth: {
user: msg.user,
pass: msg.pw,
},
logger: false,
emitLogs: true,
})
const main = async () => {
// Wait until client connects and authorizes
try {
await client.connect()
node.status({fill:"green",shape:"dot",text:"Connected"})
} catch (e) {
node.status({fill:"red",shape:"ring",text:"Failed to connect"})
msg.payload = e
node.error(`Could not connect to mail server. ${e.message}`, msg)
node.send(msg)
return
}
// These are all optional
// msg.serverInfo = client.serverInfo
// msg.capabilities = client.capabilities
// msg.mailboxes = await client.listTree()
// msg.inboxStatus = await client.status('INBOX', { recent: true, unseen: true, messages: true, })
// What properties to return for each email
const fetchOptions = {
// bodyStructure: true,
// envelope: true,
// flags: true,
// headers: true,
// headers: [ 'date', 'subject', 'to', 'from', 'X-Spam-Score', 'X-CampaignID', ],
// labels: true,
// size: true,
source: true,
// threadId: true,
// uid: true,
// bodyParts: [
// 'text',
// ],
}
// What to search for
const search = {
// seen: false,
// flagged: true,
from: 'for-you@news.outdooractive.com',
}
// Select and lock a mailbox. Throws if mailbox does not exist
let mb = await client.getMailboxLock(msg.mailbox)
node.status({fill:"blue",shape:"ring",text:"INBOX open"})
// client.mailbox includes information about currently selected mailbox
msg.inbox = client.mailbox
try {
const srch = client.fetch(search, fetchOptions, { uid: true })
node.send({topic: 'Messages found', payload: srch})
const foundIds = []
// Process all weekly routes emails in the inbox
for await ( let message of srch ) {
// Note the id so we can process them
foundIds.push(message.uid)
// msg.message = message
const mail = await simpleParser(message.source)
// msg.mail = mail
const received = mail.date
const $ = cheerio.load(sanitizeHtml(mail.html, sanitizeOpts))
const entries = $('body > div:nth-child(2) > div:nth-child(1) > table > tbody > tr > td > div:nth-child(5) > table > tbody > tr > td > div').find('div')
// Process each route in the current email - also updates the outdoorActive var
entries.each( (i, entry) => {
const d = $(entry).find('table > tbody > tr > td > table > tbody')
// Title/Who: body > div:nth-child(1) > table > tbody > tr > td > table > tbody > tr:nth-child(1) > td > table > tbody > tr:nth-child(1) > td > a
const d1 = $(d).find('tr:nth-child(1) > td > table > tbody > tr:nth-child(1) > td > a')
let u = new url.URL(d1[0].attribs.href)
let routeId = u.pathname.split('/').slice(-1)[0]
const title = {
href: `${u.origin}${u.pathname}`,
text: d1[0].firstChild.data,
}
const d2 = $(d).find('tr:nth-child(1) > td > table > tbody > tr:nth-child(2) > td > a')
u = new url.URL(d2[0].attribs.href)
const who = {
href: `${u.origin}${u.pathname}`,
text: d2[0].firstChild.data,
}
// Image: body > div:nth-child(1) > table > tbody > tr > td > table > tbody > tr:nth-child(2) > td > table > tbody > tr > td > a
const d3 = $(d).find('tr:nth-child(2) > td > table > tbody > tr > td > a > img')
const image = {
src: d3[0].attribs.src,
width: d3[0].attribs.width,
height: d3[0].attribs.height,
}
// Meta: body > div:nth-child(1) > table > tbody > tr > td > table > tbody > tr:nth-child(3) > td > table > tbody > tr:nth-child(2)
const d4 = $(d).find('tr:nth-child(3) > td > table > tbody > tr:nth-child(2) > td')
const details = {
distance: d4[0].firstChild.data,
time: d4[1].firstChild.data,
up: d4[2].firstChild.data,
down: d4[3].firstChild.data,
}
// console.log({i, title, who, image, details, d1, d2, d3, d4})
outdoorActive[routeId] = {title, who, image, details, received}
// node.send({payload: entry.html()})
})
}
// Clean up
if (foundIds.length > 0) {
// Mark emails as read by setting the \Seen flag
const r1 = await client.messageFlagsAdd({ uid: foundIds }, ['\\Seen'])
node.send({topic: 'Seen flag set?', payload: r1})
// Move emails to the target folder
const r2 = await client.messageMove({ uid: foundIds }, 'INBOX/2-Info/Sheffield/Walks-Cycling')
node.send({ topic: 'Messages moved?', payload: r2 })
}
// Mark the found emails as read & move to INBOX/2-Info/Sheffield/Walks-Cycling
// let r1 = await client.messageFlagsSet(srch, ['\Seen'])
// console.log(r1)
// let result = await client.messageMove('1:*', 'INBOX/2-Info/Sheffield/Walks-Cycling')
// console.log('Moved %s messages', result.uidMap.size)
} catch (e) {
node.error(`Something went wrong. ${e.message}`)
} finally {
// Make sure lock is released, otherwise next `getMailboxLock()` never returns
mb.release()
node.status({fill:"grey",shape:"dot",text:"INBOX Lock Released"})
}
// log out and close connection
await client.logout()
node.status({fill:"grey",shape:"ring",text:"Logged out"})
}
main().catch(err => node.error(err))
global.set('outdoorActive', outdoorActive, 'file')
// return msg