Projektstart
This commit is contained in:
146
backend/node_modules/imapflow/lib/commands/append.js
generated
vendored
Normal file
146
backend/node_modules/imapflow/lib/commands/append.js
generated
vendored
Normal file
@@ -0,0 +1,146 @@
|
||||
'use strict';
|
||||
|
||||
const { formatFlag, canUseFlag, formatDateTime, normalizePath, encodePath, comparePaths, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Appends a message to a mailbox
|
||||
module.exports = async (connection, destination, content, flags, idate) => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state) || !destination) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof content === 'string') {
|
||||
content = Buffer.from(content);
|
||||
}
|
||||
|
||||
if (connection.capabilities.has('APPENDLIMIT')) {
|
||||
let appendLimit = connection.capabilities.get('APPENDLIMIT');
|
||||
if (typeof appendLimit === 'number' && appendLimit < content.length) {
|
||||
let err = new Error('Message content too big for APPENDLIMIT=' + appendLimit);
|
||||
err.serverResponseCode = 'APPENDLIMIT';
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
destination = normalizePath(connection, destination);
|
||||
|
||||
let expectExists = comparePaths(connection, connection.mailbox.path, destination);
|
||||
|
||||
flags = (Array.isArray(flags) ? flags : [].concat(flags || []))
|
||||
.map(flag => flag && formatFlag(flag.toString()))
|
||||
.filter(flag => flag && canUseFlag(connection.mailbox, flag));
|
||||
|
||||
let attributes = [{ type: 'ATOM', value: encodePath(connection, destination) }];
|
||||
|
||||
idate = idate ? formatDateTime(idate) : false;
|
||||
|
||||
if (flags.length || idate) {
|
||||
attributes.push(flags.map(flag => ({ type: 'ATOM', value: flag })));
|
||||
}
|
||||
|
||||
if (idate) {
|
||||
attributes.push({ type: 'STRING', value: idate }); // force quotes as required by date-time
|
||||
}
|
||||
|
||||
let isLiteral8 = false;
|
||||
if (connection.capabilities.has('BINARY') && !connection.disableBinary) {
|
||||
// Value is literal8 if it contains NULL bytes. The server must support the BINARY extension
|
||||
// and if it does not then send the value as a regular literal and hope for the best
|
||||
isLiteral8 = content.indexOf(Buffer.from([0])) >= 0;
|
||||
}
|
||||
|
||||
attributes.push({ type: 'LITERAL', value: content, isLiteral8 });
|
||||
|
||||
let map = { destination };
|
||||
if (connection.mailbox && connection.mailbox.path) {
|
||||
map.path = connection.mailbox.path;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec('APPEND', attributes, {
|
||||
untagged: expectExists
|
||||
? {
|
||||
EXISTS: async untagged => {
|
||||
map.seq = Number(untagged.command);
|
||||
|
||||
if (expectExists) {
|
||||
let prevCount = connection.mailbox.exists;
|
||||
if (map.seq !== prevCount) {
|
||||
connection.mailbox.exists = map.seq;
|
||||
connection.emit('exists', {
|
||||
path: connection.mailbox.path,
|
||||
count: map.seq,
|
||||
prevCount
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
: false
|
||||
});
|
||||
|
||||
let section = response.response.attributes && response.response.attributes[0] && response.response.attributes[0].section;
|
||||
if (section && section.length) {
|
||||
let responseCode = section[0] && typeof section[0].value === 'string' ? section[0].value : '';
|
||||
switch (responseCode.toUpperCase()) {
|
||||
case 'APPENDUID':
|
||||
{
|
||||
let uidValidity = section[1] && typeof section[1].value === 'string' && !isNaN(section[1].value) ? BigInt(section[1].value) : false;
|
||||
let uid = section[2] && typeof section[2].value === 'string' && !isNaN(section[2].value) ? Number(section[2].value) : false;
|
||||
if (uidValidity) {
|
||||
map.uidValidity = uidValidity;
|
||||
}
|
||||
if (uid) {
|
||||
map.uid = uid;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
response.next();
|
||||
|
||||
if (expectExists && !map.seq) {
|
||||
// try to use NOOP to get the new sequence number
|
||||
try {
|
||||
response = await connection.exec('NOOP', false, {
|
||||
untagged: {
|
||||
EXISTS: async untagged => {
|
||||
map.seq = Number(untagged.command);
|
||||
|
||||
if (expectExists) {
|
||||
let prevCount = connection.mailbox.exists;
|
||||
if (map.seq !== prevCount) {
|
||||
connection.mailbox.exists = map.seq;
|
||||
connection.emit('exists', {
|
||||
path: connection.mailbox.path,
|
||||
count: map.seq,
|
||||
prevCount
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
comment: 'Sequence not found from APPEND output'
|
||||
});
|
||||
response.next();
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
}
|
||||
}
|
||||
|
||||
if (map.seq && !map.uid) {
|
||||
let list = await connection.search({ seq: map.seq }, { uid: true });
|
||||
if (list && list.length) {
|
||||
map.uid = list[0];
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
174
backend/node_modules/imapflow/lib/commands/authenticate.js
generated
vendored
Normal file
174
backend/node_modules/imapflow/lib/commands/authenticate.js
generated
vendored
Normal file
@@ -0,0 +1,174 @@
|
||||
'use strict';
|
||||
|
||||
const { getStatusCode, getErrorText } = require('../tools.js');
|
||||
|
||||
async function authOauth(connection, username, accessToken) {
|
||||
let oauthbearer;
|
||||
let command;
|
||||
let breaker;
|
||||
|
||||
if (connection.capabilities.has('AUTH=OAUTHBEARER')) {
|
||||
oauthbearer = [`n,a=${username},`, `host=${connection.servername}`, `port=993`, `auth=Bearer ${accessToken}`, '', ''].join('\x01');
|
||||
command = 'OAUTHBEARER';
|
||||
breaker = 'AQ==';
|
||||
} else if (connection.capabilities.has('AUTH=XOAUTH') || connection.capabilities.has('AUTH=XOAUTH2')) {
|
||||
oauthbearer = [`user=${username}`, `auth=Bearer ${accessToken}`, '', ''].join('\x01');
|
||||
command = 'XOAUTH2';
|
||||
breaker = '';
|
||||
}
|
||||
|
||||
let errorResponse = false;
|
||||
try {
|
||||
let response = await connection.exec(
|
||||
'AUTHENTICATE',
|
||||
[
|
||||
{ type: 'ATOM', value: command },
|
||||
{ type: 'ATOM', value: Buffer.from(oauthbearer).toString('base64'), sensitive: true }
|
||||
],
|
||||
{
|
||||
onPlusTag: async resp => {
|
||||
if (resp.attributes && resp.attributes[0] && resp.attributes[0].type === 'TEXT') {
|
||||
try {
|
||||
errorResponse = JSON.parse(Buffer.from(resp.attributes[0].value, 'base64').toString());
|
||||
} catch (err) {
|
||||
connection.log.debug({ errorResponse: resp.attributes[0].value, err });
|
||||
}
|
||||
}
|
||||
|
||||
connection.log.debug({ src: 'c', msg: breaker, comment: `Error response for ${command}` });
|
||||
connection.write(breaker);
|
||||
}
|
||||
}
|
||||
);
|
||||
response.next();
|
||||
|
||||
connection.authCapabilities.set(`AUTH=${command}`, true);
|
||||
|
||||
return username;
|
||||
} catch (err) {
|
||||
let errorCode = getStatusCode(err.response);
|
||||
if (errorCode) {
|
||||
err.serverResponseCode = errorCode;
|
||||
}
|
||||
err.authenticationFailed = true;
|
||||
err.response = await getErrorText(err.response);
|
||||
if (errorResponse) {
|
||||
err.oauthError = errorResponse;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function authLogin(connection, username, password) {
|
||||
let errorResponse = false;
|
||||
try {
|
||||
let response = await connection.exec('AUTHENTICATE', [{ type: 'ATOM', value: 'LOGIN' }], {
|
||||
onPlusTag: async resp => {
|
||||
if (resp.attributes && resp.attributes[0] && resp.attributes[0].type === 'TEXT') {
|
||||
let question = Buffer.from(resp.attributes[0].value, 'base64').toString();
|
||||
switch (
|
||||
question.toLowerCase().replace(/[:\x00]*$/, '') // eslint-disable-line no-control-regex
|
||||
) {
|
||||
case 'username':
|
||||
case 'user name': {
|
||||
let encodedUsername = Buffer.from(username).toString('base64');
|
||||
connection.log.debug({ src: 'c', msg: encodedUsername, comment: `Encoded username for AUTH=LOGIN` });
|
||||
connection.write(encodedUsername);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'password':
|
||||
connection.log.debug({ src: 'c', msg: '(* value hidden *)', comment: `Encoded password for AUTH=LOGIN` });
|
||||
connection.write(Buffer.from(password).toString('base64'));
|
||||
break;
|
||||
|
||||
default: {
|
||||
let error = new Error(`Unknown LOGIN question "${question}"`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
response.next();
|
||||
|
||||
connection.authCapabilities.set(`AUTH=LOGIN`, true);
|
||||
|
||||
return username;
|
||||
} catch (err) {
|
||||
let errorCode = getStatusCode(err.response);
|
||||
if (errorCode) {
|
||||
err.serverResponseCode = errorCode;
|
||||
}
|
||||
err.authenticationFailed = true;
|
||||
err.response = await getErrorText(err.response);
|
||||
if (errorResponse) {
|
||||
err.oauthError = errorResponse;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function authPlain(connection, username, password, authzid) {
|
||||
let errorResponse = false;
|
||||
try {
|
||||
let response = await connection.exec('AUTHENTICATE', [{ type: 'ATOM', value: 'PLAIN' }], {
|
||||
onPlusTag: async () => {
|
||||
// SASL PLAIN format: [authzid]\x00authcid\x00password
|
||||
// authzid: authorization identity (who to impersonate)
|
||||
// authcid: authentication identity (who is authenticating)
|
||||
let authzidValue = authzid || '';
|
||||
let encodedResponse = Buffer.from([authzidValue, username, password].join('\x00')).toString('base64');
|
||||
let loggedResponse = Buffer.from([authzidValue, username, '(* value hidden *)'].join('\x00')).toString('base64');
|
||||
connection.log.debug({ src: 'c', msg: loggedResponse, comment: `Encoded response for AUTH=PLAIN${authzid ? ' with authzid' : ''}` });
|
||||
connection.write(encodedResponse);
|
||||
}
|
||||
});
|
||||
|
||||
response.next();
|
||||
|
||||
connection.authCapabilities.set(`AUTH=PLAIN`, true);
|
||||
|
||||
// Return the identity we're authorized as (authzid if provided, otherwise username)
|
||||
return authzid || username;
|
||||
} catch (err) {
|
||||
let errorCode = getStatusCode(err.response);
|
||||
if (errorCode) {
|
||||
err.serverResponseCode = errorCode;
|
||||
}
|
||||
err.authenticationFailed = true;
|
||||
err.response = await getErrorText(err.response);
|
||||
if (errorResponse) {
|
||||
err.oauthError = errorResponse;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticates user using LOGIN
|
||||
module.exports = async (connection, username, { accessToken, password, loginMethod, authzid }) => {
|
||||
if (connection.state !== connection.states.NOT_AUTHENTICATED) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
// AUTH=OAUTHBEARER and AUTH=XOAUTH in the context of OAuth2 or very similar so we can handle these together
|
||||
if (connection.capabilities.has('AUTH=OAUTHBEARER') || connection.capabilities.has('AUTH=XOAUTH') || connection.capabilities.has('AUTH=XOAUTH2')) {
|
||||
return await authOauth(connection, username, accessToken);
|
||||
}
|
||||
}
|
||||
|
||||
if (password) {
|
||||
if ((!loginMethod && connection.capabilities.has('AUTH=PLAIN')) || loginMethod === 'AUTH=PLAIN') {
|
||||
return await authPlain(connection, username, password, authzid);
|
||||
}
|
||||
|
||||
if ((!loginMethod && connection.capabilities.has('AUTH=LOGIN')) || loginMethod === 'AUTH=LOGIN') {
|
||||
return await authLogin(connection, username, password);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Unsupported authentication mechanism');
|
||||
};
|
||||
20
backend/node_modules/imapflow/lib/commands/capability.js
generated
vendored
Normal file
20
backend/node_modules/imapflow/lib/commands/capability.js
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
// Refresh capabilities from server
|
||||
module.exports = async connection => {
|
||||
if (connection.capabilities.size && !connection.expectCapabilityUpdate) {
|
||||
return connection.capabilities;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
// untagged capability response is processed by global handler
|
||||
response = await connection.exec('CAPABILITY');
|
||||
|
||||
response.next();
|
||||
return connection.capabilities;
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
28
backend/node_modules/imapflow/lib/commands/close.js
generated
vendored
Normal file
28
backend/node_modules/imapflow/lib/commands/close.js
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
// Closes a mailbox
|
||||
module.exports = async connection => {
|
||||
if (connection.state !== connection.states.SELECTED) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec('CLOSE');
|
||||
response.next();
|
||||
|
||||
let currentMailbox = connection.mailbox;
|
||||
connection.mailbox = false;
|
||||
connection.currentSelectCommand = false;
|
||||
connection.state = connection.states.AUTHENTICATED;
|
||||
|
||||
if (currentMailbox) {
|
||||
connection.emit('mailboxClose', currentMailbox);
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
19
backend/node_modules/imapflow/lib/commands/compress.js
generated
vendored
Normal file
19
backend/node_modules/imapflow/lib/commands/compress.js
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
// Requests compression from server
|
||||
module.exports = async connection => {
|
||||
if (!connection.capabilities.has('COMPRESS=DEFLATE') || connection._inflate) {
|
||||
// nothing to do here
|
||||
return false;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec('COMPRESS', [{ type: 'ATOM', value: 'DEFLATE' }]);
|
||||
response.next();
|
||||
return true;
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
51
backend/node_modules/imapflow/lib/commands/copy.js
generated
vendored
Normal file
51
backend/node_modules/imapflow/lib/commands/copy.js
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
'use strict';
|
||||
|
||||
const { normalizePath, encodePath, expandRange, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Copies messages from current mailbox to some other mailbox
|
||||
module.exports = async (connection, range, destination, options) => {
|
||||
if (connection.state !== connection.states.SELECTED || !range || !destination) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
destination = normalizePath(connection, destination);
|
||||
|
||||
let attributes = [
|
||||
{ type: 'SEQUENCE', value: range },
|
||||
{ type: 'ATOM', value: encodePath(connection, destination) }
|
||||
];
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec(options.uid ? 'UID COPY' : 'COPY', attributes);
|
||||
response.next();
|
||||
|
||||
let map = { path: connection.mailbox.path, destination };
|
||||
let section = response.response.attributes && response.response.attributes[0] && response.response.attributes[0].section;
|
||||
let responseCode = section && section.length && section[0] && typeof section[0].value === 'string' ? section[0].value : '';
|
||||
switch (responseCode) {
|
||||
case 'COPYUID':
|
||||
{
|
||||
let uidValidity = section[1] && typeof section[1].value === 'string' && !isNaN(section[1].value) ? BigInt(section[1].value) : false;
|
||||
if (uidValidity) {
|
||||
map.uidValidity = uidValidity;
|
||||
}
|
||||
|
||||
let sourceUids = section[2] && typeof section[2].value === 'string' ? expandRange(section[2].value) : false;
|
||||
let destinationUids = section[3] && typeof section[3].value === 'string' ? expandRange(section[3].value) : false;
|
||||
if (sourceUids && destinationUids && sourceUids.length === destinationUids.length) {
|
||||
map.uidMap = new Map(sourceUids.map((uid, i) => [uid, destinationUids[i]]));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return map;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
76
backend/node_modules/imapflow/lib/commands/create.js
generated
vendored
Normal file
76
backend/node_modules/imapflow/lib/commands/create.js
generated
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
'use strict';
|
||||
|
||||
const { encodePath, normalizePath, getStatusCode, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Creates a new mailbox
|
||||
module.exports = async (connection, path) => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
path = normalizePath(connection, path);
|
||||
|
||||
let response;
|
||||
try {
|
||||
let map = {
|
||||
path
|
||||
};
|
||||
response = await connection.exec('CREATE', [{ type: 'ATOM', value: encodePath(connection, path) }]);
|
||||
|
||||
let section =
|
||||
response.response.attributes &&
|
||||
response.response.attributes[0] &&
|
||||
response.response.attributes[0].section &&
|
||||
response.response.attributes[0].section.length
|
||||
? response.response.attributes[0].section
|
||||
: false;
|
||||
|
||||
if (section) {
|
||||
let key;
|
||||
section.forEach((attribute, i) => {
|
||||
if (i % 2 === 0) {
|
||||
key = attribute && typeof attribute.value === 'string' ? attribute.value : false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
let value;
|
||||
switch (key.toLowerCase()) {
|
||||
case 'mailboxid':
|
||||
key = 'mailboxId';
|
||||
value = Array.isArray(attribute) && attribute[0] && typeof attribute[0].value === 'string' ? attribute[0].value : false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (key && value) {
|
||||
map[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
map.created = true;
|
||||
response.next();
|
||||
|
||||
//make sure we are subscribed to the new folder as well
|
||||
await connection.run('SUBSCRIBE', path);
|
||||
|
||||
return map;
|
||||
} catch (err) {
|
||||
let errorCode = getStatusCode(err.response);
|
||||
if (errorCode === 'ALREADYEXISTS') {
|
||||
// no need to do anything, mailbox already exists
|
||||
return {
|
||||
path,
|
||||
created: false
|
||||
};
|
||||
}
|
||||
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
31
backend/node_modules/imapflow/lib/commands/delete.js
generated
vendored
Normal file
31
backend/node_modules/imapflow/lib/commands/delete.js
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
const { encodePath, normalizePath, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Deletes an existing mailbox
|
||||
module.exports = async (connection, path) => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
path = normalizePath(connection, path);
|
||||
|
||||
if (connection.state === connection.states.SELECTED && connection.mailbox.path === path) {
|
||||
await connection.run('CLOSE');
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
let map = {
|
||||
path
|
||||
};
|
||||
response = await connection.exec('DELETE', [{ type: 'ATOM', value: encodePath(connection, path) }]);
|
||||
response.next();
|
||||
return map;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
43
backend/node_modules/imapflow/lib/commands/enable.js
generated
vendored
Normal file
43
backend/node_modules/imapflow/lib/commands/enable.js
generated
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
// Enables extensions
|
||||
module.exports = async (connection, extensionList) => {
|
||||
if (!connection.capabilities.has('ENABLE') || connection.state !== connection.states.AUTHENTICATED) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
extensionList = extensionList.filter(extension => connection.capabilities.has(extension.toUpperCase()));
|
||||
if (!extensionList.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
let enabled = new Set();
|
||||
response = await connection.exec(
|
||||
'ENABLE',
|
||||
extensionList.map(extension => ({ type: 'ATOM', value: extension.toUpperCase() })),
|
||||
{
|
||||
untagged: {
|
||||
ENABLED: async untagged => {
|
||||
if (!untagged.attributes || !untagged.attributes.length) {
|
||||
return;
|
||||
}
|
||||
untagged.attributes.forEach(attr => {
|
||||
if (attr.value && typeof attr.value === 'string') {
|
||||
enabled.add(attr.value.toUpperCase().trim());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
connection.enabled = enabled;
|
||||
response.next();
|
||||
return enabled;
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
41
backend/node_modules/imapflow/lib/commands/expunge.js
generated
vendored
Normal file
41
backend/node_modules/imapflow/lib/commands/expunge.js
generated
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
const { enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Deletes specified messages
|
||||
module.exports = async (connection, range, options) => {
|
||||
if (connection.state !== connection.states.SELECTED || !range) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
await connection.messageFlagsAdd(range, ['\\Deleted'], options);
|
||||
|
||||
let byUid = options.uid && connection.capabilities.has('UIDPLUS');
|
||||
let command = byUid ? 'UID EXPUNGE' : 'EXPUNGE';
|
||||
let attributes = byUid ? [{ type: 'SEQUENCE', value: range }] : false;
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec(command, attributes);
|
||||
|
||||
// A OK [HIGHESTMODSEQ 9122] Expunge completed (0.010 + 0.000 + 0.012 secs).
|
||||
let section = response.response.attributes && response.response.attributes[0] && response.response.attributes[0].section;
|
||||
let responseCode = section && section.length && section[0] && typeof section[0].value === 'string' ? section[0].value : '';
|
||||
if (responseCode.toUpperCase() === 'HIGHESTMODSEQ') {
|
||||
let highestModseq = section[1] && typeof section[1].value === 'string' && !isNaN(section[1].value) ? BigInt(section[1].value) : false;
|
||||
if (highestModseq && (!connection.mailbox.highestModseq || highestModseq > connection.mailbox.highestModseq)) {
|
||||
connection.mailbox.highestModseq = highestModseq;
|
||||
}
|
||||
}
|
||||
|
||||
response.next();
|
||||
return true;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
222
backend/node_modules/imapflow/lib/commands/fetch.js
generated
vendored
Normal file
222
backend/node_modules/imapflow/lib/commands/fetch.js
generated
vendored
Normal file
@@ -0,0 +1,222 @@
|
||||
'use strict';
|
||||
|
||||
const { formatMessageResponse } = require('../tools');
|
||||
|
||||
// Fetches emails from server
|
||||
module.exports = async (connection, range, query, options) => {
|
||||
if (connection.state !== connection.states.SELECTED || !range) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
let mailbox = connection.mailbox;
|
||||
|
||||
const commandKey = connection.capabilities.has('BINARY') && options.binary && !connection.disableBinary ? 'BINARY' : 'BODY';
|
||||
|
||||
let retryCount = 0;
|
||||
const maxRetries = 4;
|
||||
const baseDelay = 1000; // Start with 1 second delay
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
let messages = {
|
||||
count: 0,
|
||||
list: []
|
||||
};
|
||||
|
||||
let response;
|
||||
try {
|
||||
let attributes = [{ type: 'SEQUENCE', value: (range || '*').toString() }];
|
||||
|
||||
let queryStructure = [];
|
||||
|
||||
let setBodyPeek = (attributes, partial) => {
|
||||
let bodyPeek = {
|
||||
type: 'ATOM',
|
||||
value: `${commandKey}.PEEK`,
|
||||
section: [],
|
||||
partial
|
||||
};
|
||||
|
||||
if (Array.isArray(attributes)) {
|
||||
attributes.forEach(attribute => {
|
||||
bodyPeek.section.push(attribute);
|
||||
});
|
||||
} else if (attributes) {
|
||||
bodyPeek.section.push(attributes);
|
||||
}
|
||||
|
||||
queryStructure.push(bodyPeek);
|
||||
};
|
||||
|
||||
['all', 'fast', 'full', 'uid', 'flags', 'bodyStructure', 'envelope', 'internalDate'].forEach(key => {
|
||||
if (query[key]) {
|
||||
queryStructure.push({ type: 'ATOM', value: key.toUpperCase() });
|
||||
}
|
||||
});
|
||||
|
||||
if (query.size) {
|
||||
queryStructure.push({ type: 'ATOM', value: 'RFC822.SIZE' });
|
||||
}
|
||||
|
||||
if (query.source) {
|
||||
let partial;
|
||||
if (typeof query.source === 'object' && (query.source.start || query.source.maxLength)) {
|
||||
partial = [Number(query.source.start) || 0];
|
||||
if (query.source.maxLength && !isNaN(query.source.maxLength)) {
|
||||
partial.push(Number(query.source.maxLength));
|
||||
}
|
||||
}
|
||||
queryStructure.push({ type: 'ATOM', value: `${commandKey}.PEEK`, section: [], partial });
|
||||
}
|
||||
|
||||
// if possible, always request for unique email id
|
||||
if (connection.capabilities.has('OBJECTID')) {
|
||||
queryStructure.push({ type: 'ATOM', value: 'EMAILID' });
|
||||
} else if (connection.capabilities.has('X-GM-EXT-1')) {
|
||||
queryStructure.push({ type: 'ATOM', value: 'X-GM-MSGID' });
|
||||
}
|
||||
|
||||
if (query.threadId) {
|
||||
if (connection.capabilities.has('OBJECTID')) {
|
||||
queryStructure.push({ type: 'ATOM', value: 'THREADID' });
|
||||
} else if (connection.capabilities.has('X-GM-EXT-1')) {
|
||||
queryStructure.push({ type: 'ATOM', value: 'X-GM-THRID' });
|
||||
}
|
||||
}
|
||||
|
||||
if (query.labels) {
|
||||
if (connection.capabilities.has('X-GM-EXT-1')) {
|
||||
queryStructure.push({ type: 'ATOM', value: 'X-GM-LABELS' });
|
||||
}
|
||||
}
|
||||
|
||||
// always ask for modseq if possible
|
||||
if (connection.enabled.has('CONDSTORE') && !mailbox.noModseq) {
|
||||
queryStructure.push({ type: 'ATOM', value: 'MODSEQ' });
|
||||
}
|
||||
|
||||
// always make sure to include UID in the request as well even though server might auto-add it itself
|
||||
if (!query.uid) {
|
||||
queryStructure.push({ type: 'ATOM', value: 'UID' });
|
||||
}
|
||||
|
||||
if (query.headers) {
|
||||
if (Array.isArray(query.headers)) {
|
||||
setBodyPeek([{ type: 'ATOM', value: 'HEADER.FIELDS' }, query.headers.map(header => ({ type: 'ATOM', value: header }))]);
|
||||
} else {
|
||||
setBodyPeek({ type: 'ATOM', value: 'HEADER' });
|
||||
}
|
||||
}
|
||||
|
||||
if (query.bodyParts && query.bodyParts.length) {
|
||||
query.bodyParts.forEach(part => {
|
||||
if (!part) {
|
||||
return;
|
||||
}
|
||||
let key;
|
||||
let partial;
|
||||
if (typeof part === 'object') {
|
||||
if (!part.key || typeof part.key !== 'string') {
|
||||
return;
|
||||
}
|
||||
key = part.key.toUpperCase();
|
||||
if (part.start || part.maxLength) {
|
||||
partial = [Number(part.start) || 0];
|
||||
if (part.maxLength && !isNaN(part.maxLength)) {
|
||||
partial.push(Number(part.maxLength));
|
||||
}
|
||||
}
|
||||
} else if (typeof part === 'string') {
|
||||
key = part.toUpperCase();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
setBodyPeek({ type: 'ATOM', value: key }, partial);
|
||||
});
|
||||
}
|
||||
|
||||
if (queryStructure.length === 1) {
|
||||
queryStructure = queryStructure.pop();
|
||||
}
|
||||
|
||||
attributes.push(queryStructure);
|
||||
|
||||
if (options.changedSince && connection.enabled.has('CONDSTORE') && !mailbox.noModseq) {
|
||||
let changedSinceArgs = [
|
||||
{
|
||||
type: 'ATOM',
|
||||
value: 'CHANGEDSINCE'
|
||||
},
|
||||
{
|
||||
type: 'ATOM',
|
||||
value: options.changedSince.toString()
|
||||
}
|
||||
];
|
||||
|
||||
if (options.uid && connection.enabled.has('QRESYNC')) {
|
||||
changedSinceArgs.push({
|
||||
type: 'ATOM',
|
||||
value: 'VANISHED'
|
||||
});
|
||||
}
|
||||
|
||||
attributes.push(changedSinceArgs);
|
||||
}
|
||||
|
||||
response = await connection.exec(options.uid ? 'UID FETCH' : 'FETCH', attributes, {
|
||||
untagged: {
|
||||
FETCH: async untagged => {
|
||||
messages.count++;
|
||||
let formatted = await formatMessageResponse(untagged, mailbox);
|
||||
if (typeof options.onUntaggedFetch === 'function') {
|
||||
await new Promise((resolve, reject) => {
|
||||
options.onUntaggedFetch(formatted, err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
messages.list.push(formatted);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
response.next();
|
||||
return messages;
|
||||
} catch (err) {
|
||||
if (err.code === 'ETHROTTLE') {
|
||||
// Calculate exponential backoff delay
|
||||
const backoffDelay = Math.min(baseDelay * Math.pow(2, retryCount), 30000); // Cap at 30 seconds
|
||||
|
||||
// Use throttle reset time if provided and longer than backoff
|
||||
const delay = err.throttleReset && err.throttleReset > backoffDelay ? err.throttleReset : backoffDelay;
|
||||
|
||||
connection.log.warn({
|
||||
msg: 'Retrying throttled request with exponential backoff',
|
||||
cid: connection.id,
|
||||
code: err.code,
|
||||
response: err.responseText,
|
||||
throttleReset: err.throttleReset,
|
||||
retryCount,
|
||||
delayMs: delay
|
||||
});
|
||||
|
||||
// Wait before retrying
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
|
||||
retryCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
62
backend/node_modules/imapflow/lib/commands/id.js
generated
vendored
Normal file
62
backend/node_modules/imapflow/lib/commands/id.js
generated
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
'use strict';
|
||||
|
||||
const { formatDateTime } = require('../tools.js');
|
||||
|
||||
// Sends ID info to server and updates server info data based on response
|
||||
module.exports = async (connection, clientInfo) => {
|
||||
if (!connection.capabilities.has('ID')) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
let map = {};
|
||||
|
||||
// convert object into an array of value tuples
|
||||
let formattedClientInfo = !clientInfo
|
||||
? null
|
||||
: Object.keys(clientInfo)
|
||||
.map(key => [key, formatValue(key, clientInfo[key])])
|
||||
.filter(entry => entry[1])
|
||||
.flatMap(entry => entry);
|
||||
|
||||
if (formattedClientInfo && !formattedClientInfo.length) {
|
||||
// value array has no elements
|
||||
formattedClientInfo = null;
|
||||
}
|
||||
|
||||
response = await connection.exec('ID', [formattedClientInfo], {
|
||||
untagged: {
|
||||
ID: async untagged => {
|
||||
let params = untagged.attributes && untagged.attributes[0];
|
||||
let key;
|
||||
(Array.isArray(params) ? params : [].concat(params || [])).forEach((val, i) => {
|
||||
if (i % 2 === 0) {
|
||||
key = val.value;
|
||||
} else if (typeof key === 'string' && typeof val.value === 'string') {
|
||||
map[key.toLowerCase().trim()] = val.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
connection.serverInfo = map;
|
||||
response.next();
|
||||
return map;
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function formatValue(key, value) {
|
||||
switch (key.toLowerCase()) {
|
||||
case 'date':
|
||||
// Date has to be in imap date-time format
|
||||
return formatDateTime(value);
|
||||
default:
|
||||
// Other values are strings without newlines
|
||||
return (value || '').toString().replace(/\s+/g, ' ');
|
||||
}
|
||||
}
|
||||
211
backend/node_modules/imapflow/lib/commands/idle.js
generated
vendored
Normal file
211
backend/node_modules/imapflow/lib/commands/idle.js
generated
vendored
Normal file
@@ -0,0 +1,211 @@
|
||||
'use strict';
|
||||
|
||||
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();
|
||||
});
|
||||
};
|
||||
328
backend/node_modules/imapflow/lib/commands/list.js
generated
vendored
Normal file
328
backend/node_modules/imapflow/lib/commands/list.js
generated
vendored
Normal file
@@ -0,0 +1,328 @@
|
||||
'use strict';
|
||||
|
||||
const { decodePath, encodePath, normalizePath } = require('../tools.js');
|
||||
const { specialUse } = require('../special-use');
|
||||
|
||||
// Lists mailboxes from server
|
||||
module.exports = async (connection, reference, mailbox, options) => {
|
||||
options = options || {};
|
||||
|
||||
const FLAG_SORT_ORDER = ['\\Inbox', '\\Flagged', '\\Sent', '\\Drafts', '\\All', '\\Archive', '\\Junk', '\\Trash'];
|
||||
const SOURCE_SORT_ORDER = ['user', 'extension', 'name'];
|
||||
|
||||
let listCommand = connection.capabilities.has('XLIST') && !connection.capabilities.has('SPECIAL-USE') ? 'XLIST' : 'LIST';
|
||||
|
||||
let response;
|
||||
try {
|
||||
let entries = [];
|
||||
|
||||
let statusMap = new Map();
|
||||
let returnArgs = [];
|
||||
let statusQueryAttributes = [];
|
||||
|
||||
if (options.statusQuery) {
|
||||
Object.keys(options.statusQuery || {}).forEach(key => {
|
||||
if (!options.statusQuery[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (key.toUpperCase()) {
|
||||
case 'MESSAGES':
|
||||
case 'RECENT':
|
||||
case 'UIDNEXT':
|
||||
case 'UIDVALIDITY':
|
||||
case 'UNSEEN':
|
||||
statusQueryAttributes.push({ type: 'ATOM', value: key.toUpperCase() });
|
||||
break;
|
||||
|
||||
case 'HIGHESTMODSEQ':
|
||||
if (connection.capabilities.has('CONDSTORE')) {
|
||||
statusQueryAttributes.push({ type: 'ATOM', value: key.toUpperCase() });
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (listCommand === 'LIST' && connection.capabilities.has('LIST-STATUS') && statusQueryAttributes.length) {
|
||||
returnArgs.push({ type: 'ATOM', value: 'STATUS' }, statusQueryAttributes);
|
||||
if (connection.capabilities.has('SPECIAL-USE')) {
|
||||
returnArgs.push({ type: 'ATOM', value: 'SPECIAL-USE' });
|
||||
}
|
||||
}
|
||||
|
||||
let specialUseMatches = {};
|
||||
let addSpecialUseMatch = (entry, type, source) => {
|
||||
if (!specialUseMatches[type]) {
|
||||
specialUseMatches[type] = [];
|
||||
}
|
||||
specialUseMatches[type].push({ entry, source });
|
||||
};
|
||||
|
||||
let specialUseHints = {};
|
||||
if (options.specialUseHints && typeof options.specialUseHints === 'object') {
|
||||
for (let type of Object.keys(options.specialUseHints)) {
|
||||
if (
|
||||
['sent', 'junk', 'trash', 'drafts', 'archive'].includes(type) &&
|
||||
options.specialUseHints[type] &&
|
||||
typeof options.specialUseHints[type] === 'string'
|
||||
) {
|
||||
specialUseHints[normalizePath(connection, options.specialUseHints[type])] = `\\${type.replace(/^./, c => c.toUpperCase())}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let runList = async (reference, mailbox) => {
|
||||
const cmdArgs = [encodePath(connection, reference), encodePath(connection, mailbox)];
|
||||
|
||||
if (returnArgs.length) {
|
||||
cmdArgs.push({ type: 'ATOM', value: 'RETURN' }, returnArgs);
|
||||
}
|
||||
|
||||
response = await connection.exec(listCommand, cmdArgs, {
|
||||
untagged: {
|
||||
[listCommand]: async untagged => {
|
||||
if (!untagged.attributes || !untagged.attributes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let entry = {
|
||||
path: normalizePath(connection, decodePath(connection, (untagged.attributes[2] && untagged.attributes[2].value) || '')),
|
||||
pathAsListed: (untagged.attributes[2] && untagged.attributes[2].value) || '',
|
||||
flags: new Set(untagged.attributes[0].map(entry => entry.value)),
|
||||
delimiter: untagged.attributes[1] && untagged.attributes[1].value,
|
||||
listed: true
|
||||
};
|
||||
|
||||
if (specialUseHints[entry.path]) {
|
||||
addSpecialUseMatch(entry, specialUseHints[entry.path], 'user');
|
||||
}
|
||||
|
||||
if (listCommand === 'XLIST' && entry.flags.has('\\Inbox')) {
|
||||
// XLIST specific flag, ignore
|
||||
entry.flags.delete('\\Inbox');
|
||||
if (entry.path !== 'INBOX') {
|
||||
// XLIST may use localised inbox name
|
||||
addSpecialUseMatch(entry, '\\Inbox', 'extension');
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.path.toUpperCase() === 'INBOX') {
|
||||
addSpecialUseMatch(entry, '\\Inbox', 'name');
|
||||
}
|
||||
|
||||
if (entry.delimiter && entry.path.charAt(0) === entry.delimiter) {
|
||||
entry.path = entry.path.slice(1);
|
||||
}
|
||||
|
||||
entry.parentPath = entry.delimiter && entry.path ? entry.path.substr(0, entry.path.lastIndexOf(entry.delimiter)) : '';
|
||||
entry.parent = entry.delimiter ? entry.path.split(entry.delimiter) : [entry.path];
|
||||
entry.name = entry.parent.pop();
|
||||
|
||||
let { flag: specialUseFlag, source: flagSource } = specialUse(
|
||||
connection.capabilities.has('XLIST') || connection.capabilities.has('SPECIAL-USE'),
|
||||
entry
|
||||
);
|
||||
|
||||
if (specialUseFlag) {
|
||||
addSpecialUseMatch(entry, specialUseFlag, flagSource);
|
||||
}
|
||||
|
||||
entries.push(entry);
|
||||
},
|
||||
|
||||
STATUS: async untagged => {
|
||||
let statusPath = normalizePath(connection, decodePath(connection, (untagged.attributes[0] && untagged.attributes[0].value) || ''));
|
||||
let statusList = untagged.attributes && Array.isArray(untagged.attributes[1]) ? untagged.attributes[1] : false;
|
||||
if (!statusList || !statusPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
let key;
|
||||
|
||||
let map = { path: statusPath };
|
||||
|
||||
statusList.forEach((entry, i) => {
|
||||
if (i % 2 === 0) {
|
||||
key = entry && typeof entry.value === 'string' ? entry.value : false;
|
||||
return;
|
||||
}
|
||||
if (!key || !entry || typeof entry.value !== 'string') {
|
||||
return;
|
||||
}
|
||||
let value = false;
|
||||
switch (key.toUpperCase()) {
|
||||
case 'MESSAGES':
|
||||
key = 'messages';
|
||||
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
||||
break;
|
||||
|
||||
case 'RECENT':
|
||||
key = 'recent';
|
||||
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
||||
break;
|
||||
|
||||
case 'UIDNEXT':
|
||||
key = 'uidNext';
|
||||
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
||||
break;
|
||||
|
||||
case 'UIDVALIDITY':
|
||||
key = 'uidValidity';
|
||||
value = !isNaN(entry.value) ? BigInt(entry.value) : false;
|
||||
break;
|
||||
|
||||
case 'UNSEEN':
|
||||
key = 'unseen';
|
||||
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
||||
break;
|
||||
|
||||
case 'HIGHESTMODSEQ':
|
||||
key = 'highestModseq';
|
||||
value = !isNaN(entry.value) ? BigInt(entry.value) : false;
|
||||
break;
|
||||
}
|
||||
if (value === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
map[key] = value;
|
||||
});
|
||||
|
||||
statusMap.set(statusPath, map);
|
||||
}
|
||||
}
|
||||
});
|
||||
response.next();
|
||||
};
|
||||
|
||||
let normalizedReference = normalizePath(connection, reference || '');
|
||||
await runList(normalizedReference, normalizePath(connection, mailbox || '', true));
|
||||
|
||||
if (options.listOnly) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
if (normalizedReference && !specialUseMatches['\\Inbox']) {
|
||||
// INBOX was most probably not included in the listing if namespace was used
|
||||
await runList('', 'INBOX');
|
||||
}
|
||||
|
||||
if (options.statusQuery) {
|
||||
for (let entry of entries) {
|
||||
if (!entry.flags.has('\\Noselect') && !entry.flags.has('\\NonExistent')) {
|
||||
if (statusMap.has(entry.path)) {
|
||||
entry.status = statusMap.get(entry.path);
|
||||
} else if (!statusMap.size) {
|
||||
// run STATUS command
|
||||
try {
|
||||
entry.status = await connection.run('STATUS', entry.path, options.statusQuery);
|
||||
} catch (err) {
|
||||
entry.status = { error: err };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response = await connection.exec(
|
||||
'LSUB',
|
||||
[encodePath(connection, normalizePath(connection, reference || '')), encodePath(connection, normalizePath(connection, mailbox || '', true))],
|
||||
{
|
||||
untagged: {
|
||||
LSUB: async untagged => {
|
||||
if (!untagged.attributes || !untagged.attributes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let entry = {
|
||||
path: normalizePath(connection, decodePath(connection, (untagged.attributes[2] && untagged.attributes[2].value) || '')),
|
||||
pathAsListed: (untagged.attributes[2] && untagged.attributes[2].value) || '',
|
||||
flags: new Set(untagged.attributes[0].map(entry => entry.value)),
|
||||
delimiter: untagged.attributes[1] && untagged.attributes[1].value,
|
||||
subscribed: true
|
||||
};
|
||||
|
||||
if (entry.path.toUpperCase() === 'INBOX') {
|
||||
addSpecialUseMatch(entry, '\\Inbox', 'name');
|
||||
}
|
||||
|
||||
if (entry.delimiter && entry.path.charAt(0) === entry.delimiter) {
|
||||
entry.path = entry.path.slice(1);
|
||||
}
|
||||
|
||||
entry.parentPath = entry.delimiter && entry.path ? entry.path.substr(0, entry.path.lastIndexOf(entry.delimiter)) : '';
|
||||
entry.parent = entry.delimiter ? entry.path.split(entry.delimiter) : [entry.path];
|
||||
entry.name = entry.parent.pop();
|
||||
|
||||
let existing = entries.find(existing => existing.path === entry.path);
|
||||
if (existing) {
|
||||
existing.subscribed = true;
|
||||
entry.flags.forEach(flag => existing.flags.add(flag));
|
||||
} else {
|
||||
// ignore non-listed folders
|
||||
/*
|
||||
let specialUseFlag = specialUse(connection.capabilities.has('XLIST') || connection.capabilities.has('SPECIAL-USE'), entry);
|
||||
if (specialUseFlag && !flagsSeen.has(specialUseFlag)) {
|
||||
entry.specialUse = specialUseFlag;
|
||||
}
|
||||
entries.push(entry);
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
response.next();
|
||||
|
||||
for (let type of Object.keys(specialUseMatches)) {
|
||||
let sortedEntries = specialUseMatches[type].sort((a, b) => {
|
||||
let aSource = SOURCE_SORT_ORDER.indexOf(a.source);
|
||||
let bSource = SOURCE_SORT_ORDER.indexOf(b.source);
|
||||
if (aSource === bSource) {
|
||||
return a.entry.path.localeCompare(b.entry.path);
|
||||
}
|
||||
return aSource - bSource;
|
||||
});
|
||||
|
||||
if (!sortedEntries[0].entry.specialUse) {
|
||||
sortedEntries[0].entry.specialUse = type;
|
||||
sortedEntries[0].entry.specialUseSource = sortedEntries[0].source;
|
||||
}
|
||||
}
|
||||
|
||||
let inboxEntry = entries.find(entry => entry.specialUse === '\\Inbox');
|
||||
if (inboxEntry && !inboxEntry.subscribed) {
|
||||
// override server settings and make INBOX always as subscribed
|
||||
inboxEntry.subscribed = true;
|
||||
}
|
||||
|
||||
return entries.sort((a, b) => {
|
||||
if (a.specialUse && !b.specialUse) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.specialUse && b.specialUse) {
|
||||
return 1;
|
||||
}
|
||||
if (a.specialUse && b.specialUse) {
|
||||
return FLAG_SORT_ORDER.indexOf(a.specialUse) - FLAG_SORT_ORDER.indexOf(b.specialUse);
|
||||
}
|
||||
|
||||
let aList = [].concat(a.parent).concat(a.name);
|
||||
let bList = [].concat(b.parent).concat(b.name);
|
||||
|
||||
for (let i = 0; i < aList.length; i++) {
|
||||
let aPart = aList[i];
|
||||
let bPart = bList[i];
|
||||
if (aPart !== bPart) {
|
||||
return aPart.localeCompare(bPart || '');
|
||||
}
|
||||
}
|
||||
|
||||
return a.path.localeCompare(b.path);
|
||||
});
|
||||
} catch (err) {
|
||||
connection.log.warn({ msg: 'Failed to list folders', err, cid: connection.id });
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
31
backend/node_modules/imapflow/lib/commands/login.js
generated
vendored
Normal file
31
backend/node_modules/imapflow/lib/commands/login.js
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
const { getStatusCode, getErrorText } = require('../tools.js');
|
||||
|
||||
// Authenticates user using LOGIN
|
||||
module.exports = async (connection, username, password) => {
|
||||
if (connection.state !== connection.states.NOT_AUTHENTICATED) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let response = await connection.exec('LOGIN', [
|
||||
{ type: 'STRING', value: username },
|
||||
{ type: 'STRING', value: password, sensitive: true }
|
||||
]);
|
||||
response.next();
|
||||
|
||||
connection.authCapabilities.set('LOGIN', true);
|
||||
|
||||
return username;
|
||||
} catch (err) {
|
||||
let errorCode = getStatusCode(err.response);
|
||||
if (errorCode) {
|
||||
err.serverResponseCode = errorCode;
|
||||
}
|
||||
err.authenticationFailed = true;
|
||||
err.response = await getErrorText(err.response);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
34
backend/node_modules/imapflow/lib/commands/logout.js
generated
vendored
Normal file
34
backend/node_modules/imapflow/lib/commands/logout.js
generated
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
// Logs out user and closes connection
|
||||
module.exports = async connection => {
|
||||
if (connection.state === connection.states.LOGOUT) {
|
||||
// nothing to do here
|
||||
return false;
|
||||
}
|
||||
|
||||
if (connection.state === connection.states.NOT_AUTHENTICATED) {
|
||||
connection.state = connection.states.LOGOUT;
|
||||
connection.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec('LOGOUT');
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err.code === 'NoConnection') {
|
||||
return true;
|
||||
}
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
} finally {
|
||||
// close even if command failed
|
||||
connection.state = connection.states.LOGOUT;
|
||||
if (response && typeof response.next === 'function') {
|
||||
response.next();
|
||||
}
|
||||
connection.close();
|
||||
}
|
||||
};
|
||||
67
backend/node_modules/imapflow/lib/commands/move.js
generated
vendored
Normal file
67
backend/node_modules/imapflow/lib/commands/move.js
generated
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
'use strict';
|
||||
|
||||
const { normalizePath, encodePath, expandRange, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Moves messages from current mailbox to some other mailbox
|
||||
module.exports = async (connection, range, destination, options) => {
|
||||
if (connection.state !== connection.states.SELECTED || !range || !destination) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
destination = normalizePath(connection, destination);
|
||||
|
||||
let attributes = [
|
||||
{ type: 'SEQUENCE', value: range },
|
||||
{ type: 'ATOM', value: encodePath(connection, destination) }
|
||||
];
|
||||
|
||||
let map = { path: connection.mailbox.path, destination };
|
||||
|
||||
if (!connection.capabilities.has('MOVE')) {
|
||||
let result = await connection.messageCopy(range, destination, options);
|
||||
await connection.messageDelete(range, Object.assign({ silent: true }, options));
|
||||
return result;
|
||||
}
|
||||
|
||||
let checkMoveInfo = response => {
|
||||
let section = response.attributes && response.attributes[0] && response.attributes[0].section;
|
||||
let responseCode = section && section.length && section[0] && typeof section[0].value === 'string' ? section[0].value : '';
|
||||
switch (responseCode) {
|
||||
case 'COPYUID':
|
||||
{
|
||||
let uidValidity = section[1] && typeof section[1].value === 'string' && !isNaN(section[1].value) ? BigInt(section[1].value) : false;
|
||||
if (uidValidity) {
|
||||
map.uidValidity = uidValidity;
|
||||
}
|
||||
|
||||
let sourceUids = section[2] && typeof section[2].value === 'string' ? expandRange(section[2].value) : false;
|
||||
let destinationUids = section[3] && typeof section[3].value === 'string' ? expandRange(section[3].value) : false;
|
||||
if (sourceUids && destinationUids && sourceUids.length === destinationUids.length) {
|
||||
map.uidMap = new Map(sourceUids.map((uid, i) => [uid, destinationUids[i]]));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec(options.uid ? 'UID MOVE' : 'MOVE', attributes, {
|
||||
untagged: {
|
||||
OK: async untagged => {
|
||||
checkMoveInfo(untagged);
|
||||
}
|
||||
}
|
||||
});
|
||||
response.next();
|
||||
|
||||
checkMoveInfo(response.response);
|
||||
return map;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
107
backend/node_modules/imapflow/lib/commands/namespace.js
generated
vendored
Normal file
107
backend/node_modules/imapflow/lib/commands/namespace.js
generated
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
'use strict';
|
||||
|
||||
// Requests NAMESPACE info from server
|
||||
module.exports = async connection => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connection.capabilities.has('NAMESPACE')) {
|
||||
// try to derive from listing
|
||||
let { prefix, delimiter } = await getListPrefix(connection);
|
||||
if (delimiter && prefix && prefix.charAt(prefix.length - 1) !== delimiter) {
|
||||
prefix += delimiter;
|
||||
}
|
||||
let map = {
|
||||
personal: [{ prefix: prefix || '', delimiter }],
|
||||
other: false,
|
||||
shared: false
|
||||
};
|
||||
connection.namespaces = map;
|
||||
connection.namespace = connection.namespaces.personal[0];
|
||||
return connection.namespace;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
let map = {};
|
||||
response = await connection.exec('NAMESPACE', false, {
|
||||
untagged: {
|
||||
NAMESPACE: async untagged => {
|
||||
if (!untagged.attributes || !untagged.attributes.length) {
|
||||
return;
|
||||
}
|
||||
map.personal = getNamsepaceInfo(untagged.attributes[0]);
|
||||
map.other = getNamsepaceInfo(untagged.attributes[1]);
|
||||
map.shared = getNamsepaceInfo(untagged.attributes[2]);
|
||||
}
|
||||
}
|
||||
});
|
||||
connection.namespaces = map;
|
||||
|
||||
// make sure that we have the first personal namespace always set
|
||||
if (!connection.namespaces.personal[0]) {
|
||||
connection.namespaces.personal[0] = { prefix: '', delimiter: '.' };
|
||||
}
|
||||
connection.namespaces.personal[0].prefix = connection.namespaces.personal[0].prefix || '';
|
||||
response.next();
|
||||
|
||||
connection.namespace = connection.namespaces.personal[0];
|
||||
|
||||
return connection.namespace;
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return {
|
||||
error: true,
|
||||
status: err.responseStatus,
|
||||
text: err.responseText
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
async function getListPrefix(connection) {
|
||||
let response;
|
||||
try {
|
||||
let map = {};
|
||||
response = await connection.exec('LIST', ['', ''], {
|
||||
untagged: {
|
||||
LIST: async untagged => {
|
||||
if (!untagged.attributes || !untagged.attributes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
map.flags = new Set(untagged.attributes[0].map(entry => entry.value));
|
||||
map.delimiter = untagged.attributes[1] && untagged.attributes[1].value;
|
||||
map.prefix = (untagged.attributes[2] && untagged.attributes[2].value) || '';
|
||||
if (map.delimiter && map.prefix.charAt(0) === map.delimiter) {
|
||||
map.prefix = map.prefix.slice(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
response.next();
|
||||
return map;
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function getNamsepaceInfo(attribute) {
|
||||
if (!attribute || !attribute.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return attribute
|
||||
.filter(entry => entry.length >= 2 && typeof entry[0].value === 'string' && typeof entry[1].value === 'string')
|
||||
.map(entry => {
|
||||
let prefix = entry[0].value;
|
||||
let delimiter = entry[1].value;
|
||||
|
||||
if (delimiter && prefix && prefix.charAt(prefix.length - 1) !== delimiter) {
|
||||
prefix += delimiter;
|
||||
}
|
||||
return { prefix, delimiter };
|
||||
});
|
||||
}
|
||||
13
backend/node_modules/imapflow/lib/commands/noop.js
generated
vendored
Normal file
13
backend/node_modules/imapflow/lib/commands/noop.js
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
// Sends a NO-OP command
|
||||
module.exports = async connection => {
|
||||
try {
|
||||
let response = await connection.exec('NOOP', false, { comment: 'Requested by command' });
|
||||
response.next();
|
||||
return true;
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
102
backend/node_modules/imapflow/lib/commands/quota.js
generated
vendored
Normal file
102
backend/node_modules/imapflow/lib/commands/quota.js
generated
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
'use strict';
|
||||
|
||||
const { encodePath, normalizePath, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Requests quota information for a mailbox
|
||||
module.exports = async (connection, path) => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state) || !path) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connection.capabilities.has('QUOTA')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
path = normalizePath(connection, path);
|
||||
|
||||
let map = { path };
|
||||
|
||||
let processQuotaResponse = untagged => {
|
||||
let attributes = untagged.attributes && untagged.attributes[1];
|
||||
if (!attributes || !attributes.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let key = false;
|
||||
attributes.forEach((attribute, i) => {
|
||||
if (i % 3 === 0) {
|
||||
key = attribute && typeof attribute.value === 'string' ? attribute.value.toLowerCase() : false;
|
||||
return;
|
||||
}
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
let value = attribute && typeof attribute.value === 'string' && !isNaN(attribute.value) ? Number(attribute.value) : false;
|
||||
if (value === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (i % 3 === 1) {
|
||||
// usage
|
||||
if (!map[key]) {
|
||||
map[key] = {};
|
||||
}
|
||||
map[key].usage = value * (key === 'storage' ? 1024 : 1);
|
||||
}
|
||||
|
||||
if (i % 3 === 2) {
|
||||
// limit
|
||||
if (!map[key]) {
|
||||
map[key] = {};
|
||||
}
|
||||
map[key].limit = value * (key === 'storage' ? 1024 : 1);
|
||||
|
||||
if (map[key].limit) {
|
||||
map[key].status = Math.round(((map[key].usage || 0) / map[key].limit) * 100) + '%';
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let quotaFound = false;
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec('GETQUOTAROOT', [{ type: 'ATOM', value: encodePath(connection, path) }], {
|
||||
untagged: {
|
||||
QUOTAROOT: async untagged => {
|
||||
let quotaRoot =
|
||||
untagged.attributes && untagged.attributes[1] && typeof untagged.attributes[1].value === 'string'
|
||||
? untagged.attributes[1].value
|
||||
: false;
|
||||
if (quotaRoot) {
|
||||
map.quotaRoot = quotaRoot;
|
||||
}
|
||||
},
|
||||
QUOTA: async untagged => {
|
||||
quotaFound = true;
|
||||
processQuotaResponse(untagged);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
response.next();
|
||||
|
||||
if (map.quotaRoot && !quotaFound) {
|
||||
response = await connection.exec('GETQUOTA', [{ type: 'ATOM', value: map.quotaRoot }], {
|
||||
untagged: {
|
||||
QUOTA: async untagged => {
|
||||
processQuotaResponse(untagged);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return map;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
36
backend/node_modules/imapflow/lib/commands/rename.js
generated
vendored
Normal file
36
backend/node_modules/imapflow/lib/commands/rename.js
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
'use strict';
|
||||
|
||||
const { encodePath, normalizePath, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Renames existing mailbox
|
||||
module.exports = async (connection, path, newPath) => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
path = normalizePath(connection, path);
|
||||
newPath = normalizePath(connection, newPath);
|
||||
|
||||
if (connection.state === connection.states.SELECTED && connection.mailbox.path === path) {
|
||||
await connection.run('CLOSE');
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
let map = {
|
||||
path,
|
||||
newPath
|
||||
};
|
||||
response = await connection.exec('RENAME', [
|
||||
{ type: 'ATOM', value: encodePath(connection, path) },
|
||||
{ type: 'ATOM', value: encodePath(connection, newPath) }
|
||||
]);
|
||||
response.next();
|
||||
return map;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
50
backend/node_modules/imapflow/lib/commands/search.js
generated
vendored
Normal file
50
backend/node_modules/imapflow/lib/commands/search.js
generated
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
'use strict';
|
||||
|
||||
const { enhanceCommandError } = require('../tools.js');
|
||||
const { searchCompiler } = require('../search-compiler.js');
|
||||
|
||||
// Updates flags for a message
|
||||
module.exports = async (connection, query, options) => {
|
||||
if (connection.state !== connection.states.SELECTED) {
|
||||
// nothing to do here
|
||||
return false;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
let attributes;
|
||||
|
||||
if (!query || query === true || (typeof query === 'object' && (!Object.keys(query).length || (Object.keys(query).length === 1 && query.all)))) {
|
||||
// search for all messages
|
||||
attributes = [{ type: 'ATOM', value: 'ALL' }];
|
||||
} else if (query && typeof query === 'object') {
|
||||
// normal query
|
||||
attributes = searchCompiler(connection, query);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
let results = new Set();
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec(options.uid ? 'UID SEARCH' : 'SEARCH', attributes, {
|
||||
untagged: {
|
||||
SEARCH: async untagged => {
|
||||
if (untagged && untagged.attributes && untagged.attributes.length) {
|
||||
untagged.attributes.forEach(attribute => {
|
||||
if (attribute && attribute.value && typeof attribute.value === 'string' && !isNaN(attribute.value)) {
|
||||
results.add(Number(attribute.value));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
response.next();
|
||||
return Array.from(results).sort((a, b) => a - b);
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
216
backend/node_modules/imapflow/lib/commands/select.js
generated
vendored
Normal file
216
backend/node_modules/imapflow/lib/commands/select.js
generated
vendored
Normal file
@@ -0,0 +1,216 @@
|
||||
'use strict';
|
||||
|
||||
const { encodePath, normalizePath, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Selects a mailbox
|
||||
module.exports = async (connection, path, options) => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
options = options || {};
|
||||
|
||||
path = normalizePath(connection, path);
|
||||
|
||||
if (!connection.folders.has(path)) {
|
||||
let folders = await connection.run('LIST', '', path);
|
||||
if (!folders) {
|
||||
throw new Error('Failed to fetch folders');
|
||||
}
|
||||
folders.forEach(folder => {
|
||||
connection.folders.set(folder.path, folder);
|
||||
});
|
||||
}
|
||||
|
||||
let folderListData = connection.folders.has(path) ? connection.folders.get(path) : false;
|
||||
|
||||
let response;
|
||||
try {
|
||||
let map = { path };
|
||||
if (folderListData) {
|
||||
['delimiter', 'specialUse', 'subscribed', 'listed'].forEach(key => {
|
||||
if (folderListData[key]) {
|
||||
map[key] = folderListData[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let extraArgs = [];
|
||||
if (connection.enabled.has('QRESYNC') && options.changedSince && options.uidValidity) {
|
||||
extraArgs.push([
|
||||
{ type: 'ATOM', value: 'QRESYNC' },
|
||||
[
|
||||
{ type: 'ATOM', value: options.uidValidity?.toString() },
|
||||
{ type: 'ATOM', value: options.changedSince.toString() }
|
||||
]
|
||||
]);
|
||||
map.qresync = true;
|
||||
}
|
||||
|
||||
let encodedPath = encodePath(connection, path);
|
||||
|
||||
let selectCommand = {
|
||||
command: !options.readOnly ? 'SELECT' : 'EXAMINE',
|
||||
arguments: [{ type: encodedPath.indexOf('&') >= 0 ? 'STRING' : 'ATOM', value: encodedPath }].concat(extraArgs || [])
|
||||
};
|
||||
|
||||
response = await connection.exec(selectCommand.command, selectCommand.arguments, {
|
||||
untagged: {
|
||||
OK: async untagged => {
|
||||
if (!untagged.attributes || !untagged.attributes.length) {
|
||||
return;
|
||||
}
|
||||
let section = !untagged.attributes[0].value && untagged.attributes[0].section;
|
||||
if (section && section.length > 1 && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
|
||||
let key = section[0].value.toLowerCase();
|
||||
let value;
|
||||
|
||||
if (typeof section[1].value === 'string') {
|
||||
value = section[1].value;
|
||||
} else if (Array.isArray(section[1])) {
|
||||
value = section[1].map(entry => (typeof entry.value === 'string' ? entry.value : false)).filter(entry => entry);
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case 'highestmodseq':
|
||||
key = 'highestModseq';
|
||||
if (/^[0-9]+$/.test(value)) {
|
||||
value = BigInt(value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'mailboxid':
|
||||
key = 'mailboxId';
|
||||
if (Array.isArray(value) && value.length) {
|
||||
value = value[0];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'permanentflags':
|
||||
key = 'permanentFlags';
|
||||
value = new Set(value);
|
||||
break;
|
||||
|
||||
case 'uidnext':
|
||||
key = 'uidNext';
|
||||
value = Number(value);
|
||||
break;
|
||||
|
||||
case 'uidvalidity':
|
||||
key = 'uidValidity';
|
||||
if (/^[0-9]+$/.test(value)) {
|
||||
value = BigInt(value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
map[key] = value;
|
||||
}
|
||||
|
||||
if (section && section.length === 1 && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
|
||||
let key = section[0].value.toLowerCase();
|
||||
switch (key) {
|
||||
case 'nomodseq':
|
||||
key = 'noModseq';
|
||||
map[key] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
FLAGS: async untagged => {
|
||||
if (!untagged.attributes || (!untagged.attributes.length && Array.isArray(untagged.attributes[0]))) {
|
||||
return;
|
||||
}
|
||||
let flags = untagged.attributes[0].map(flag => (typeof flag.value === 'string' ? flag.value : false)).filter(flag => flag);
|
||||
map.flags = new Set(flags);
|
||||
},
|
||||
EXISTS: async untagged => {
|
||||
let num = Number(untagged.command);
|
||||
if (isNaN(num)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
map.exists = num;
|
||||
},
|
||||
VANISHED: async untagged => {
|
||||
await connection.untaggedVanished(
|
||||
untagged,
|
||||
// mailbox is not yet open, so use a dummy mailbox object
|
||||
{ path, uidNext: false, uidValidity: false }
|
||||
);
|
||||
},
|
||||
// we should only get an untagged FETCH for a SELECT/EXAMINE if QRESYNC was asked for
|
||||
FETCH: async untagged => {
|
||||
await connection.untaggedFetch(
|
||||
untagged,
|
||||
// mailbox is not yet open, so use a dummy mailbox object
|
||||
{ path, uidNext: false, uidValidity: false }
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let section = !response.response.attributes[0].value && response.response.attributes[0].section;
|
||||
if (section && section.length && section[0].type === 'ATOM' && typeof section[0].value === 'string') {
|
||||
switch (section[0].value.toUpperCase()) {
|
||||
case 'READ-ONLY':
|
||||
map.readOnly = true;
|
||||
break;
|
||||
case 'READ-WRITE':
|
||||
default:
|
||||
map.readOnly = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
map.qresync &&
|
||||
// UIDVALIDITY must be the same
|
||||
(options.uidValidity !== map.uidValidity ||
|
||||
// HIGHESTMODSEQ response must be present
|
||||
!map.highestModseq ||
|
||||
// NOMODSEQ is not allowed
|
||||
map.noModseq)
|
||||
) {
|
||||
// QRESYNC does not apply here, so unset it
|
||||
map.qresync = false;
|
||||
}
|
||||
|
||||
let currentMailbox = connection.mailbox;
|
||||
connection.mailbox = false;
|
||||
|
||||
if (currentMailbox && currentMailbox.path !== path) {
|
||||
connection.emit('mailboxClose', currentMailbox);
|
||||
}
|
||||
|
||||
connection.mailbox = map;
|
||||
connection.currentSelectCommand = selectCommand;
|
||||
connection.state = connection.states.SELECTED;
|
||||
|
||||
if (!currentMailbox || currentMailbox.path !== path) {
|
||||
connection.emit('mailboxOpen', connection.mailbox);
|
||||
}
|
||||
|
||||
response.next();
|
||||
return map;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
|
||||
if (connection.state === connection.states.SELECTED) {
|
||||
// reset selected state
|
||||
|
||||
let currentMailbox = connection.mailbox;
|
||||
|
||||
connection.mailbox = false;
|
||||
connection.currentSelectCommand = false;
|
||||
connection.state = connection.states.AUTHENTICATED;
|
||||
|
||||
if (currentMailbox) {
|
||||
connection.emit('mailboxClose', currentMailbox);
|
||||
}
|
||||
}
|
||||
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
19
backend/node_modules/imapflow/lib/commands/starttls.js
generated
vendored
Normal file
19
backend/node_modules/imapflow/lib/commands/starttls.js
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
// Requests STARTTLS info from server
|
||||
module.exports = async connection => {
|
||||
if (!connection.capabilities.has('STARTTLS') || connection.secureConnection) {
|
||||
// nothing to do here
|
||||
return false;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec('STARTTLS');
|
||||
response.next();
|
||||
return true;
|
||||
} catch (err) {
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
143
backend/node_modules/imapflow/lib/commands/status.js
generated
vendored
Normal file
143
backend/node_modules/imapflow/lib/commands/status.js
generated
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
'use strict';
|
||||
|
||||
const { encodePath, normalizePath } = require('../tools.js');
|
||||
|
||||
// Requests info about a mailbox
|
||||
module.exports = async (connection, path, query) => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state) || !path) {
|
||||
// nothing to do here
|
||||
return false;
|
||||
}
|
||||
|
||||
path = normalizePath(connection, path);
|
||||
let encodedPath = encodePath(connection, path);
|
||||
|
||||
let attributes = [{ type: encodedPath.indexOf('&') >= 0 ? 'STRING' : 'ATOM', value: encodedPath }];
|
||||
|
||||
let queryAttributes = [];
|
||||
Object.keys(query || {}).forEach(key => {
|
||||
if (!query[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (key.toUpperCase()) {
|
||||
case 'MESSAGES':
|
||||
case 'RECENT':
|
||||
case 'UIDNEXT':
|
||||
case 'UIDVALIDITY':
|
||||
case 'UNSEEN':
|
||||
queryAttributes.push({ type: 'ATOM', value: key.toUpperCase() });
|
||||
break;
|
||||
|
||||
case 'HIGHESTMODSEQ':
|
||||
if (connection.capabilities.has('CONDSTORE')) {
|
||||
queryAttributes.push({ type: 'ATOM', value: key.toUpperCase() });
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (!queryAttributes.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
attributes.push(queryAttributes);
|
||||
|
||||
let response;
|
||||
try {
|
||||
let map = { path };
|
||||
response = await connection.exec('STATUS', attributes, {
|
||||
untagged: {
|
||||
STATUS: async untagged => {
|
||||
// If STATUS is for current mailbox then update mailbox values
|
||||
let updateCurrent = connection.state === connection.states.SELECTED && path === connection.mailbox.path;
|
||||
|
||||
let list = untagged.attributes && Array.isArray(untagged.attributes[1]) ? untagged.attributes[1] : false;
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
let key;
|
||||
list.forEach((entry, i) => {
|
||||
if (i % 2 === 0) {
|
||||
key = entry && typeof entry.value === 'string' ? entry.value : false;
|
||||
return;
|
||||
}
|
||||
if (!key || !entry || typeof entry.value !== 'string') {
|
||||
return;
|
||||
}
|
||||
let value = false;
|
||||
switch (key.toUpperCase()) {
|
||||
case 'MESSAGES':
|
||||
key = 'messages';
|
||||
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
||||
if (updateCurrent) {
|
||||
let prevCount = connection.mailbox.exists;
|
||||
if (prevCount !== value) {
|
||||
// somehow message count in current folder has changed?
|
||||
connection.mailbox.exists = value;
|
||||
connection.emit('exists', {
|
||||
path,
|
||||
count: value,
|
||||
prevCount
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'RECENT':
|
||||
key = 'recent';
|
||||
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
||||
break;
|
||||
|
||||
case 'UIDNEXT':
|
||||
key = 'uidNext';
|
||||
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
||||
if (updateCurrent) {
|
||||
connection.mailbox.uidNext = value;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'UIDVALIDITY':
|
||||
key = 'uidValidity';
|
||||
value = !isNaN(entry.value) ? BigInt(entry.value) : false;
|
||||
break;
|
||||
|
||||
case 'UNSEEN':
|
||||
key = 'unseen';
|
||||
value = !isNaN(entry.value) ? Number(entry.value) : false;
|
||||
break;
|
||||
|
||||
case 'HIGHESTMODSEQ':
|
||||
key = 'highestModseq';
|
||||
value = !isNaN(entry.value) ? BigInt(entry.value) : false;
|
||||
if (updateCurrent) {
|
||||
connection.mailbox.highestModseq = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (value === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
map[key] = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
response.next();
|
||||
return map;
|
||||
} catch (err) {
|
||||
if (err.responseStatus === 'NO') {
|
||||
let folders = await connection.run('LIST', '', path, { listOnly: true });
|
||||
if (folders && !folders.length) {
|
||||
let error = new Error(`Mailbox doesn't exist: ${path}`);
|
||||
error.code = 'NotFound';
|
||||
error.response = err;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
79
backend/node_modules/imapflow/lib/commands/store.js
generated
vendored
Normal file
79
backend/node_modules/imapflow/lib/commands/store.js
generated
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
'use strict';
|
||||
|
||||
const { formatFlag, canUseFlag, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Updates flags for a message
|
||||
module.exports = async (connection, range, flags, options) => {
|
||||
if (connection.state !== connection.states.SELECTED || !range || (options.useLabels && !connection.capabilities.has('X-GM-EXT-1'))) {
|
||||
// nothing to do here
|
||||
return false;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
let operation;
|
||||
|
||||
operation = 'FLAGS';
|
||||
|
||||
if (options.useLabels) {
|
||||
operation = 'X-GM-LABELS';
|
||||
} else if (options.silent) {
|
||||
operation = `${operation}.SILENT`;
|
||||
}
|
||||
|
||||
switch ((options.operation || '').toLowerCase()) {
|
||||
case 'set':
|
||||
// do nothing, keep operation value as is
|
||||
break;
|
||||
case 'remove':
|
||||
operation = `-${operation}`;
|
||||
break;
|
||||
case 'add':
|
||||
default:
|
||||
operation = `+${operation}`;
|
||||
break;
|
||||
}
|
||||
|
||||
flags = (Array.isArray(flags) ? flags : [].concat(flags || []))
|
||||
.map(flag => {
|
||||
flag = formatFlag(flag);
|
||||
|
||||
if (!canUseFlag(connection.mailbox, flag) && operation !== 'remove') {
|
||||
// it does not seem that we can set this flag
|
||||
return false;
|
||||
}
|
||||
|
||||
return flag;
|
||||
})
|
||||
.filter(flag => flag);
|
||||
|
||||
if (!flags.length && options.operation !== 'set') {
|
||||
// nothing to do here
|
||||
return false;
|
||||
}
|
||||
|
||||
let attributes = [{ type: 'SEQUENCE', value: range }, { type: 'ATOM', value: operation }, flags.map(flag => ({ type: 'ATOM', value: flag }))];
|
||||
|
||||
if (options.unchangedSince && connection.enabled.has('CONDSTORE') && !connection.mailbox.noModseq) {
|
||||
attributes.push([
|
||||
{
|
||||
type: 'ATOM',
|
||||
value: 'UNCHANGEDSINCE'
|
||||
},
|
||||
{
|
||||
type: 'ATOM',
|
||||
value: options.unchangedSince.toString()
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec(options.uid ? 'UID STORE' : 'STORE', attributes);
|
||||
response.next();
|
||||
return true;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
24
backend/node_modules/imapflow/lib/commands/subscribe.js
generated
vendored
Normal file
24
backend/node_modules/imapflow/lib/commands/subscribe.js
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
const { encodePath, normalizePath, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Subscribes to a mailbox
|
||||
module.exports = async (connection, path) => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
path = normalizePath(connection, path);
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec('SUBSCRIBE', [{ type: 'ATOM', value: encodePath(connection, path) }]);
|
||||
response.next();
|
||||
return true;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
24
backend/node_modules/imapflow/lib/commands/unsubscribe.js
generated
vendored
Normal file
24
backend/node_modules/imapflow/lib/commands/unsubscribe.js
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
const { encodePath, normalizePath, enhanceCommandError } = require('../tools.js');
|
||||
|
||||
// Unsubscribes from a mailbox
|
||||
module.exports = async (connection, path) => {
|
||||
if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state)) {
|
||||
// nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
path = normalizePath(connection, path);
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await connection.exec('UNSUBSCRIBE', [{ type: 'ATOM', value: encodePath(connection, path) }]);
|
||||
response.next();
|
||||
return true;
|
||||
} catch (err) {
|
||||
await enhanceCommandError(err);
|
||||
connection.log.warn({ err, cid: connection.id });
|
||||
return false;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user