Projektstart

This commit is contained in:
2026-01-22 15:49:12 +01:00
parent 7212eb6f7a
commit 57e5f652f8
10637 changed files with 2598792 additions and 64 deletions

146
backend/node_modules/imapflow/lib/commands/append.js generated vendored Normal file
View 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;
}
};

View 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');
};

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}
};

View 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;
}
};

View 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;
}
};