imapflow
Version:
IMAP Client for Node
212 lines (184 loc) • 7.38 kB
JavaScript
;
const NOOP_INTERVAL = 2 * 60 * 1000;
async function runIdle(connection) {
let response;
let preCheckWaitQueue = [];
try {
connection.idling = true;
//let idleSent = false;
let doneRequested = false;
let doneSent = false;
let canEnd = false;
let preCheck = async () => {
doneRequested = true;
if (canEnd && !doneSent) {
connection.log.debug({
src: 'c',
msg: `DONE`,
comment: `breaking IDLE`,
lockId: connection.currentLock?.lockId,
path: connection.mailbox && connection.mailbox.path
});
connection.write('DONE');
doneSent = true;
connection.idling = false;
connection.preCheck = false; // unset itself
while (preCheckWaitQueue.length) {
let { resolve } = preCheckWaitQueue.shift();
resolve();
}
}
};
let connectionPreCheck = () => {
let handler = new Promise((resolve, reject) => {
preCheckWaitQueue.push({ resolve, reject });
});
connection.log.trace({
msg: 'Requesting IDLE break',
lockId: connection.currentLock?.lockId,
path: connection.mailbox && connection.mailbox.path,
queued: preCheckWaitQueue.length,
doneRequested,
canEnd,
doneSent
});
preCheck().catch(err => connection.log.warn({ err, cid: connection.id }));
return handler;
};
connection.preCheck = connectionPreCheck;
response = await connection.exec('IDLE', false, {
onPlusTag: async () => {
connection.log.debug({ msg: `Initiated IDLE, waiting for server input`, lockId: connection.currentLock?.lockId, doneRequested });
canEnd = true;
if (doneRequested) {
try {
await preCheck();
} catch (err) {
connection.log.warn({ err, cid: connection.id });
}
}
},
onSend: () => {
//idleSent = true;
}
});
// unset before response.next() if preCheck function is not already cleared (usually is)
if (typeof connection.preCheck === 'function' && connection.preCheck === connectionPreCheck) {
connection.log.trace({
msg: 'Clearing pre-check function',
lockId: connection.currentLock?.lockId,
path: connection.mailbox && connection.mailbox.path,
queued: preCheckWaitQueue.length,
doneRequested,
canEnd,
doneSent
});
connection.preCheck = false;
while (preCheckWaitQueue.length) {
let { resolve } = preCheckWaitQueue.shift();
resolve();
}
}
response.next();
return;
} catch (err) {
connection.preCheck = false;
connection.idling = false;
connection.log.warn({ err, cid: connection.id });
while (preCheckWaitQueue.length) {
let { reject } = preCheckWaitQueue.shift();
reject(err);
}
return false;
}
}
// Listens for changes in mailbox
module.exports = async (connection, maxIdleTime) => {
if (connection.state !== connection.states.SELECTED) {
// nothing to do here
return;
}
if (connection.capabilities.has('IDLE')) {
let idleTimer;
let stillIdling = false;
let runIdleLoop = async () => {
if (maxIdleTime) {
idleTimer = setTimeout(() => {
if (connection.idling) {
if (typeof connection.preCheck === 'function') {
stillIdling = true;
// request IDLE break if IDLE has been running for allowed time
connection.log.trace({ msg: 'Max allowed IDLE time reached', cid: connection.id });
connection.preCheck().catch(err => connection.log.warn({ err, cid: connection.id }));
}
}
}, maxIdleTime);
}
let resp = await runIdle(connection);
clearTimeout(idleTimer);
if (stillIdling) {
stillIdling = false;
return runIdleLoop();
}
return resp;
};
return runIdleLoop();
}
let idleTimer;
return new Promise(resolve => {
if (!connection.currentSelectCommand) {
return resolve();
}
// no IDLE support, fallback to NOOP'ing
connection.preCheck = async () => {
connection.preCheck = false; // unset itself
clearTimeout(idleTimer);
connection.log.debug({ src: 'c', msg: `breaking NOOP loop` });
connection.idling = false;
resolve();
};
let selectCommand = connection.currentSelectCommand;
let idleCheck = async () => {
let response;
switch (connection.missingIdleCommand) {
case 'SELECT':
// FIXME: somehow a loop occurs after some time of idling with SELECT
connection.log.debug({ src: 'c', msg: `Running SELECT to detect changes in folder` });
response = await connection.exec(selectCommand.command, selectCommand.arguments);
break;
case 'STATUS':
{
let statusArgs = [selectCommand.arguments[0], []]; // path
for (let key of ['MESSAGES', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN']) {
statusArgs[1].push({ type: 'ATOM', value: key.toUpperCase() });
}
connection.log.debug({ src: 'c', msg: `Running STATUS to detect changes in folder` });
response = await connection.exec('STATUS', statusArgs);
}
break;
case 'NOOP':
default:
response = await connection.exec('NOOP', false, { comment: 'IDLE not supported' });
break;
}
response.next();
};
let noopInterval = maxIdleTime ? Math.min(NOOP_INTERVAL, maxIdleTime) : NOOP_INTERVAL;
let runLoop = () => {
idleCheck()
.then(() => {
clearTimeout(idleTimer);
idleTimer = setTimeout(runLoop, noopInterval);
})
.catch(err => {
clearTimeout(idleTimer);
connection.preCheck = false;
connection.log.warn({ err, cid: connection.id });
resolve();
});
};
connection.log.debug({ src: 'c', msg: `initiated NOOP loop` });
connection.idling = true;
runLoop();
});
};