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

283
backend/node_modules/imapflow/lib/charsets.js generated vendored Normal file
View File

@@ -0,0 +1,283 @@
'use strict';
const CHARACTER_SETS = [
'US-ASCII',
'ISO-8859-1',
'ISO-8859-2',
'ISO-8859-3',
'ISO-8859-4',
'ISO-8859-5',
'ISO-8859-6',
'ISO-8859-7',
'ISO-8859-8',
'ISO-8859-9',
'ISO-8859-10',
'ISO_6937-2-add',
'JIS_X0201',
'JIS_Encoding',
'Shift_JIS',
'EUC-JP',
'Extended_UNIX_Code_Fixed_Width_for_Japanese',
'BS_4730',
'SEN_850200_C',
'IT',
'ES',
'DIN_66003',
'NS_4551-1',
'NF_Z_62-010',
'ISO-10646-UTF-1',
'ISO_646.basic:1983',
'INVARIANT',
'ISO_646.irv:1983',
'NATS-SEFI',
'NATS-SEFI-ADD',
'NATS-DANO',
'NATS-DANO-ADD',
'SEN_850200_B',
'KS_C_5601-1987',
'ISO-2022-KR',
'EUC-KR',
'ISO-2022-JP',
'ISO-2022-JP-2',
'JIS_C6220-1969-jp',
'JIS_C6220-1969-ro',
'PT',
'greek7-old',
'latin-greek',
'NF_Z_62-010_(1973)',
'Latin-greek-1',
'ISO_5427',
'JIS_C6226-1978',
'BS_viewdata',
'INIS',
'INIS-8',
'INIS-cyrillic',
'ISO_5427:1981',
'ISO_5428:1980',
'GB_1988-80',
'GB_2312-80',
'NS_4551-2',
'videotex-suppl',
'PT2',
'ES2',
'MSZ_7795.3',
'JIS_C6226-1983',
'greek7',
'ASMO_449',
'iso-ir-90',
'JIS_C6229-1984-a',
'JIS_C6229-1984-b',
'JIS_C6229-1984-b-add',
'JIS_C6229-1984-hand',
'JIS_C6229-1984-hand-add',
'JIS_C6229-1984-kana',
'ISO_2033-1983',
'ANSI_X3.110-1983',
'T.61-7bit',
'T.61-8bit',
'ECMA-cyrillic',
'CSA_Z243.4-1985-1',
'CSA_Z243.4-1985-2',
'CSA_Z243.4-1985-gr',
'ISO-8859-6-E',
'ISO-8859-6-I',
'T.101-G2',
'ISO-8859-8-E',
'ISO-8859-8-I',
'CSN_369103',
'JUS_I.B1.002',
'IEC_P27-1',
'JUS_I.B1.003-serb',
'JUS_I.B1.003-mac',
'greek-ccitt',
'NC_NC00-10:81',
'ISO_6937-2-25',
'GOST_19768-74',
'ISO_8859-supp',
'ISO_10367-box',
'latin-lap',
'JIS_X0212-1990',
'DS_2089',
'us-dk',
'dk-us',
'KSC5636',
'UNICODE-1-1-UTF-7',
'ISO-2022-CN',
'ISO-2022-CN-EXT',
'UTF-8',
'ISO-8859-13',
'ISO-8859-14',
'ISO-8859-15',
'ISO-8859-16',
'GBK',
'GB18030',
'OSD_EBCDIC_DF04_15',
'OSD_EBCDIC_DF03_IRV',
'OSD_EBCDIC_DF04_1',
'ISO-11548-1',
'KZ-1048',
'ISO-10646-UCS-2',
'ISO-10646-UCS-4',
'ISO-10646-UCS-Basic',
'ISO-10646-Unicode-Latin1',
'ISO-10646-J-1',
'ISO-Unicode-IBM-1261',
'ISO-Unicode-IBM-1268',
'ISO-Unicode-IBM-1276',
'ISO-Unicode-IBM-1264',
'ISO-Unicode-IBM-1265',
'UNICODE-1-1',
'SCSU',
'UTF-7',
'UTF-16BE',
'UTF-16LE',
'UTF-16',
'CESU-8',
'UTF-32',
'UTF-32BE',
'UTF-32LE',
'BOCU-1',
'ISO-8859-1-Windows-3.0-Latin-1',
'ISO-8859-1-Windows-3.1-Latin-1',
'ISO-8859-2-Windows-Latin-2',
'ISO-8859-9-Windows-Latin-5',
'hp-roman8',
'Adobe-Standard-Encoding',
'Ventura-US',
'Ventura-International',
'DEC-MCS',
'IBM850',
'PC8-Danish-Norwegian',
'IBM862',
'PC8-Turkish',
'IBM-Symbols',
'IBM-Thai',
'HP-Legal',
'HP-Pi-font',
'HP-Math8',
'Adobe-Symbol-Encoding',
'HP-DeskTop',
'Ventura-Math',
'Microsoft-Publishing',
'Windows-31J',
'GB2312',
'Big5',
'macintosh',
'IBM037',
'IBM038',
'IBM273',
'IBM274',
'IBM275',
'IBM277',
'IBM278',
'IBM280',
'IBM281',
'IBM284',
'IBM285',
'IBM290',
'IBM297',
'IBM420',
'IBM423',
'IBM424',
'IBM437',
'IBM500',
'IBM851',
'IBM852',
'IBM855',
'IBM857',
'IBM860',
'IBM861',
'IBM863',
'IBM864',
'IBM865',
'IBM868',
'IBM869',
'IBM870',
'IBM871',
'IBM880',
'IBM891',
'IBM903',
'IBM904',
'IBM905',
'IBM918',
'IBM1026',
'EBCDIC-AT-DE',
'EBCDIC-AT-DE-A',
'EBCDIC-CA-FR',
'EBCDIC-DK-NO',
'EBCDIC-DK-NO-A',
'EBCDIC-FI-SE',
'EBCDIC-FI-SE-A',
'EBCDIC-FR',
'EBCDIC-IT',
'EBCDIC-PT',
'EBCDIC-ES',
'EBCDIC-ES-A',
'EBCDIC-ES-S',
'EBCDIC-UK',
'EBCDIC-US',
'UNKNOWN-8BIT',
'MNEMONIC',
'MNEM',
'VISCII',
'VIQR',
'KOI8-R',
'HZ-GB-2312',
'IBM866',
'IBM775',
'KOI8-U',
'IBM00858',
'IBM00924',
'IBM01140',
'IBM01141',
'IBM01142',
'IBM01143',
'IBM01144',
'IBM01145',
'IBM01146',
'IBM01147',
'IBM01148',
'IBM01149',
'Big5-HKSCS',
'IBM1047',
'PTCP154',
'Amiga-1251',
'KOI7-switched',
'BRF',
'TSCII',
'CP51932',
'windows-874',
'windows-1250',
'windows-1251',
'windows-1252',
'windows-1253',
'windows-1254',
'windows-1255',
'windows-1256',
'windows-1257',
'windows-1258',
'TIS-620',
'CP50220'
];
const CHARSET_MAP = new Map();
CHARACTER_SETS.forEach(entry => {
let key = entry.replace(/[_-\s]/g, '').toLowerCase();
let modifiedKey = key
.replace(/^windows/, 'win')
.replace(/^usascii/, 'ascii')
.replace(/^iso8859/, 'latin');
CHARSET_MAP.set(key, entry);
if (!CHARSET_MAP.has(modifiedKey)) {
CHARSET_MAP.set(modifiedKey, entry);
}
});
module.exports.resolveCharset = charset => {
let key = charset.replace(/[_-\s]/g, '').toLowerCase();
if (CHARSET_MAP.has(key)) {
return CHARSET_MAP.get(key);
}
return null;
};

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

View File

@@ -0,0 +1,190 @@
/* eslint no-console: 0, new-cap: 0 */
'use strict';
const imapFormalSyntax = require('./imap-formal-syntax');
const formatRespEntry = (entry, returnEmpty) => {
if (typeof entry === 'string') {
return Buffer.from(entry);
}
if (typeof entry === 'number') {
return Buffer.from(entry.toString());
}
if (Buffer.isBuffer(entry)) {
return entry;
}
if (returnEmpty) {
return null;
}
return Buffer.alloc(0);
};
/**
* Compiles an input object into
*/
module.exports = async (response, options) => {
let { asArray, isLogging, literalPlus, literalMinus } = options || {};
const respParts = [];
let resp = [].concat(formatRespEntry(response.tag, true) || []).concat(response.command ? formatRespEntry(' ' + response.command) : []);
let val;
let lastType;
let walk = async (node, options) => {
options = options || {};
let lastRespEntry = resp.length && resp[resp.length - 1];
let lastRespByte = (lastRespEntry && lastRespEntry.length && lastRespEntry[lastRespEntry.length - 1]) || '';
if (typeof lastRespByte === 'number') {
lastRespByte = String.fromCharCode(lastRespByte);
}
if (lastType === 'LITERAL' || (!['(', '<', '['].includes(lastRespByte) && resp.length)) {
if (options.subArray) {
// ignore separator
} else {
resp.push(formatRespEntry(' '));
}
}
if (node && node.buffer && !Buffer.isBuffer(node)) {
// mongodb binary
node = node.buffer;
}
if (Array.isArray(node)) {
lastType = 'LIST';
resp.push(formatRespEntry('('));
// check if we need to skip separator WS between two arrays
let subArray = node.length > 1 && Array.isArray(node[0]);
for (let child of node) {
if (subArray && !Array.isArray(child)) {
subArray = false;
}
await walk(child, { subArray });
}
resp.push(formatRespEntry(')'));
return;
}
if (!node && typeof node !== 'string' && typeof node !== 'number' && !Buffer.isBuffer(node)) {
resp.push(formatRespEntry('NIL'));
return;
}
if (typeof node === 'string' || Buffer.isBuffer(node)) {
if (isLogging && node.length > 100) {
resp.push(formatRespEntry('"(* ' + node.length + 'B string *)"'));
} else {
resp.push(formatRespEntry(JSON.stringify(node.toString())));
}
return;
}
if (typeof node === 'number') {
resp.push(formatRespEntry(Math.round(node) || 0)); // Only integers allowed
return;
}
lastType = node.type;
if (isLogging && node.sensitive) {
resp.push(formatRespEntry('"(* value hidden *)"'));
return;
}
switch (node.type.toUpperCase()) {
case 'LITERAL':
if (isLogging) {
resp.push(formatRespEntry('"(* ' + node.value.length + 'B literal *)"'));
} else {
let literalLength = !node.value ? 0 : Math.max(node.value.length, 0);
let canAppend = !asArray || literalPlus || (literalMinus && literalLength <= 4096);
let usePlus = canAppend && (literalMinus || literalPlus);
resp.push(formatRespEntry(`${node.isLiteral8 ? '~' : ''}{${literalLength}${usePlus ? '+' : ''}}\r\n`));
if (canAppend) {
if (node.value && node.value.length) {
resp.push(formatRespEntry(node.value));
}
} else {
respParts.push(resp);
resp = [].concat(formatRespEntry(node.value, true) || []);
}
}
break;
case 'STRING':
if (isLogging && node.value.length > 100) {
resp.push(formatRespEntry('"(* ' + node.value.length + 'B string *)"'));
} else {
resp.push(formatRespEntry(JSON.stringify((node.value || '').toString())));
}
break;
case 'TEXT':
case 'SEQUENCE':
if (node.value) {
resp.push(formatRespEntry(node.value));
}
break;
case 'NUMBER':
resp.push(formatRespEntry(node.value || 0));
break;
case 'ATOM':
case 'SECTION':
val = (node.value || '').toString();
if (!node.section || val) {
if (node.value === '' || imapFormalSyntax.verify(val.charAt(0) === '\\' ? val.substr(1) : val, imapFormalSyntax['ATOM-CHAR']()) >= 0) {
val = JSON.stringify(val);
}
resp.push(formatRespEntry(val));
}
if (node.section) {
resp.push(formatRespEntry('['));
for (let child of node.section) {
await walk(child);
}
resp.push(formatRespEntry(']'));
}
if (node.partial) {
resp.push(formatRespEntry(`<${node.partial.join('.')}>`));
}
break;
}
};
if (response.attributes) {
let attributes = Array.isArray(response.attributes) ? response.attributes : [].concat(response.attributes);
for (let child of attributes) {
await walk(child);
}
}
if (resp.length) {
respParts.push(resp);
}
for (let i = 0; i < respParts.length; i++) {
respParts[i] = Buffer.concat(respParts[i]);
}
return asArray ? respParts : respParts.flatMap(entry => entry);
};

View File

@@ -0,0 +1,147 @@
/* eslint object-shorthand:0, new-cap: 0, no-useless-concat: 0 */
'use strict';
// IMAP Formal Syntax
// http://tools.ietf.org/html/rfc3501#section-9
function expandRange(start, end) {
let chars = [];
for (let i = start; i <= end; i++) {
chars.push(i);
}
return String.fromCharCode(...chars);
}
function excludeChars(source, exclude) {
let sourceArr = Array.prototype.slice.call(source);
for (let i = sourceArr.length - 1; i >= 0; i--) {
if (exclude.indexOf(sourceArr[i]) >= 0) {
sourceArr.splice(i, 1);
}
}
return sourceArr.join('');
}
module.exports = {
CHAR() {
let value = expandRange(0x01, 0x7f);
this.CHAR = function () {
return value;
};
return value;
},
CHAR8() {
let value = expandRange(0x01, 0xff);
this.CHAR8 = function () {
return value;
};
return value;
},
SP() {
return ' ';
},
CTL() {
let value = expandRange(0x00, 0x1f) + '\x7F';
this.CTL = function () {
return value;
};
return value;
},
DQUOTE() {
return '"';
},
ALPHA() {
let value = expandRange(0x41, 0x5a) + expandRange(0x61, 0x7a);
this.ALPHA = function () {
return value;
};
return value;
},
DIGIT() {
let value = expandRange(0x30, 0x39);
this.DIGIT = function () {
return value;
};
return value;
},
'ATOM-CHAR'() {
let value = excludeChars(this.CHAR(), this['atom-specials']());
this['ATOM-CHAR'] = function () {
return value;
};
return value;
},
'ASTRING-CHAR'() {
let value = this['ATOM-CHAR']() + this['resp-specials']();
this['ASTRING-CHAR'] = function () {
return value;
};
return value;
},
'TEXT-CHAR'() {
let value = excludeChars(this.CHAR(), '\r\n');
this['TEXT-CHAR'] = function () {
return value;
};
return value;
},
'atom-specials'() {
let value = '(' + ')' + '{' + this.SP() + this.CTL() + this['list-wildcards']() + this['quoted-specials']() + this['resp-specials']();
this['atom-specials'] = function () {
return value;
};
return value;
},
'list-wildcards'() {
return '%' + '*';
},
'quoted-specials'() {
let value = this.DQUOTE() + '\\';
this['quoted-specials'] = function () {
return value;
};
return value;
},
'resp-specials'() {
return ']';
},
tag() {
let value = excludeChars(this['ASTRING-CHAR'](), '+');
this.tag = function () {
return value;
};
return value;
},
command() {
let value = this.ALPHA() + this.DIGIT() + '-';
this.command = function () {
return value;
};
return value;
},
verify(str, allowedChars) {
for (let i = 0, len = str.length; i < len; i++) {
if (allowedChars.indexOf(str.charAt(i)) < 0) {
return i;
}
}
return -1;
}
};

View File

@@ -0,0 +1,9 @@
'use strict';
const parser = require('./imap-parser');
const compiler = require('./imap-compiler');
module.exports = {
parser,
compiler
};

View File

@@ -0,0 +1,67 @@
'use strict';
const imapFormalSyntax = require('./imap-formal-syntax');
const { ParserInstance } = require('./parser-instance');
module.exports = async (command, options) => {
options = options || {};
let nullBytesRemoved = 0;
// special case with a buggy IMAP server where responses are padded with zero bytes
if (command[0] === 0) {
// find the first non null byte and trim
let firstNonNull = -1;
for (let i = 0; i < command.length; i++) {
if (command[i] !== 0) {
firstNonNull = i;
break;
}
}
if (firstNonNull === -1) {
// All bytes are null
return { tag: '*', command: 'BAD', attributes: [] };
}
command = command.slice(firstNonNull);
nullBytesRemoved = firstNonNull;
}
const parser = new ParserInstance(command, options);
const response = {};
try {
response.tag = await parser.getTag();
await parser.getSpace();
response.command = await parser.getCommand();
if (nullBytesRemoved) {
response.nullBytesRemoved = nullBytesRemoved;
}
if (['UID', 'AUTHENTICATE'].indexOf((response.command || '').toUpperCase()) >= 0) {
await parser.getSpace();
response.command += ' ' + (await parser.getElement(imapFormalSyntax.command()));
}
if (parser.remainder.trim().length) {
await parser.getSpace();
response.attributes = await parser.getAttributes();
}
if (parser.humanReadable) {
response.attributes = (response.attributes || []).concat({
type: 'TEXT',
value: parser.humanReadable
});
}
} catch (err) {
if (err.code === 'ParserErrorExchange' && err.parserContext && err.parserContext.value) {
return err.parserContext.value;
}
throw err;
}
return response;
};

View File

@@ -0,0 +1,257 @@
'use strict';
const Transform = require('stream').Transform;
const logger = require('../logger');
const LINE = 0x01;
const LITERAL = 0x02;
const LF = 0x0a;
const CR = 0x0d;
const NUM_0 = 0x30;
const NUM_9 = 0x39;
const CURLY_OPEN = 0x7b;
const CURLY_CLOSE = 0x7d;
// Maximum allowed literal size: 1GB (1073741824 bytes)
const MAX_LITERAL_SIZE = 1024 * 1024 * 1024;
class ImapStream extends Transform {
constructor(options) {
super({
//writableHighWaterMark: 3,
readableObjectMode: true,
writableObjectMode: false
});
this.options = options || {};
this.cid = this.options.cid;
this.log =
this.options.logger && typeof this.options.logger === 'object'
? this.options.logger
: logger.child({
component: 'imap-connection',
cid: this.cid
});
this.readBytesCounter = 0;
this.state = LINE;
this.literalWaiting = 0;
this.inputBuffer = []; // lines
this.lineBuffer = []; // current line
this.literalBuffer = [];
this.literals = [];
this.compress = false;
this.secureConnection = this.options.secureConnection;
this.processingInput = false;
this.inputQueue = []; // unprocessed input chunks
}
checkLiteralMarker(line) {
if (!line || !line.length) {
return false;
}
let pos = line.length - 1;
if (line[pos] === LF) {
pos--;
} else {
return false;
}
if (pos >= 0 && line[pos] === CR) {
pos--;
}
if (pos < 0) {
return false;
}
if (!pos || line[pos] !== CURLY_CLOSE) {
return false;
}
pos--;
let numBytes = [];
for (; pos > 0; pos--) {
let c = line[pos];
if (c >= NUM_0 && c <= NUM_9) {
numBytes.unshift(c);
continue;
}
if (c === CURLY_OPEN && numBytes.length) {
const literalSize = Number(Buffer.from(numBytes).toString());
if (literalSize > MAX_LITERAL_SIZE) {
const err = new Error(`Literal size ${literalSize} exceeds maximum allowed size of ${MAX_LITERAL_SIZE} bytes`);
err.code = 'LiteralTooLarge';
err.literalSize = literalSize;
err.maxSize = MAX_LITERAL_SIZE;
this.emit('error', err);
return false;
}
this.state = LITERAL;
this.literalWaiting = literalSize;
return true;
}
return false;
}
return false;
}
async processInputChunk(chunk, startPos) {
startPos = startPos || 0;
if (startPos >= chunk.length) {
return;
}
switch (this.state) {
case LINE: {
let lineStart = startPos;
for (let i = startPos, len = chunk.length; i < len; i++) {
if (chunk[i] === LF) {
// line end found
this.lineBuffer.push(chunk.slice(lineStart, i + 1));
lineStart = i + 1;
let line = Buffer.concat(this.lineBuffer);
this.inputBuffer.push(line);
this.lineBuffer = [];
// try to detect if this is a literal start
if (this.checkLiteralMarker(line)) {
// switch into line mode and start over
return await this.processInputChunk(chunk, lineStart);
}
// reached end of command input, emit it
let payload = this.inputBuffer.length === 1 ? this.inputBuffer[0] : Buffer.concat(this.inputBuffer);
let literals = this.literals;
this.inputBuffer = [];
this.literals = [];
if (payload.length) {
// remove final line terminator
let skipBytes = 0;
if (payload.length >= 1 && payload[payload.length - 1] === LF) {
skipBytes++;
if (payload.length >= 2 && payload[payload.length - 2] === CR) {
skipBytes++;
}
}
if (skipBytes) {
payload = payload.slice(0, payload.length - skipBytes);
}
if (payload.length) {
await new Promise(resolve => {
this.push({ payload, literals, next: resolve });
});
}
}
}
}
if (lineStart < chunk.length) {
this.lineBuffer.push(chunk.slice(lineStart));
}
break;
}
case LITERAL: {
// exactly until end of chunk
if (chunk.length === startPos + this.literalWaiting) {
if (!startPos) {
this.literalBuffer.push(chunk);
} else {
this.literalBuffer.push(chunk.slice(startPos));
}
this.literalWaiting -= chunk.length;
this.literals.push(Buffer.concat(this.literalBuffer));
this.literalBuffer = [];
this.state = LINE;
return;
} else if (chunk.length > startPos + this.literalWaiting) {
let partial = chunk.slice(startPos, startPos + this.literalWaiting);
this.literalBuffer.push(partial);
startPos += partial.length;
this.literalWaiting -= partial.length;
this.literals.push(Buffer.concat(this.literalBuffer));
this.literalBuffer = [];
this.state = LINE;
return await this.processInputChunk(chunk, startPos);
} else {
let partial = chunk.slice(startPos);
this.literalBuffer.push(partial);
startPos += partial.length;
this.literalWaiting -= partial.length;
return;
}
}
}
}
async processInput() {
let data;
let processedCount = 0;
while ((data = this.inputQueue.shift())) {
await this.processInputChunk(data.chunk);
// mark chunk as processed
data.next();
// Yield to event loop every 10 chunks to prevent CPU blocking
processedCount++;
if (processedCount % 10 === 0) {
await new Promise(resolve => setImmediate(resolve));
}
}
}
_transform(chunk, encoding, next) {
if (typeof chunk === 'string') {
chunk = Buffer.from(chunk, encoding);
}
if (!chunk || !chunk.length) {
return next();
}
this.readBytesCounter += chunk.length;
if (this.options.logRaw) {
this.log.trace({
src: 's',
msg: 'read from socket',
data: chunk.toString('base64'),
compress: !!this.compress,
secure: !!this.secureConnection,
cid: this.cid
});
}
if (chunk && chunk.length) {
this.inputQueue.push({ chunk, next });
}
if (!this.processingInput) {
this.processingInput = true;
this.processInput()
.catch(err => this.emit('error', err))
.finally(() => (this.processingInput = false));
}
}
_flush(next) {
next();
}
}
module.exports.ImapStream = ImapStream;

View File

@@ -0,0 +1,165 @@
/* eslint new-cap: 0 */
'use strict';
const imapFormalSyntax = require('./imap-formal-syntax');
const { TokenParser } = require('./token-parser');
class ParserInstance {
constructor(input, options) {
this.input = (input || '').toString();
this.options = options || {};
this.remainder = this.input;
this.pos = 0;
}
async getTag() {
if (!this.tag) {
this.tag = await this.getElement(imapFormalSyntax.tag() + '*+', true);
}
return this.tag;
}
async getCommand() {
if (this.tag === '+') {
// special case
this.humanReadable = this.remainder.trim();
this.remainder = '';
return '';
}
if (!this.command) {
this.command = await this.getElement(imapFormalSyntax.command());
}
switch ((this.command || '').toString().toUpperCase()) {
case 'OK':
case 'NO':
case 'BAD':
case 'PREAUTH':
case 'BYE':
{
let match = this.remainder.match(/^\s+\[/);
if (match) {
let nesting = 1;
for (let i = match[0].length; i <= this.remainder.length; i++) {
let c = this.remainder[i];
if (c === '[') {
nesting++;
} else if (c === ']') {
nesting--;
}
if (!nesting) {
this.humanReadable = this.remainder.substring(i + 1).trim();
this.remainder = this.remainder.substring(0, i + 1);
break;
}
}
} else {
this.humanReadable = this.remainder.trim();
this.remainder = '';
}
}
break;
}
return this.command;
}
async getElement(syntax) {
let match, element, errPos;
if (this.remainder.match(/^\s/)) {
let error = new Error(`Unexpected whitespace at position ${this.pos} [E1]`);
error.code = 'ParserError1';
error.parserContext = { input: this.input, pos: this.pos };
throw error;
}
if ((match = this.remainder.match(/^\s*[^\s]+(?=\s|$)/))) {
element = match[0];
if ((errPos = imapFormalSyntax.verify(element, syntax)) >= 0) {
if (this.tag === 'Server' && element === 'Unavailable.') {
// Exchange error
let error = new Error(`Server returned an error: ${this.input}`);
error.code = 'ParserErrorExchange';
error.parserContext = {
input: this.input,
element,
pos: this.pos,
value: {
tag: '*',
command: 'BAD',
attributes: [{ type: 'TEXT', value: this.input }]
}
};
throw error;
}
let error = new Error(`Unexpected char at position ${this.pos + errPos} [E2: ${JSON.stringify(element.charAt(errPos))}]`);
error.code = 'ParserError2';
error.parserContext = { input: this.input, element, pos: this.pos };
throw error;
}
} else {
let error = new Error(`Unexpected end of input at position ${this.pos} [E3]`);
error.code = 'ParserError3';
error.parserContext = { input: this.input, pos: this.pos };
throw error;
}
this.pos += match[0].length;
this.remainder = this.remainder.substr(match[0].length);
return element;
}
async getSpace() {
if (!this.remainder.length) {
if (this.tag === '+' && this.pos === 1) {
// special case, empty + response
return;
}
let error = new Error(`Unexpected end of input at position ${this.pos} [E4]`);
error.code = 'ParserError4';
error.parserContext = { input: this.input, pos: this.pos };
throw error;
}
if (imapFormalSyntax.verify(this.remainder.charAt(0), imapFormalSyntax.SP()) >= 0) {
let error = new Error(`Unexpected char at position ${this.pos} [E5: ${JSON.stringify(this.remainder.charAt(0))}]`);
error.code = 'ParserError5';
error.parserContext = { input: this.input, element: this.remainder, pos: this.pos };
throw error;
}
this.pos++;
this.remainder = this.remainder.substr(1);
}
async getAttributes() {
if (!this.remainder.length) {
let error = new Error(`Unexpected end of input at position ${this.pos} [E6]`);
error.code = 'ParserError6';
error.parserContext = { input: this.input, pos: this.pos };
throw error;
}
if (this.remainder.match(/^\s/)) {
let error = new Error(`Unexpected whitespace at position ${this.pos} [E7]`);
error.code = 'ParserError7';
error.parserContext = { input: this.input, element: this.remainder, pos: this.pos };
throw error;
}
const tokenParser = new TokenParser(this, this.pos, this.remainder, this.options);
return await tokenParser.getAttributes();
}
}
module.exports.ParserInstance = ParserInstance;

View File

@@ -0,0 +1,654 @@
/* eslint new-cap: 0 */
'use strict';
const imapFormalSyntax = require('./imap-formal-syntax');
const STATE_ATOM = 0x001;
const STATE_LITERAL = 0x002;
const STATE_NORMAL = 0x003;
const STATE_PARTIAL = 0x004;
const STATE_SEQUENCE = 0x005;
const STATE_STRING = 0x006;
const STATE_TEXT = 0x007;
const RE_DIGITS = /^\d+$/;
const RE_SINGLE_DIGIT = /^\d$/;
const MAX_NODE_DEPTH = 25;
class TokenParser {
constructor(parent, startPos, str, options) {
this.str = (str || '').toString();
this.options = options || {};
this.parent = parent;
this.tree = this.currentNode = this.createNode();
this.pos = startPos || 0;
this.currentNode.type = 'TREE';
this.state = STATE_NORMAL;
}
async getAttributes() {
await this.processString();
const attributes = [];
let branch = attributes;
let walk = async node => {
let curBranch = branch;
let elm;
let partial;
if (!node.isClosed && node.type === 'SEQUENCE' && node.value === '*') {
node.isClosed = true;
node.type = 'ATOM';
}
// If the node was never closed, throw it
if (!node.isClosed) {
let error = new Error(`Unexpected end of input at position ${this.pos + this.str.length - 1} [E9]`);
error.code = 'ParserError9';
error.parserContext = { input: this.str, pos: this.pos + this.str.length - 1 };
throw error;
}
let type = (node.type || '').toString().toUpperCase();
switch (type) {
case 'LITERAL':
case 'STRING':
case 'SEQUENCE':
elm = {
type: node.type.toUpperCase(),
value: node.value
};
branch.push(elm);
break;
case 'ATOM':
if (node.value.toUpperCase() === 'NIL') {
branch.push(null);
break;
}
elm = {
type: node.type.toUpperCase(),
value: node.value
};
branch.push(elm);
break;
case 'SECTION':
branch = branch[branch.length - 1].section = [];
break;
case 'LIST':
elm = [];
branch.push(elm);
branch = elm;
break;
case 'PARTIAL':
partial = node.value.split('.').map(Number);
branch[branch.length - 1].partial = partial;
break;
}
for (let childNode of node.childNodes) {
await walk(childNode);
}
branch = curBranch;
};
await walk(this.tree);
return attributes;
}
createNode(parentNode, startPos) {
let node = {
childNodes: [],
type: false,
value: '',
isClosed: true
};
if (parentNode) {
node.parentNode = parentNode;
node.depth = parentNode.depth + 1;
} else {
node.depth = 0;
}
if (node.depth > MAX_NODE_DEPTH) {
let error = new Error('Too much nesting in IMAP string');
error.code = 'MAX_IMAP_NESTING_REACHED';
error._imapStr = this.str;
throw error;
}
if (typeof startPos === 'number') {
node.startPos = startPos;
}
if (parentNode) {
parentNode.childNodes.push(node);
}
return node;
}
async processString() {
let chr, i, len;
const checkSP = () => {
// jump to the next non whitespace pos
while (this.str.charAt(i + 1) === ' ') {
i++;
}
};
for (i = 0, len = this.str.length; i < len; i++) {
chr = this.str.charAt(i);
switch (this.state) {
case STATE_NORMAL:
switch (chr) {
// DQUOTE starts a new string
case '"':
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'string';
this.state = STATE_STRING;
this.currentNode.isClosed = false;
break;
// ( starts a new list
case '(':
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'LIST';
this.currentNode.isClosed = false;
break;
// ) closes a list
case ')':
if (this.currentNode.type !== 'LIST') {
let error = new Error(`Unexpected list terminator ) at position ${this.pos + i} [E10]`);
error.code = 'ParserError10';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode.isClosed = true;
this.currentNode.endPos = this.pos + i;
this.currentNode = this.currentNode.parentNode;
checkSP();
break;
// ] closes section group
case ']':
if (this.currentNode.type !== 'SECTION') {
let error = new Error(`Unexpected section terminator ] at position ${this.pos + i} [E11]`);
error.code = 'ParserError11';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode.isClosed = true;
this.currentNode.endPos = this.pos + i;
this.currentNode = this.currentNode.parentNode;
checkSP();
break;
// < starts a new partial
case '<':
if (this.str.charAt(i - 1) !== ']') {
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'ATOM';
this.currentNode.value = chr;
this.state = STATE_ATOM;
} else {
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'PARTIAL';
this.state = STATE_PARTIAL;
this.currentNode.isClosed = false;
}
break;
// binary literal8
case '~': {
let nextChr = this.str.charAt(i + 1);
if (nextChr !== '{') {
if (imapFormalSyntax['ATOM-CHAR']().indexOf(nextChr) >= 0) {
// treat as ATOM
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'ATOM';
this.currentNode.value = chr;
this.state = STATE_ATOM;
break;
}
let error = new Error(`Unexpected literal8 marker at position ${this.pos + i} [E12]`);
error.code = 'ParserError12';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.expectedLiteralType = 'literal8';
break;
}
// { starts a new literal
case '{':
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'LITERAL';
this.currentNode.literalType = this.expectedLiteralType || 'literal';
this.expectedLiteralType = false;
this.state = STATE_LITERAL;
this.currentNode.isClosed = false;
break;
// * starts a new sequence
case '*':
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'SEQUENCE';
this.currentNode.value = chr;
this.currentNode.isClosed = false;
this.state = STATE_SEQUENCE;
break;
// normally a space should never occur
case ' ':
// just ignore
break;
// [ starts section
case '[':
// If it is the *first* element after response command, then process as a response argument list
if (['OK', 'NO', 'BAD', 'BYE', 'PREAUTH'].includes(this.parent.command.toUpperCase()) && this.currentNode === this.tree) {
this.currentNode.endPos = this.pos + i;
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'ATOM';
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'SECTION';
this.currentNode.isClosed = false;
this.state = STATE_NORMAL;
// RFC2221 defines a response code REFERRAL whose payload is an
// RFC2192/RFC5092 imapurl that we will try to parse as an ATOM but
// fail quite badly at parsing. Since the imapurl is such a unique
// (and crazy) term, we just specialize that case here.
if (this.str.substr(i + 1, 9).toUpperCase() === 'REFERRAL ') {
// create the REFERRAL atom
this.currentNode = this.createNode(this.currentNode, this.pos + i + 1);
this.currentNode.type = 'ATOM';
this.currentNode.endPos = this.pos + i + 8;
this.currentNode.value = 'REFERRAL';
this.currentNode = this.currentNode.parentNode;
// eat all the way through the ] to be the IMAPURL token.
this.currentNode = this.createNode(this.currentNode, this.pos + i + 10);
// just call this an ATOM, even though IMAPURL might be more correct
this.currentNode.type = 'ATOM';
// jump i to the ']'
i = this.str.indexOf(']', i + 10);
this.currentNode.endPos = this.pos + i - 1;
this.currentNode.value = this.str.substring(this.currentNode.startPos - this.pos, this.currentNode.endPos - this.pos + 1);
this.currentNode = this.currentNode.parentNode;
// close out the SECTION
this.currentNode.isClosed = true;
this.currentNode = this.currentNode.parentNode;
checkSP();
}
break;
}
/* falls through */
default:
// Any ATOM supported char starts a new Atom sequence, otherwise throw an error
// Allow \ as the first char for atom to support system flags
// Allow % to support LIST '' %
// Allow 8bit characters (presumably unicode)
if (imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 && chr !== '\\' && chr !== '%' && chr.charCodeAt(0) < 0x80) {
let error = new Error(`Unexpected char at position ${this.pos + i} [E13: ${JSON.stringify(chr)}]`);
error.code = 'ParserError13';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'ATOM';
this.currentNode.value = chr;
this.state = STATE_ATOM;
break;
}
break;
case STATE_ATOM:
// space finishes an atom
if (chr === ' ') {
this.currentNode.endPos = this.pos + i - 1;
this.currentNode = this.currentNode.parentNode;
this.state = STATE_NORMAL;
break;
}
//
if (
this.currentNode.parentNode &&
((chr === ')' && this.currentNode.parentNode.type === 'LIST') || (chr === ']' && this.currentNode.parentNode.type === 'SECTION'))
) {
this.currentNode.endPos = this.pos + i - 1;
this.currentNode = this.currentNode.parentNode;
this.currentNode.isClosed = true;
this.currentNode.endPos = this.pos + i;
this.currentNode = this.currentNode.parentNode;
this.state = STATE_NORMAL;
checkSP();
break;
}
if ((chr === ',' || chr === ':') && RE_DIGITS.test(this.currentNode.value)) {
this.currentNode.type = 'SEQUENCE';
this.currentNode.isClosed = true;
this.state = STATE_SEQUENCE;
}
// [ starts a section group for this element
// Allowed only for selected elements, otherwise falls through to regular ATOM processing
if (chr === '[' && ['BODY', 'BODY.PEEK', 'BINARY', 'BINARY.PEEK'].indexOf(this.currentNode.value.toUpperCase()) >= 0) {
this.currentNode.endPos = this.pos + i;
this.currentNode = this.createNode(this.currentNode.parentNode, this.pos + i);
this.currentNode.type = 'SECTION';
this.currentNode.isClosed = false;
this.state = STATE_NORMAL;
break;
}
// if the char is not ATOM compatible, throw. Allow \* as an exception
if (
imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 &&
chr.charCodeAt(0) < 0x80 && // allow 8bit (presumably unicode) bytes
chr !== ']' &&
!(chr === '*' && this.currentNode.value === '\\') &&
(!this.parent || !this.parent.command || !['NO', 'BAD', 'OK'].includes(this.parent.command))
) {
let error = new Error(`Unexpected char at position ${this.pos + i} [E16: ${JSON.stringify(chr)}]`);
error.code = 'ParserError16';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
} else if (this.currentNode.value === '\\*') {
let error = new Error(`Unexpected char at position ${this.pos + i} [E17: ${JSON.stringify(chr)}]`);
error.code = 'ParserError17';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode.value += chr;
break;
case STATE_STRING:
// DQUOTE ends the string sequence
if (chr === '"') {
this.currentNode.endPos = this.pos + i;
this.currentNode.isClosed = true;
this.currentNode = this.currentNode.parentNode;
this.state = STATE_NORMAL;
checkSP();
break;
}
// \ Escapes the following char
if (chr === '\\') {
i++;
if (i >= len) {
let error = new Error(`Unexpected end of input at position ${this.pos + i} [E18]`);
error.code = 'ParserError18';
error.parserContext = { input: this.str, pos: this.pos + i };
throw error;
}
chr = this.str.charAt(i);
}
this.currentNode.value += chr;
break;
case STATE_PARTIAL:
if (chr === '>') {
if (this.currentNode.value.at(-1) === '.') {
let error = new Error(`Unexpected end of partial at position ${this.pos + i} [E19]`);
error.code = 'ParserError19';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode.endPos = this.pos + i;
this.currentNode.isClosed = true;
this.currentNode = this.currentNode.parentNode;
this.state = STATE_NORMAL;
checkSP();
break;
}
if (chr === '.' && (!this.currentNode.value.length || this.currentNode.value.match(/\./))) {
let error = new Error(`Unexpected partial separator . at position ${this.pos + i} [E20]`);
error.code = 'ParserError20';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
if (imapFormalSyntax.DIGIT().indexOf(chr) < 0 && chr !== '.') {
let error = new Error(`Unexpected char at position ${this.pos + i} [E21: ${JSON.stringify(chr)}]`);
error.code = 'ParserError21';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
if (this.currentNode.value.match(/^0$|\.0$/) && chr !== '.') {
let error = new Error(`Invalid partial at position ${this.pos + i} [E22: ${JSON.stringify(chr)}]`);
error.code = 'ParserError22';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode.value += chr;
break;
case STATE_LITERAL:
if (this.currentNode.started) {
// only relevant if literals are not already parsed out from input
// Disabled NULL byte check
// See https://github.com/emailjs/emailjs-imap-handler/commit/f11b2822bedabe492236e8263afc630134a3c41c
/*
if (chr === '\u0000') {
throw new Error('Unexpected \\x00 at position ' + (this.pos + i));
}
*/
this.currentNode.chBuffer[this.currentNode.chPos++] = chr.charCodeAt(0);
if (this.currentNode.chPos >= this.currentNode.literalLength) {
this.currentNode.endPos = this.pos + i;
this.currentNode.isClosed = true;
this.currentNode.value = this.currentNode.chBuffer.toString('binary');
this.currentNode.chBuffer = Buffer.alloc(0);
this.currentNode = this.currentNode.parentNode;
this.state = STATE_NORMAL;
checkSP();
}
break;
}
if (chr === '+' && this.options.literalPlus) {
this.currentNode.literalPlus = true;
break;
}
if (chr === '}') {
if (!('literalLength' in this.currentNode)) {
let error = new Error(`Unexpected literal prefix end char } at position ${this.pos + i} [E23]`);
error.code = 'ParserError23';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
if (this.str.charAt(i + 1) === '\n') {
i++;
} else if (this.str.charAt(i + 1) === '\r' && this.str.charAt(i + 2) === '\n') {
i += 2;
} else {
let error = new Error(`Unexpected char at position ${this.pos + i} [E24: ${JSON.stringify(chr)}]`);
error.code = 'ParserError24';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode.literalLength = Number(this.currentNode.literalLength);
if (!this.currentNode.literalLength) {
// special case where literal content length is 0
// close the node right away, do not wait for additional input
this.currentNode.endPos = this.pos + i;
this.currentNode.isClosed = true;
this.currentNode = this.currentNode.parentNode;
this.state = STATE_NORMAL;
checkSP();
} else if (this.options.literals) {
// use the next precached literal values
this.currentNode.value = this.options.literals.shift();
// only APPEND arguments are kept as Buffers
/*
if ((this.parent.command || '').toString().toUpperCase() !== 'APPEND') {
this.currentNode.value = this.currentNode.value.toString('binary');
}
*/
this.currentNode.endPos = this.pos + i + this.currentNode.value.length;
this.currentNode.started = false;
this.currentNode.isClosed = true;
this.currentNode = this.currentNode.parentNode;
this.state = STATE_NORMAL;
checkSP();
} else {
this.currentNode.started = true;
// Allocate expected size buffer. Max size check is already performed
// Maybe should use allocUnsafe instead?
this.currentNode.chBuffer = Buffer.alloc(this.currentNode.literalLength);
this.currentNode.chPos = 0;
}
break;
}
if (imapFormalSyntax.DIGIT().indexOf(chr) < 0) {
let error = new Error(`Unexpected char at position ${this.pos + i} [E25: ${JSON.stringify(chr)}]`);
error.code = 'ParserError25';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
if (this.currentNode.literalLength === '0') {
let error = new Error(`Invalid literal at position ${this.pos + i} [E26]`);
error.code = 'ParserError26';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode.literalLength = (this.currentNode.literalLength || '') + chr;
break;
case STATE_SEQUENCE:
// space finishes the sequence set
if (chr === ' ') {
if (!RE_SINGLE_DIGIT.test(this.currentNode.value.at(-1)) && this.currentNode.value.at(-1) !== '*') {
let error = new Error(`Unexpected whitespace at position ${this.pos + i} [E27]`);
error.code = 'ParserError27';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
if (this.currentNode.value !== '*' && this.currentNode.value.at(-1) === '*' && this.currentNode.value.at(-2) !== ':') {
let error = new Error(`Unexpected whitespace at position ${this.pos + i} [E28]`);
error.code = 'ParserError28';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode.isClosed = true;
this.currentNode.endPos = this.pos + i - 1;
this.currentNode = this.currentNode.parentNode;
this.state = STATE_NORMAL;
break;
} else if (this.currentNode.parentNode && chr === ']' && this.currentNode.parentNode.type === 'SECTION') {
this.currentNode.endPos = this.pos + i - 1;
this.currentNode = this.currentNode.parentNode;
this.currentNode.isClosed = true;
this.currentNode.endPos = this.pos + i;
this.currentNode = this.currentNode.parentNode;
this.state = STATE_NORMAL;
checkSP();
break;
}
if (chr === ':') {
if (!RE_SINGLE_DIGIT.test(this.currentNode.value.at(-1)) && this.currentNode.value.at(-1) !== '*') {
let error = new Error(`Unexpected range separator : at position ${this.pos + i} [E29]`);
error.code = 'ParserError29';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
} else if (chr === '*') {
if ([',', ':'].indexOf(this.currentNode.value.at(-1)) < 0) {
let error = new Error(`Unexpected range wildcard at position ${this.pos + i} [E30]`);
error.code = 'ParserError30';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
} else if (chr === ',') {
if (!RE_SINGLE_DIGIT.test(this.currentNode.value.at(-1)) && this.currentNode.value.at(-1) !== '*') {
let error = new Error(`Unexpected sequence separator , at position ${this.pos + i} [E31]`);
error.code = 'ParserError31';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
if (this.currentNode.value.at(-1) === '*' && this.currentNode.value.at(-2) !== ':') {
let error = new Error(`Unexpected sequence separator , at position ${this.pos + i} [E32]`);
error.code = 'ParserError32';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
} else if (!RE_SINGLE_DIGIT.test(chr)) {
let error = new Error(`Unexpected char at position ${this.pos + i} [E33: ${JSON.stringify(chr)}]`);
error.code = 'ParserError33';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
if (RE_SINGLE_DIGIT.test(chr) && this.currentNode.value.at(-1) === '*') {
let error = new Error(`Unexpected number at position ${this.pos + i} [E34: ${JSON.stringify(chr)}]`);
error.code = 'ParserError34';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode.value += chr;
break;
case STATE_TEXT:
this.currentNode.value += chr;
break;
}
}
}
}
module.exports.TokenParser = TokenParser;

34
backend/node_modules/imapflow/lib/imap-commands.js generated vendored Normal file
View File

@@ -0,0 +1,34 @@
/* eslint global-require:0 */
'use strict';
module.exports = new Map([
['ID', require('./commands/id.js')],
['CAPABILITY', require('./commands/capability.js')],
['NAMESPACE', require('./commands/namespace.js')],
['LOGIN', require('./commands/login.js')],
['LOGOUT', require('./commands/logout.js')],
['STARTTLS', require('./commands/starttls.js')],
['LIST', require('./commands/list.js')],
['ENABLE', require('./commands/enable.js')],
['SELECT', require('./commands/select.js')],
['FETCH', require('./commands/fetch.js')],
['CREATE', require('./commands/create.js')],
['DELETE', require('./commands/delete.js')],
['RENAME', require('./commands/rename.js')],
['CLOSE', require('./commands/close.js')],
['SUBSCRIBE', require('./commands/subscribe.js')],
['UNSUBSCRIBE', require('./commands/unsubscribe.js')],
['STORE', require('./commands/store.js')],
['SEARCH', require('./commands/search.js')],
['NOOP', require('./commands/noop.js')],
['EXPUNGE', require('./commands/expunge.js')],
['APPEND', require('./commands/append.js')],
['STATUS', require('./commands/status.js')],
['COPY', require('./commands/copy.js')],
['MOVE', require('./commands/move.js')],
['COMPRESS', require('./commands/compress.js')],
['QUOTA', require('./commands/quota.js')],
['IDLE', require('./commands/idle.js')],
['AUTHENTICATE', require('./commands/authenticate.js')]
]);

790
backend/node_modules/imapflow/lib/imap-flow.d.ts generated vendored Normal file
View File

@@ -0,0 +1,790 @@
import { EventEmitter } from 'events';
import { ConnectionOptions } from 'tls';
import { Readable } from 'stream';
export interface ImapFlowOptions {
/** Hostname of the IMAP server */
host: string;
/** Port number for the IMAP server */
port: number;
/** If true, the connection will use TLS. If false, then TLS is used only if the server supports STARTTLS extension */
secure?: boolean;
/** Servername for SNI (or when host is set to an IP address) */
servername?: string;
/** If true, do not use COMPRESS=DEFLATE extension even if server supports it */
disableCompression?: boolean;
/** Authentication options */
auth?: {
/** Username */
user: string;
/** Password for regular authentication (if using OAuth2 then use `accessToken` instead) */
pass?: string;
/** OAuth2 access token, if using OAuth2 authentication */
accessToken?: string;
/** Optional login method override. Set to 'LOGIN', 'AUTH=LOGIN' or 'AUTH=PLAIN' to use specific method */
loginMethod?: string;
/** Authorization identity for SASL PLAIN (used for admin impersonation/delegation). When set, authenticates as `user` but authorizes as `authzid` */
authzid?: string;
};
/** Client identification info sent to the server if server supports ID extension */
clientInfo?: IdInfoObject;
/** If true, then do not start IDLE when connection is established */
disableAutoIdle?: boolean;
/** Additional TLS options (see Node.js TLS documentation) */
tls?: ConnectionOptions;
/** Custom logger instance. Set to false to disable logging */
logger?: Logger | false;
/** If true, log data read and written to socket encoded in base64 */
logRaw?: boolean;
/** If true, emit 'log' events */
emitLogs?: boolean;
/** If true, then logs out automatically after successful authentication */
verifyOnly?: boolean;
/** If true and verifyOnly is set, lists mailboxes */
includeMailboxes?: boolean;
/** Proxy URL. Supports HTTP CONNECT (http:, https:) and SOCKS (socks:, socks4:, socks5:) proxies */
proxy?: string;
/** If true, then use QRESYNC instead of CONDSTORE. EXPUNGE notifications will include UID instead of sequence number */
qresync?: boolean;
/** If set, then breaks and restarts IDLE every maxIdleTime ms */
maxIdleTime?: number;
/** What command to run if IDLE is not supported. Defaults to 'NOOP' */
missingIdleCommand?: 'NOOP' | 'SELECT' | 'STATUS';
/** If true, ignores BINARY extension when making FETCH and APPEND calls */
disableBinary?: boolean;
/** If true, do not enable supported extensions */
disableAutoEnable?: boolean;
/** How long to wait for the connection to be established. Defaults to 90 seconds */
connectionTimeout?: number;
/** How long to wait for the greeting. Defaults to 16 seconds */
greetingTimeout?: number;
/** How long to wait for socket inactivity before timing out the connection. Defaults to 5 minutes */
socketTimeout?: number;
/** If true, uses TLS. If false, uses cleartext. If not set, upgrades to TLS if available */
doSTARTTLS?: boolean;
/** Custom instance ID string for logs */
id?: string;
/** Optional expunge event handler function */
expungeHandler?: (event: ExpungeEvent) => Promise<void> | void;
}
export interface Logger {
debug(obj: any): void;
info(obj: any): void;
warn(obj: any): void;
error(obj: any): void;
}
export interface MailboxObject {
/** Mailbox path */
path: string;
/** Mailbox path delimiter, usually "." or "/" */
delimiter: string;
/** List of flags for this mailbox */
flags: Set<string>;
/** One of special-use flags (if applicable): "\All", "\Archive", "\Drafts", "\Flagged", "\Junk", "\Sent", "\Trash". Additionally INBOX has non-standard "\Inbox" flag set */
specialUse?: string;
/** True if mailbox was found from the output of LIST command */
listed?: boolean;
/** True if mailbox was found from the output of LSUB command */
subscribed?: boolean;
/** A Set of flags available to use in this mailbox. If it is not set or includes special flag "\*" then any flag can be used */
permanentFlags?: Set<string>;
/** Unique mailbox ID if server has OBJECTID extension enabled */
mailboxId?: string;
/** Latest known modseq value if server has CONDSTORE or XYMHIGHESTMODSEQ enabled */
highestModseq?: bigint;
/** If true then the server doesn't support the persistent storage of mod-sequences for the mailbox */
noModseq?: boolean;
/** Mailbox UIDVALIDITY value */
uidValidity: bigint;
/** Next predicted UID */
uidNext: number;
/** Messages in this folder */
exists: number;
/** Read-only state */
readOnly?: boolean;
}
export interface MailboxLockObject {
/** Mailbox path */
path: string;
/** Release current lock */
release(): void;
}
export interface IdInfoObject {
/** Name of the program */
name?: string;
/** Version number of the program */
version?: string;
/** Name of the operating system */
os?: string;
/** Vendor of the client/server */
vendor?: string;
/** URL to contact for support */
'support-url'?: string;
/** Date program was released */
date?: Date;
[key: string]: any;
}
export interface QuotaResponse {
/** Mailbox path this quota applies to */
path: string;
/** Storage quota if provided by server */
storage?: {
/** Used storage in bytes */
used: number;
/** Total storage available */
limit: number;
};
/** Message count quota if provided by server */
messages?: {
/** Stored messages */
used: number;
/** Maximum messages allowed */
limit: number;
};
}
export interface ListResponse {
/** Mailbox path (unicode string) */
path: string;
/** Mailbox path as listed in the LIST/LSUB response */
pathAsListed: string;
/** Mailbox name (last part of path after delimiter) */
name: string;
/** Mailbox path delimiter, usually "." or "/" */
delimiter: string;
/** An array of parent folder names. All names are in unicode */
parent: string[];
/** Same as parent, but as a complete string path (unicode string) */
parentPath: string;
/** A set of flags for this mailbox */
flags: Set<string>;
/** One of special-use flags (if applicable) */
specialUse?: string;
/** True if mailbox was found from the output of LIST command */
listed: boolean;
/** True if mailbox was found from the output of LSUB command */
subscribed: boolean;
/** If statusQuery was used, then this value includes the status response */
status?: StatusObject;
}
export interface ListOptions {
/** Request status items for every listed entry */
statusQuery?: {
/** If true request count of messages */
messages?: boolean;
/** If true request count of messages with \Recent tag */
recent?: boolean;
/** If true request predicted next UID */
uidNext?: boolean;
/** If true request mailbox UIDVALIDITY value */
uidValidity?: boolean;
/** If true request count of unseen messages */
unseen?: boolean;
/** If true request last known modseq value */
highestModseq?: boolean;
};
/** Set specific paths as special use folders */
specialUseHints?: {
/** Path to "Sent Mail" folder */
sent?: string;
/** Path to "Trash" folder */
trash?: string;
/** Path to "Junk Mail" folder */
junk?: string;
/** Path to "Drafts" folder */
drafts?: string;
};
}
export interface ListTreeResponse {
/** If true then this is root node without any additional properties besides folders */
root?: boolean;
/** Mailbox path */
path?: string;
/** Mailbox name (last part of path after delimiter) */
name?: string;
/** Mailbox path delimiter, usually "." or "/" */
delimiter?: string;
/** List of flags for this mailbox */
flags?: Set<string>;
/** One of special-use flags (if applicable) */
specialUse?: string;
/** True if mailbox was found from the output of LIST command */
listed?: boolean;
/** True if mailbox was found from the output of LSUB command */
subscribed?: boolean;
/** If true then this mailbox can not be selected in the UI */
disabled?: boolean;
/** An array of subfolders */
folders?: ListTreeResponse[];
/** Status response */
status?: StatusObject;
}
export interface MailboxCreateResponse {
/** Full mailbox path */
path: string;
/** Unique mailbox ID if server supports OBJECTID extension */
mailboxId?: string;
/** If true then mailbox was created otherwise it already existed */
created: boolean;
}
export interface MailboxRenameResponse {
/** Full mailbox path that was renamed */
path: string;
/** New full mailbox path */
newPath: string;
}
export interface MailboxDeleteResponse {
/** Full mailbox path that was deleted */
path: string;
}
export interface StatusObject {
/** Full mailbox path that was checked */
path: string;
/** Count of messages */
messages?: number;
/** Count of messages with \Recent tag */
recent?: number;
/** Predicted next UID */
uidNext?: number;
/** Mailbox UIDVALIDITY value */
uidValidity?: bigint;
/** Count of unseen messages */
unseen?: number;
/** Last known modseq value (if CONDSTORE extension is enabled) */
highestModseq?: bigint;
}
export type SequenceString = string | number | bigint;
export interface SearchObject {
/** Message ordering sequence range */
seq?: SequenceString;
/** Messages with (value is true) or without (value is false) \Answered flag */
answered?: boolean;
/** Messages with (value is true) or without (value is false) \Deleted flag */
deleted?: boolean;
/** Messages with (value is true) or without (value is false) \Draft flag */
draft?: boolean;
/** Messages with (value is true) or without (value is false) \Flagged flag */
flagged?: boolean;
/** Messages with (value is true) or without (value is false) \Seen flag */
seen?: boolean;
/** If true matches all messages */
all?: boolean;
/** If true matches messages that have the \Recent flag set but not the \Seen flag */
new?: boolean;
/** If true matches messages that do not have the \Recent flag set */
old?: boolean;
/** If true matches messages that have the \Recent flag set */
recent?: boolean;
/** Matches From: address field */
from?: string;
/** Matches To: address field */
to?: string;
/** Matches Cc: address field */
cc?: string;
/** Matches Bcc: address field */
bcc?: string;
/** Matches message body */
body?: string;
/** Matches message subject */
subject?: string;
/** Matches any text in headers and body */
text?: string;
/** Matches messages larger than value */
larger?: number;
/** Matches messages smaller than value */
smaller?: number;
/** UID sequence range */
uid?: SequenceString;
/** Matches messages with modseq higher than value */
modseq?: bigint;
/** Unique email ID. Only used if server supports OBJECTID or X-GM-EXT-1 extensions */
emailId?: string;
/** Unique thread ID. Only used if server supports OBJECTID or X-GM-EXT-1 extensions */
threadId?: string;
/** Matches messages received before date */
before?: Date | string;
/** Matches messages received on date (ignores time) */
on?: Date | string;
/** Matches messages received after date */
since?: Date | string;
/** Matches messages sent before date */
sentBefore?: Date | string;
/** Matches messages sent on date (ignores time) */
sentOn?: Date | string;
/** Matches messages sent after date */
sentSince?: Date | string;
/** Matches messages that have the custom flag set */
keyword?: string;
/** Matches messages that do not have the custom flag set */
unKeyword?: string;
/** Matches messages with header key set if value is true or messages where header partially matches a string value */
header?: { [key: string]: boolean | string };
/** A SearchObject object. It must not match */
not?: SearchObject;
/** An array of 2 or more SearchObject objects. At least one of these must match */
or?: SearchObject[];
/** Gmail raw search query (only for Gmail) */
gmraw?: string;
/** Gmail raw search query (alias for gmraw) */
gmailraw?: string;
}
export interface FetchQueryObject {
/** If true then include UID in the response */
uid?: boolean;
/** If true then include flags Set in the response */
flags?: boolean;
/** If true then include parsed BODYSTRUCTURE object in the response */
bodyStructure?: boolean;
/** If true then include parsed ENVELOPE object in the response */
envelope?: boolean;
/** If true then include internal date value in the response */
internalDate?: boolean;
/** If true then include message size in the response */
size?: boolean;
/** If true then include full message in the response */
source?: boolean | {
/** Include full message in the response starting from start byte */
start?: number;
/** Include full message in the response, up to maxLength bytes */
maxLength?: number;
};
/** If true then include thread ID in the response (only if server supports either OBJECTID or X-GM-EXT-1 extensions) */
threadId?: boolean;
/** If true then include GMail labels in the response (only if server supports X-GM-EXT-1 extension) */
labels?: boolean;
/** If true then includes full headers of the message in the response. If the value is an array of header keys then includes only headers listed in the array */
headers?: boolean | string[];
/** An array of BODYPART identifiers to include in the response */
bodyParts?: Array<string | { key: string; start?: number; maxLength?: number }>;
/** Fast macro equivalent to flags, internalDate, size */
fast?: boolean;
/** All macro equivalent to flags, internalDate, size, envelope */
all?: boolean;
/** Full macro equivalent to flags, internalDate, size, envelope, bodyStructure */
full?: boolean;
}
export interface MessageAddressObject {
/** Name of the address object (unicode) */
name?: string;
/** Email address */
address?: string;
}
export interface MessageEnvelopeObject {
/** Header date */
date?: Date;
/** Message subject (unicode) */
subject?: string;
/** Message ID of the message */
messageId?: string;
/** Message ID from In-Reply-To header */
inReplyTo?: string;
/** Array of addresses from the From: header */
from?: MessageAddressObject[];
/** Array of addresses from the Sender: header */
sender?: MessageAddressObject[];
/** Array of addresses from the Reply-To: header */
replyTo?: MessageAddressObject[];
/** Array of addresses from the To: header */
to?: MessageAddressObject[];
/** Array of addresses from the Cc: header */
cc?: MessageAddressObject[];
/** Array of addresses from the Bcc: header */
bcc?: MessageAddressObject[];
}
export interface MessageStructureObject {
/** Body part number. This value can be used to later fetch the contents of this part of the message */
part?: string;
/** Content-Type of this node */
type: string;
/** Additional parameters for Content-Type, eg "charset" */
parameters?: { [key: string]: string };
/** Content-ID */
id?: string;
/** Transfer encoding */
encoding?: string;
/** Expected size of the node */
size?: number;
/** Message envelope of embedded RFC822 message */
envelope?: MessageEnvelopeObject;
/** Content disposition */
disposition?: string;
/** Additional parameters for Content-Disposition */
dispositionParameters?: { [key: string]: string };
/** An array of child nodes if this is a multipart node */
childNodes?: MessageStructureObject[];
/** MD5 hash */
md5?: string;
/** Language */
language?: string[];
/** Location */
location?: string;
/** Description */
description?: string;
/** Line count */
lineCount?: number;
}
export interface FetchMessageObject {
/** Message sequence number. Always included in the response */
seq: number;
/** Message UID number. Always included in the response */
uid: number;
/** Message source for the requested byte range */
source?: Buffer;
/** Message Modseq number. Always included if the server supports CONDSTORE extension */
modseq?: bigint;
/** Unique email ID. Always included if server supports OBJECTID or X-GM-EXT-1 extensions */
emailId?: string;
/** Unique thread ID. Only present if server supports OBJECTID or X-GM-EXT-1 extension */
threadId?: string;
/** A Set of labels. Only present if server supports X-GM-EXT-1 extension */
labels?: Set<string>;
/** Message size */
size?: number;
/** A set of message flags */
flags?: Set<string>;
/** Flag color like "red", or "yellow". This value is derived from the flags Set */
flagColor?: string;
/** Message envelope */
envelope?: MessageEnvelopeObject;
/** Message body structure */
bodyStructure?: MessageStructureObject;
/** Message internal date */
internalDate?: Date | string;
/** A Map of message body parts where key is requested part identifier and value is a Buffer */
bodyParts?: Map<string, Buffer>;
/** Requested header lines as Buffer */
headers?: Buffer;
/** Account unique ID for this email */
id?: string;
}
export interface DownloadObject {
/** Content metadata */
meta: {
/** The fetch response size */
expectedSize: number;
/** Content-Type of the streamed file */
contentType: string;
/** Charset of the body part */
charset?: string;
/** Content-Disposition of the streamed file */
disposition?: string;
/** Filename of the streamed body part */
filename?: string;
/** Transfer encoding */
encoding?: string;
/** If content uses flowed formatting */
flowed?: boolean;
/** If flowed text uses delSp */
delSp?: boolean;
};
/** Streamed content */
content: Readable;
}
export interface AppendResponseObject {
/** Full mailbox path where the message was uploaded to */
destination: string;
/** Mailbox UIDVALIDITY if server has UIDPLUS extension enabled */
uidValidity?: bigint;
/** UID of the uploaded message if server has UIDPLUS extension enabled */
uid?: number;
/** Sequence number of the uploaded message if path is currently selected mailbox */
seq?: number;
}
export interface CopyResponseObject {
/** Path of source mailbox */
path: string;
/** Path of destination mailbox */
destination: string;
/** Destination mailbox UIDVALIDITY if server has UIDPLUS extension enabled */
uidValidity?: bigint;
/** Map of UID values where key is UID in source mailbox and value is the UID for the same message in destination mailbox */
uidMap?: Map<number, number>;
}
export interface FetchOptions {
/** If true then uses UID numbers instead of sequence numbers */
uid?: boolean;
/** If set then only messages with a higher modseq value are returned */
changedSince?: bigint;
/** If true then requests a binary response if the server supports this */
binary?: boolean;
}
export interface StoreOptions {
/** If true then uses UID numbers instead of sequence numbers */
uid?: boolean;
/** If set then only messages with a lower or equal modseq value are updated */
unchangedSince?: bigint;
/** If true then update Gmail labels instead of message flags */
useLabels?: boolean;
/** If true then does not emit 'flags' event */
silent?: boolean;
}
export interface MailboxOpenOptions {
/** If true then opens mailbox in read-only mode */
readOnly?: boolean;
/** Optional description for mailbox lock tracking */
description?: string;
}
export interface ExpungeEvent {
/** Mailbox path */
path: string;
/** Sequence number (if vanished is false) */
seq?: number;
/** UID number (if vanished is true or QRESYNC is enabled) */
uid?: number;
/** True if message was expunged using VANISHED response */
vanished: boolean;
/** True if VANISHED EARLIER response */
earlier?: boolean;
}
export interface ExistsEvent {
/** Mailbox path */
path: string;
/** Updated count of messages */
count: number;
/** Message count before this update */
prevCount: number;
}
export interface FlagsEvent {
/** Mailbox path */
path: string;
/** Sequence number of updated message */
seq: number;
/** UID number of updated message (if server provided this value) */
uid?: number;
/** Updated modseq number for the mailbox */
modseq?: bigint;
/** A set of all flags for the updated message */
flags: Set<string>;
/** Flag color if message is flagged */
flagColor?: string;
}
export interface LogEvent {
/** Log level */
level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
/** Timestamp */
t: number;
/** Connection ID */
cid: string;
/** Log order number */
lo: number;
/** Additional log data */
[key: string]: any;
}
export interface ResponseEvent {
/** Response type */
response: string;
/** Response code */
code?: string;
}
export class AuthenticationFailure extends Error {
authenticationFailed: true;
serverResponseCode?: string;
response?: string;
oauthError?: any;
}
export class ImapFlow extends EventEmitter {
/** Current module version */
static version: string;
/** Instance ID for logs */
id: string;
/** Server identification info */
serverInfo: IdInfoObject | null;
/** Is the connection currently encrypted or not */
secureConnection: boolean;
/** Active IMAP capabilities */
capabilities: Map<string, boolean | number>;
/** Enabled capabilities */
enabled: Set<string>;
/** Is the connection currently usable or not */
usable: boolean;
/** Currently authenticated user */
authenticated: string | boolean;
/** Currently selected mailbox */
mailbox: MailboxObject | false;
/** Is current mailbox idling */
idling: boolean;
constructor(options: ImapFlowOptions);
/** Initiates a connection against IMAP server */
connect(): Promise<void>;
/** Graceful connection close by sending logout command to server */
logout(): Promise<void>;
/** Closes TCP connection without notifying the server */
close(): void;
/** Returns current quota */
getQuota(path?: string): Promise<QuotaResponse | false>;
/** Lists available mailboxes as an Array */
list(options?: ListOptions): Promise<ListResponse[]>;
/** Lists available mailboxes as a tree structured object */
listTree(options?: ListOptions): Promise<ListTreeResponse>;
/** Performs a no-op call against server */
noop(): Promise<void>;
/** Creates a new mailbox folder */
mailboxCreate(path: string | string[]): Promise<MailboxCreateResponse>;
/** Renames a mailbox */
mailboxRename(path: string | string[], newPath: string | string[]): Promise<MailboxRenameResponse>;
/** Deletes a mailbox */
mailboxDelete(path: string | string[]): Promise<MailboxDeleteResponse>;
/** Subscribes to a mailbox */
mailboxSubscribe(path: string | string[]): Promise<boolean>;
/** Unsubscribes from a mailbox */
mailboxUnsubscribe(path: string | string[]): Promise<boolean>;
/** Opens a mailbox to access messages */
mailboxOpen(path: string | string[], options?: MailboxOpenOptions): Promise<MailboxObject>;
/** Closes a previously opened mailbox */
mailboxClose(): Promise<boolean>;
/** Requests the status of the indicated mailbox */
status(path: string, query: {
messages?: boolean;
recent?: boolean;
uidNext?: boolean;
uidValidity?: boolean;
unseen?: boolean;
highestModseq?: boolean;
}): Promise<StatusObject>;
/** Starts listening for new or deleted messages from the currently opened mailbox */
idle(): Promise<boolean>;
/** Sets flags for a message or message range */
messageFlagsSet(range: SequenceString | number[] | SearchObject, flags: string[], options?: StoreOptions): Promise<boolean>;
/** Adds flags for a message or message range */
messageFlagsAdd(range: SequenceString | number[] | SearchObject, flags: string[], options?: StoreOptions): Promise<boolean>;
/** Remove specific flags from a message or message range */
messageFlagsRemove(range: SequenceString | number[] | SearchObject, flags: string[], options?: StoreOptions): Promise<boolean>;
/** Sets a colored flag for an email */
setFlagColor(range: SequenceString | number[] | SearchObject, color: string, options?: StoreOptions): Promise<boolean>;
/** Delete messages from the currently opened mailbox */
messageDelete(range: SequenceString | number[] | SearchObject, options?: { uid?: boolean }): Promise<boolean>;
/** Appends a new message to a mailbox */
append(path: string, content: string | Buffer, flags?: string[], idate?: Date | string): Promise<AppendResponseObject | false>;
/** Copies messages from current mailbox to destination mailbox */
messageCopy(range: SequenceString | number[] | SearchObject, destination: string, options?: { uid?: boolean }): Promise<CopyResponseObject | false>;
/** Moves messages from current mailbox to destination mailbox */
messageMove(range: SequenceString | number[] | SearchObject, destination: string, options?: { uid?: boolean }): Promise<CopyResponseObject | false>;
/** Search messages from the currently opened mailbox */
search(query: SearchObject, options?: { uid?: boolean }): Promise<number[] | false>;
/** Fetch messages from the currently opened mailbox */
fetch(range: SequenceString | number[] | SearchObject, query: FetchQueryObject, options?: FetchOptions): AsyncIterableIterator<FetchMessageObject>;
/** Fetch all messages from the currently opened mailbox */
fetchAll(range: SequenceString | number[] | SearchObject, query: FetchQueryObject, options?: FetchOptions): Promise<FetchMessageObject[]>;
/** Fetch a single message from the currently opened mailbox */
fetchOne(seq: SequenceString, query: FetchQueryObject, options?: FetchOptions): Promise<FetchMessageObject | false>;
/** Download either full rfc822 formatted message or a specific bodystructure part as a Stream */
download(range: SequenceString, part?: string, options?: {
uid?: boolean;
maxBytes?: number;
chunkSize?: number;
}): Promise<DownloadObject>;
/** Fetch multiple attachments as Buffer values */
downloadMany(range: SequenceString, parts: string[], options?: { uid?: boolean }): Promise<{
[part: string]: {
meta: {
contentType?: string;
charset?: string;
disposition?: string;
filename?: string;
encoding?: string;
};
content: Buffer | null;
}
}>;
/** Opens a mailbox if not already open and returns a lock */
getMailboxLock(path: string | string[], options?: MailboxOpenOptions): Promise<MailboxLockObject>;
/** Connection close event */
on(event: 'close', listener: () => void): this;
/** Error event */
on(event: 'error', listener: (error: Error) => void): this;
/** Message count in currently opened mailbox changed */
on(event: 'exists', listener: (data: ExistsEvent) => void): this;
/** Deleted message sequence number in currently opened mailbox */
on(event: 'expunge', listener: (data: ExpungeEvent) => void): this;
/** Flags were updated for a message */
on(event: 'flags', listener: (data: FlagsEvent) => void): this;
/** Mailbox was opened */
on(event: 'mailboxOpen', listener: (mailbox: MailboxObject) => void): this;
/** Mailbox was closed */
on(event: 'mailboxClose', listener: (mailbox: MailboxObject) => void): this;
/** Log event if emitLogs=true */
on(event: 'log', listener: (entry: LogEvent) => void): this;
/** Response event */
on(event: 'response', listener: (response: ResponseEvent) => void): this;
}

3678
backend/node_modules/imapflow/lib/imap-flow.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

46
backend/node_modules/imapflow/lib/jp-decoder.js generated vendored Normal file
View File

@@ -0,0 +1,46 @@
'use strict';
const { Transform } = require('stream');
const encodingJapanese = require('encoding-japanese');
class JPDecoder extends Transform {
constructor(charset) {
super();
this.charset = charset;
this.chunks = [];
this.chunklen = 0;
}
_transform(chunk, encoding, done) {
if (typeof chunk === 'string') {
chunk = Buffer.from(chunk, encoding);
}
this.chunks.push(chunk);
this.chunklen += chunk.length;
done();
}
_flush(done) {
let input = Buffer.concat(this.chunks, this.chunklen);
try {
let output = encodingJapanese.convert(input, {
to: 'UNICODE', // to_encoding
from: this.charset, // from_encoding
type: 'string'
});
if (typeof output === 'string') {
output = Buffer.from(output);
}
this.push(output);
} catch {
// keep as is on errors
this.push(input);
}
done();
}
}
module.exports.JPDecoder = JPDecoder;

View File

@@ -0,0 +1,37 @@
'use strict';
const { Transform } = require('stream');
class LimitedPassthrough extends Transform {
constructor(options) {
super();
this.options = options || {};
this.maxBytes = this.options.maxBytes || Infinity;
this.processed = 0;
this.limited = false;
}
_transform(chunk, encoding, done) {
if (this.limited) {
return done();
}
if (this.processed + chunk.length > this.maxBytes) {
if (this.maxBytes - this.processed < 1) {
return done();
}
chunk = chunk.slice(0, this.maxBytes - this.processed);
}
this.processed += chunk.length;
if (this.processed >= this.maxBytes) {
this.limited = true;
}
this.push(chunk);
done();
}
}
module.exports.LimitedPassthrough = LimitedPassthrough;

5
backend/node_modules/imapflow/lib/logger.js generated vendored Normal file
View File

@@ -0,0 +1,5 @@
'use strict';
const logger = require('pino')();
logger.level = 'trace';
module.exports = logger;

120
backend/node_modules/imapflow/lib/proxy-connection.js generated vendored Normal file
View File

@@ -0,0 +1,120 @@
'use strict';
const httpProxyClient = require('nodemailer/lib/smtp-connection/http-proxy-client');
const { SocksClient } = require('socks');
const util = require('util');
const httpProxyClientAsync = util.promisify(httpProxyClient);
const dns = require('dns').promises;
const net = require('net');
const proxyConnection = async (logger, connectionUrl, host, port) => {
let proxyUrl = new URL(connectionUrl);
let protocol = proxyUrl.protocol.replace(/:$/, '').toLowerCase();
if (!net.isIP(host)) {
let resolveResult = await dns.resolve(host);
if (resolveResult && resolveResult.length) {
host = resolveResult[0];
}
}
switch (protocol) {
// Connect using a HTTP CONNECT method
case 'http':
case 'https': {
try {
let socket = await httpProxyClientAsync(proxyUrl.href, port, host);
if (socket) {
if (proxyUrl.password) {
proxyUrl.password = '(hidden)';
}
logger.info({
msg: 'Established a socket via HTTP proxy',
proxyUrl: proxyUrl.href,
port,
host
});
}
return socket;
} catch (err) {
if (proxyUrl.password) {
proxyUrl.password = '(hidden)';
}
logger.error({
msg: 'Failed to establish a socket via HTTP proxy',
proxyUrl: proxyUrl.href,
port,
host,
err
});
throw err;
}
}
// SOCKS proxy
case 'socks':
case 'socks5':
case 'socks4':
case 'socks4a': {
let proxyType = Number(protocol.replace(/\D/g, '')) || 5;
let targetHost = proxyUrl.hostname;
if (!net.isIP(targetHost)) {
let resolveResult = await dns.resolve(targetHost);
if (resolveResult && resolveResult.length) {
targetHost = resolveResult[0];
}
}
let connectionOpts = {
proxy: {
host: targetHost,
port: Number(proxyUrl.port) || 1080,
type: proxyType
},
destination: {
host,
port
},
command: 'connect',
set_tcp_nodelay: true
};
if (proxyUrl.username || proxyUrl.password) {
connectionOpts.proxy.userId = proxyUrl.username;
connectionOpts.proxy.password = proxyUrl.password;
}
try {
const info = await SocksClient.createConnection(connectionOpts);
if (info && info.socket) {
if (proxyUrl.password) {
proxyUrl.password = '(hidden)';
}
logger.info({
msg: 'Established a socket via SOCKS proxy',
proxyUrl: proxyUrl.href,
port,
host
});
}
return info.socket;
} catch (err) {
if (proxyUrl.password) {
proxyUrl.password = '(hidden)';
}
logger.error({
msg: 'Failed to establish a socket via SOCKS proxy',
proxyUrl: proxyUrl.href,
port,
host,
err
});
throw err;
}
}
}
};
module.exports = { proxyConnection };

435
backend/node_modules/imapflow/lib/search-compiler.js generated vendored Normal file
View File

@@ -0,0 +1,435 @@
/* eslint no-control-regex:0 */
'use strict';
const { formatDate, formatFlag, canUseFlag, isDate } = require('./tools.js');
/**
* Sets a boolean flag in the IMAP search attributes.
* Automatically handles UN- prefixing for falsy values.
*
* @param {Array} attributes - Array to append the attribute to
* @param {string} term - The flag name (e.g., 'SEEN', 'DELETED')
* @param {boolean} value - Whether to set or unset the flag
* @example
* setBoolOpt(attributes, 'SEEN', false) // Adds 'UNSEEN'
* setBoolOpt(attributes, 'UNSEEN', false) // Adds 'SEEN' (removes UN prefix)
*/
let setBoolOpt = (attributes, term, value) => {
if (!value) {
// For falsy values, toggle the UN- prefix
if (/^un/i.test(term)) {
// Remove existing UN prefix
term = term.slice(2);
} else {
// Add UN prefix
term = 'UN' + term;
}
}
attributes.push({ type: 'ATOM', value: term.toUpperCase() });
};
/**
* Adds a search option with its value(s) to the attributes array.
* Handles NOT operations and array values.
*
* @param {Array} attributes - Array to append the attribute to
* @param {string} term - The search term (e.g., 'FROM', 'SUBJECT')
* @param {*} value - The value for the search term (string, array, or falsy for NOT)
* @param {string} [type='ATOM'] - The attribute type
*/
let setOpt = (attributes, term, value, type) => {
type = type || 'ATOM';
// Handle NOT operations for false or null values
if (value === false || value === null) {
attributes.push({ type, value: 'NOT' });
}
attributes.push({ type, value: term.toUpperCase() });
// Handle array values (e.g., multiple UIDs)
if (Array.isArray(value)) {
value.forEach(entry => attributes.push({ type, value: (entry || '').toString() }));
} else {
attributes.push({ type, value: value.toString() });
}
};
/**
* Processes date fields for IMAP search.
* Converts JavaScript dates to IMAP date format.
*
* @param {Array} attributes - Array to append the attribute to
* @param {string} term - The date search term (e.g., 'BEFORE', 'SINCE')
* @param {*} value - Date value to format
*/
let processDateField = (attributes, term, value) => {
if (['BEFORE', 'SENTBEFORE'].includes(term.toUpperCase()) && isDate(value) && value.toISOString().substring(11) !== '00:00:00.000Z') {
// Set to next day to include current day as well, othwerise BEFORE+AFTER
// searches for the same day but different time values do not match anything
value = new Date(value.getTime() + 24 * 3600 * 1000);
}
let date = formatDate(value);
if (!date) {
return;
}
setOpt(attributes, term, date);
};
// Pre-compiled regex for better performance
const UNICODE_PATTERN = /[^\x00-\x7F]/;
/**
* Checks if a string contains Unicode characters.
* Used to determine if CHARSET UTF-8 needs to be specified.
*
* @param {*} str - String to check
* @returns {boolean} True if string contains non-ASCII characters
*/
let isUnicodeString = str => {
if (!str || typeof str !== 'string') {
return false;
}
// Regex test is ~3-5x faster than Buffer.byteLength
// Matches any character outside ASCII range (0x00-0x7F)
return UNICODE_PATTERN.test(str);
};
/**
* Compiles a JavaScript object query into IMAP search command attributes.
* Supports standard IMAP search criteria and extensions like OBJECTID and Gmail extensions.
*
* @param {Object} connection - IMAP connection object
* @param {Object} connection.capabilities - Set of server capabilities
* @param {Object} connection.enabled - Set of enabled extensions
* @param {Object} connection.mailbox - Current mailbox information
* @param {Set} connection.mailbox.flags - Available flags in the mailbox
* @param {Object} query - Search query object
* @returns {Array} Array of IMAP search attributes
* @throws {Error} When required server extensions are not available
*
* @example
* // Simple search for unseen messages from a sender
* searchCompiler(connection, {
* unseen: true,
* from: 'sender@example.com'
* });
*
* @example
* // Complex OR search with date range
* searchCompiler(connection, {
* or: [
* { from: 'alice@example.com' },
* { from: 'bob@example.com' }
* ],
* since: new Date('2024-01-01')
* });
*/
module.exports.searchCompiler = (connection, query) => {
const attributes = [];
// Track if we need to specify UTF-8 charset
let hasUnicode = false;
const mailbox = connection.mailbox;
/**
* Recursively walks through the query object and builds IMAP attributes.
* @param {Object} params - Query parameters to process
*/
const walk = params => {
Object.keys(params || {}).forEach(term => {
switch (term.toUpperCase()) {
// Custom sequence range support (non-standard)
case 'SEQ':
{
let value = params[term];
if (typeof value === 'number') {
value = value.toString();
}
// Only accept valid sequence strings (no whitespace)
if (typeof value === 'string' && /^\S+$/.test(value)) {
attributes.push({ type: 'SEQUENCE', value });
}
}
break;
// Boolean flags that support UN- prefixing
case 'ANSWERED':
case 'DELETED':
case 'DRAFT':
case 'FLAGGED':
case 'SEEN':
case 'UNANSWERED':
case 'UNDELETED':
case 'UNDRAFT':
case 'UNFLAGGED':
case 'UNSEEN':
// toggles UN-prefix for falsy values
setBoolOpt(attributes, term, !!params[term]);
break;
// Simple boolean flags without UN- support
case 'ALL':
case 'NEW':
case 'OLD':
case 'RECENT':
if (params[term]) {
setBoolOpt(attributes, term, true);
}
break;
// Numeric comparisons
case 'LARGER':
case 'SMALLER':
case 'MODSEQ':
if (params[term]) {
setOpt(attributes, term, params[term]);
}
break;
// Text search fields - check for Unicode
case 'BCC':
case 'BODY':
case 'CC':
case 'FROM':
case 'SUBJECT':
case 'TEXT':
case 'TO':
if (isUnicodeString(params[term])) {
hasUnicode = true;
}
if (params[term]) {
setOpt(attributes, term, params[term]);
}
break;
// UID sequences
case 'UID':
if (params[term]) {
setOpt(attributes, term, params[term], 'SEQUENCE');
}
break;
// Email ID support (OBJECTID or Gmail extension)
case 'EMAILID':
if (connection.capabilities.has('OBJECTID')) {
setOpt(attributes, 'EMAILID', params[term]);
} else if (connection.capabilities.has('X-GM-EXT-1')) {
// Fallback to Gmail message ID
setOpt(attributes, 'X-GM-MSGID', params[term]);
}
break;
// Thread ID support (OBJECTID or Gmail extension)
case 'THREADID':
if (connection.capabilities.has('OBJECTID')) {
setOpt(attributes, 'THREADID', params[term]);
} else if (connection.capabilities.has('X-GM-EXT-1')) {
// Fallback to Gmail thread ID
setOpt(attributes, 'X-GM-THRID', params[term]);
}
break;
// Gmail raw search
case 'GMRAW':
case 'GMAILRAW': // alias for GMRAW
if (connection.capabilities.has('X-GM-EXT-1')) {
if (isUnicodeString(params[term])) {
hasUnicode = true;
}
setOpt(attributes, 'X-GM-RAW', params[term]);
} else {
let error = new Error('Server does not support X-GM-EXT-1 extension required for X-GM-RAW');
error.code = 'MissingServerExtension';
throw error;
}
break;
// Date searches with WITHIN extension support
case 'BEFORE':
case 'SINCE':
{
// Use WITHIN extension for better timezone handling if available
if (connection.capabilities.has('WITHIN') && isDate(params[term])) {
// Convert to seconds ago from now
const now = Date.now();
const withinSeconds = Math.round(Math.max(0, now - params[term].getTime()) / 1000);
let withinKeyword;
switch (term.toUpperCase()) {
case 'BEFORE':
withinKeyword = 'OLDER';
break;
case 'SINCE':
withinKeyword = 'YOUNGER';
break;
}
setOpt(attributes, withinKeyword, withinSeconds.toString());
break;
}
// Fallback to standard date search
processDateField(attributes, term, params[term]);
}
break;
// Standard date searches
case 'ON':
case 'SENTBEFORE':
case 'SENTON':
case 'SENTSINCE':
processDateField(attributes, term, params[term]);
break;
// Keyword/flag searches
case 'KEYWORD':
case 'UNKEYWORD':
{
let flag = formatFlag(params[term]);
// Only add if flag is supported or already exists in mailbox
if (canUseFlag(mailbox, flag) || mailbox.flags.has(flag)) {
setOpt(attributes, term, flag);
}
}
break;
// Header field searches
case 'HEADER':
if (params[term] && typeof params[term] === 'object') {
Object.keys(params[term]).forEach(header => {
let value = params[term][header];
// Allow boolean true to search for header existence
if (value === true) {
value = '';
}
// Skip non-string values (after true->'' conversion)
if (typeof value !== 'string') {
return;
}
if (isUnicodeString(value)) {
hasUnicode = true;
}
setOpt(attributes, term, [header.toUpperCase().trim(), value]);
});
}
break;
// NOT operator
case 'NOT':
{
if (!params[term]) {
break;
}
if (typeof params[term] === 'object') {
attributes.push({ type: 'ATOM', value: 'NOT' });
// Recursively process NOT conditions
walk(params[term]);
}
}
break;
// OR operator - complex logic for building OR trees
case 'OR':
{
if (!params[term] || !Array.isArray(params[term]) || !params[term].length) {
break;
}
// Single element - just process it directly
if (params[term].length === 1) {
if (typeof params[term][0] === 'object' && params[term][0]) {
walk(params[term][0]);
}
break;
}
/**
* Generates a binary tree structure for OR operations.
* IMAP OR takes exactly 2 operands, so we need to nest them.
*
* @param {Array} list - List of conditions to OR together
* @returns {Array} Binary tree structure
*/
let genOrTree = list => {
let group = false;
let groups = [];
// Group items in pairs
list.forEach((entry, i) => {
if (i % 2 === 0) {
group = [entry];
} else {
group.push(entry);
groups.push(group);
group = false;
}
});
// Handle odd number of items
if (group && group.length) {
while (group.length === 1 && Array.isArray(group[0])) {
group = group[0];
}
groups.push(group);
}
// Recursively group until we have a binary tree
while (groups.length > 2) {
groups = genOrTree(groups);
}
// Flatten single-element arrays
while (groups.length === 1 && Array.isArray(groups[0])) {
groups = groups[0];
}
return groups;
};
/**
* Walks the OR tree and generates IMAP commands.
* @param {Array|Object} entry - Tree node to process
*/
let walkOrTree = entry => {
if (Array.isArray(entry)) {
// Only add OR for multiple items
if (entry.length > 1) {
attributes.push({ type: 'ATOM', value: 'OR' });
}
entry.forEach(walkOrTree);
return;
}
if (entry && typeof entry === 'object') {
walk(entry);
}
};
walkOrTree(genOrTree(params[term]));
}
break;
}
});
};
// Process the query
walk(query);
// If we encountered Unicode strings and UTF-8 is not already accepted,
// prepend CHARSET UTF-8 to the search command
if (hasUnicode && !connection.enabled.has('UTF8=ACCEPT')) {
attributes.unshift({ type: 'ATOM', value: 'UTF-8' });
attributes.unshift({ type: 'ATOM', value: 'CHARSET' });
}
return attributes;
};

307
backend/node_modules/imapflow/lib/special-use.js generated vendored Normal file
View File

@@ -0,0 +1,307 @@
'use strict';
module.exports = {
flags: ['\\All', '\\Archive', '\\Drafts', '\\Flagged', '\\Junk', '\\Sent', '\\Trash'],
names: {
'\\Sent': [
'aika',
'bidaliak',
'bidalita',
'dihantar',
'e rometsweng',
'e tindami',
'elküldött',
'elküldöttek',
'elementos enviados',
'éléments envoyés',
'enviadas',
'enviadas',
'enviados',
'enviats',
'envoyés',
'ethunyelweyo',
'expediate',
'ezipuru',
'gesendete',
'gesendete elemente',
'gestuur',
'gönderilmiş öğeler',
'göndərilənlər',
'iberilen',
'inviati',
'išsiųstieji',
'kuthunyelwe',
'lasa',
'lähetetyt',
'messages envoyés',
'naipadala',
'nalefa',
'napadala',
'nosūtītās ziņas',
'odeslané',
'odeslaná pošta',
'padala',
'poslane',
'poslano',
'poslano',
'poslané',
'poslato',
'saadetud',
'saadetud kirjad',
'saadetud üksused',
'sendt',
'sendt',
'sent',
'sent items',
'sent messages',
'sända poster',
'sänt',
'terkirim',
'ti fi ranṣẹ',
'të dërguara',
'verzonden',
'vilivyotumwa',
'wysłane',
'đã gửi',
'σταλθέντα',
'жиберилген',
'жіберілгендер',
'изпратени',
'илгээсэн',
'ирсол шуд',
'испратено',
'надіслані',
'отправленные',
'пасланыя',
'юборилган',
'ուղարկված',
'נשלחו',
'פריטים שנשלחו',
'المرسلة',
'بھیجے گئے',
'سوزمژہ',
'لېګل شوی',
'موارد ارسال شده',
'पाठविले',
'पाठविलेले',
'प्रेषित',
'भेजा गया',
'প্রেরিত',
'প্রেরিত',
'প্ৰেৰিত',
'ਭੇਜੇ',
'મોકલેલા',
'ପଠାଗଲା',
'அனுப்பியவை',
'పంపించబడింది',
'ಕಳುಹಿಸಲಾದ',
'അയച്ചു',
'යැවු පණිවුඩ',
'ส่งแล้ว',
'გაგზავნილი',
'የተላኩ',
'បាន​ផ្ញើ',
'寄件備份',
'寄件備份',
'已发信息',
'送信済みメール',
'발신 메시지',
'보낸 편지함'
],
'\\Trash': [
'articole șterse',
'bin',
'borttagna objekt',
'deleted',
'deleted items',
'deleted messages',
'elementi eliminati',
'elementos borrados',
'elementos eliminados',
'gelöschte objekte',
'gelöschte elemente',
'item dipadam',
'itens apagados',
'itens excluídos',
'kustutatud üksused',
'mục đã xóa',
'odstraněné položky',
'odstraněná pošta',
'pesan terhapus',
'poistetut',
'praht',
'prügikast',
'silinmiş öğeler',
'slettede beskeder',
'slettede elementer',
'trash',
'törölt elemek',
'törölt',
'usunięte wiadomości',
'verwijderde items',
'vymazané správy',
'éléments supprimés',
'видалені',
'жойылғандар',
'удаленные',
'פריטים שנמחקו',
'العناصر المحذوفة',
'موارد حذف شده',
'รายการที่ลบ',
'已删除邮件',
'已刪除項目',
'已刪除項目'
],
'\\Junk': [
'bulk mail',
'correo no deseado',
'courrier indésirable',
'istenmeyen',
'istenmeyen e-posta',
'junk',
'junk e-mail',
'junk email',
'junk-e-mail',
'levélszemét',
'nevyžiadaná pošta',
'nevyžádaná pošta',
'no deseado',
'posta indesiderata',
'pourriel',
'roskaposti',
'rämpspost',
'skräppost',
'spam',
'spam',
'spamowanie',
'søppelpost',
'thư rác',
'wiadomości-śmieci',
'спам',
'דואר זבל',
'الرسائل العشوائية',
'هرزنامه',
'สแปม',
'垃圾郵件',
'垃圾邮件',
'垃圾電郵'
],
'\\Drafts': [
'ba brouillon',
'borrador',
'borrador',
'borradores',
'bozze',
'brouillons',
'bản thảo',
'ciorne',
'concepten',
'draf',
'draft',
'drafts',
'drög',
'entwürfe',
'esborranys',
'garalamalar',
'ihe edeturu',
'iidrafti',
'izinhlaka',
'juodraščiai',
'kladd',
'kladder',
'koncepty',
'koncepty',
'konsep',
'konsepte',
'kopie robocze',
'layihələr',
'luonnokset',
'melnraksti',
'meralo',
'mesazhe të padërguara',
'mga draft',
'mustandid',
'nacrti',
'nacrti',
'osnutki',
'piszkozatok',
'rascunhos',
'rasimu',
'skice',
'taslaklar',
'tsararrun saƙonni',
'utkast',
'vakiraoka',
'vázlatok',
'zirriborroak',
'àwọn àkọpamọ́',
'πρόχειρα',
'жобалар',
'нацрти',
'нооргууд',
'сиёҳнавис',
'хомаки хатлар',
'чарнавікі',
'чернетки',
'чернови',
'черновики',
'черновиктер',
'սևագրեր',
'טיוטות',
'مسودات',
'مسودات',
'موسودې',
'پیش نویسها',
'ڈرافٹ/',
'ड्राफ़्ट',
'प्रारूप',
'খসড়া',
'খসড়া',
'ড্ৰাফ্ট',
'ਡ੍ਰਾਫਟ',
'ડ્રાફ્ટસ',
'ଡ୍ରାଫ୍ଟ',
'வரைவுகள்',
'చిత్తు ప్రతులు',
'ಕರಡುಗಳು',
'കരടുകള്‍',
'කෙටුම් පත්',
'ฉบับร่าง',
'მონახაზები',
'ረቂቆች',
'សារព្រាង',
'下書き',
'草稿',
'草稿',
'草稿',
'임시 보관함'
],
'\\Archive': ['archive']
},
specialUse(hasSpecialUseExtension, folder) {
let result;
if (hasSpecialUseExtension) {
result = {
flag: module.exports.flags.find(flag => folder.flags.has(flag)),
source: 'extension'
};
}
if (!result || !result.flag) {
let name = folder.name
.toLowerCase()
.replace(/\u200e/g, '')
.trim();
result = {
flag: Object.keys(module.exports.names).find(flag => module.exports.names[flag].includes(name)),
source: 'name'
};
}
return result && result.flag ? result : { flag: null };
}
};

898
backend/node_modules/imapflow/lib/tools.js generated vendored Normal file
View File

@@ -0,0 +1,898 @@
/* eslint no-control-regex:0 */
'use strict';
const libmime = require('libmime');
const { resolveCharset } = require('./charsets');
const { compiler } = require('./handler/imap-handler');
const { createHash } = require('crypto');
const { JPDecoder } = require('./jp-decoder');
const iconv = require('iconv-lite');
const FLAG_COLORS = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'grey'];
class AuthenticationFailure extends Error {
authenticationFailed = true;
}
const tools = {
encodePath(connection, path) {
path = (path || '').toString();
if (!connection.enabled.has('UTF8=ACCEPT') && /[&\x00-\x08\x0b-\x0c\x0e-\x1f\u0080-\uffff]/.test(path)) {
try {
path = iconv.encode(path, 'utf-7-imap').toString();
} catch {
// ignore, keep name as is
}
}
return path;
},
decodePath(connection, path) {
path = (path || '').toString();
if (!connection.enabled.has('UTF8=ACCEPT') && /[&]/.test(path)) {
try {
path = iconv.decode(Buffer.from(path), 'utf-7-imap').toString();
} catch {
// ignore, keep name as is
}
}
return path;
},
normalizePath(connection, path, skipNamespace) {
if (Array.isArray(path)) {
path = path.join((connection.namespace && connection.namespace.delimiter) || '');
}
if (path.toUpperCase() === 'INBOX') {
// inbox is not case sensitive
return 'INBOX';
}
// ensure namespace prefix if needed
if (!skipNamespace && connection.namespace && connection.namespace.prefix && path.indexOf(connection.namespace.prefix) !== 0) {
path = connection.namespace.prefix + path;
}
return path;
},
comparePaths(connection, a, b) {
if (!a || !b) {
return false;
}
return tools.normalizePath(connection, a) === tools.normalizePath(connection, b);
},
updateCapabilities(list) {
let map = new Map();
if (list && Array.isArray(list)) {
list.forEach(val => {
if (typeof val.value !== 'string') {
return false;
}
let capability = val.value.toUpperCase().trim();
if (capability === 'IMAP4REV1') {
map.set('IMAP4rev1', true);
return;
}
if (capability.indexOf('APPENDLIMIT=') === 0) {
let splitPos = capability.indexOf('=');
let appendLimit = Number(capability.substr(splitPos + 1)) || 0;
map.set('APPENDLIMIT', appendLimit);
return;
}
map.set(capability, true);
});
}
return map;
},
AuthenticationFailure,
getStatusCode(response) {
return response &&
response.attributes &&
response.attributes[0] &&
response.attributes[0].section &&
response.attributes[0].section[0] &&
typeof response.attributes[0].section[0].value === 'string'
? response.attributes[0].section[0].value.toUpperCase().trim()
: false;
},
async getErrorText(response) {
if (!response) {
return false;
}
return (await compiler(response)).toString();
},
async enhanceCommandError(err) {
let errorCode = tools.getStatusCode(err.response);
if (errorCode) {
err.serverResponseCode = errorCode;
}
err.response = await tools.getErrorText(err.response);
return err;
},
getFolderTree(folders) {
let tree = {
root: true,
folders: []
};
let getTreeNode = parents => {
let node = tree;
if (!parents || !parents.length) {
return node;
}
for (let parent of parents) {
let cur = node.folders && node.folders.find(folder => folder.name === parent);
if (cur) {
node = cur;
} else {
// not yet set
cur = {
name: parent,
folders: []
};
}
}
return node;
};
for (let folder of folders) {
let parent = getTreeNode(folder.parent);
// see if entry already exists
let existing = parent.folders && parent.folders.find(existing => existing.name === folder.name);
if (existing) {
// update values
existing.name = folder.name;
existing.flags = folder.flags;
existing.path = folder.path;
existing.subscribed = !!folder.subscribed;
existing.listed = !!folder.listed;
existing.status = !!folder.status;
if (folder.specialUse) {
existing.specialUse = folder.specialUse;
}
if (folder.flags.has('\\Noselect')) {
existing.disabled = true;
}
if (folder.flags.has('\\HasChildren') && !existing.folders) {
existing.folders = [];
}
} else {
// create new
let data = {
name: folder.name,
flags: folder.flags,
path: folder.path,
subscribed: !!folder.subscribed,
listed: !!folder.listed,
status: !!folder.status
};
if (folder.delimiter) {
data.delimiter = folder.delimiter;
}
if (folder.specialUse) {
data.specialUse = folder.specialUse;
}
if (folder.flags.has('\\Noselect')) {
data.disabled = true;
}
if (folder.flags.has('\\HasChildren')) {
data.folders = [];
}
if (!parent.folders) {
parent.folders = [];
}
parent.folders.push(data);
}
}
return tree;
},
getFlagColor(flags) {
if (!flags.has('\\Flagged')) {
return null;
}
const bit0 = flags.has('$MailFlagBit0') ? 1 : 0;
const bit1 = flags.has('$MailFlagBit1') ? 2 : 0;
const bit2 = flags.has('$MailFlagBit2') ? 4 : 0;
const color = bit0 | bit1 | bit2; // eslint-disable-line no-bitwise
return FLAG_COLORS[color] || 'red'; // default to red for the unused \b111
},
getColorFlags(color) {
const colorCode = color ? FLAG_COLORS.indexOf((color || '').toString().toLowerCase().trim()) : null;
if (colorCode < 0 && colorCode !== null) {
return null;
}
const bits = [];
bits[0] = colorCode & 1; // eslint-disable-line no-bitwise
bits[1] = colorCode & 2; // eslint-disable-line no-bitwise
bits[2] = colorCode & 4; // eslint-disable-line no-bitwise
let result = { add: colorCode ? ['\\Flagged'] : [], remove: colorCode ? [] : ['\\Flagged'] };
for (let i = 0; i < bits.length; i++) {
if (bits[i]) {
result.add.push(`$MailFlagBit${i}`);
} else {
result.remove.push(`$MailFlagBit${i}`);
}
}
return result;
},
async formatMessageResponse(untagged, mailbox) {
let map = {};
map.seq = Number(untagged.command);
let key;
let attributes = (untagged.attributes && untagged.attributes[1]) || [];
for (let i = 0, len = attributes.length; i < len; i++) {
let attribute = attributes[i];
if (i % 2 === 0) {
key = (
await compiler({
attributes: [attribute]
})
)
.toString()
.toLowerCase()
.replace(/<\d+(\.\d+)?>$/, '');
continue;
}
if (typeof key !== 'string') {
// should not happen
continue;
}
let getString = attribute => {
if (!attribute) {
return false;
}
if (typeof attribute.value === 'string') {
return attribute.value;
}
if (Buffer.isBuffer(attribute.value)) {
return attribute.value.toString();
}
};
let getBuffer = attribute => {
if (!attribute) {
return false;
}
if (Buffer.isBuffer(attribute.value)) {
return attribute.value;
}
};
let getArray = attribute => {
if (Array.isArray(attribute)) {
return attribute.map(entry => (entry && typeof entry.value === 'string' ? entry.value : false)).filter(entry => entry);
}
};
switch (key) {
case 'body[]':
case 'binary[]':
map.source = getBuffer(attribute);
break;
case 'uid':
map.uid = Number(getString(attribute));
if (map.uid && (!mailbox.uidNext || mailbox.uidNext <= map.uid)) {
// current uidNext seems to be outdated, bump it
mailbox.uidNext = map.uid + 1;
}
break;
case 'modseq':
map.modseq = BigInt(getArray(attribute)[0]);
if (map.modseq && (!mailbox.highestModseq || mailbox.highestModseq < map.modseq)) {
// current highestModseq seems to be outdated, bump it
mailbox.highestModseq = map.modseq;
}
break;
case 'emailid':
map.emailId = getArray(attribute)[0];
break;
case 'x-gm-msgid':
map.emailId = getString(attribute);
break;
case 'threadid':
map.threadId = getArray(attribute)[0];
break;
case 'x-gm-thrid':
map.threadId = getString(attribute);
break;
case 'x-gm-labels':
map.labels = new Set(getArray(attribute));
break;
case 'rfc822.size':
map.size = Number(getString(attribute)) || 0;
break;
case 'flags':
map.flags = new Set(getArray(attribute));
break;
case 'envelope':
map.envelope = tools.parseEnvelope(attribute);
break;
case 'bodystructure':
map.bodyStructure = tools.parseBodystructure(attribute);
break;
case 'internaldate': {
let value = getString(attribute);
let date = new Date(value);
if (date.toString() === 'Invalid Date') {
map.internalDate = value;
} else {
map.internalDate = date;
}
break;
}
default: {
let match = key.match(/(body|binary)\[/i);
if (match) {
let partKey = key.replace(/^(body|binary)\[|]$/gi, '');
partKey = partKey.replace(/\.fields.*$/g, '');
let value = getBuffer(attribute);
if (partKey === 'header') {
map.headers = value;
break;
}
if (!map.bodyParts) {
map.bodyParts = new Map();
}
map.bodyParts.set(partKey, value);
break;
}
break;
}
}
}
if (map.emailId || map.uid) {
// define account unique ID for this email
// normalize path to use ascii, so we would always get the same ID
let path = mailbox.path;
if (/[0x80-0xff]/.test(path)) {
try {
path = iconv.encode(path, 'utf-7-imap').toString();
} catch {
// ignore
}
}
map.id =
map.emailId ||
createHash('md5')
.update([path, mailbox.uidValidity?.toString() || '', map.uid.toString()].join(':'))
.digest('hex');
}
if (map.flags) {
let flagColor = tools.getFlagColor(map.flags);
if (flagColor) {
map.flagColor = flagColor;
}
}
return map;
},
processName(name) {
name = (name || '').toString();
if (name.length > 2 && name.at(0) === '"' && name.at(-1) === '"') {
name = name.replace(/^"|"$/g, '');
}
return name;
},
parseEnvelope(entry) {
let getStrValue = obj => {
if (!obj) {
return false;
}
if (typeof obj.value === 'string') {
return obj.value;
}
if (Buffer.isBuffer(obj.value)) {
return obj.value.toString();
}
return obj.value;
};
let processAddresses = function (list) {
return []
.concat(list || [])
.map(addr => {
let address = (getStrValue(addr[2]) || '') + '@' + (getStrValue(addr[3]) || '');
if (address === '@') {
address = '';
}
return {
name: tools.processName(libmime.decodeWords(getStrValue(addr[0]))),
address
};
})
.filter(addr => addr.name || addr.address);
},
envelope = {};
if (entry[0] && entry[0].value) {
let date = new Date(getStrValue(entry[0]));
if (date.toString() === 'Invalid Date') {
envelope.date = getStrValue(entry[0]);
} else {
envelope.date = date;
}
}
if (entry[1] && entry[1].value) {
envelope.subject = libmime.decodeWords(getStrValue(entry[1]));
}
if (entry[2] && entry[2].length) {
envelope.from = processAddresses(entry[2]);
}
if (entry[3] && entry[3].length) {
envelope.sender = processAddresses(entry[3]);
}
if (entry[4] && entry[4].length) {
envelope.replyTo = processAddresses(entry[4]);
}
if (entry[5] && entry[5].length) {
envelope.to = processAddresses(entry[5]);
}
if (entry[6] && entry[6].length) {
envelope.cc = processAddresses(entry[6]);
}
if (entry[7] && entry[7].length) {
envelope.bcc = processAddresses(entry[7]);
}
if (entry[8] && entry[8].value) {
envelope.inReplyTo = (getStrValue(entry[8]) || '').toString().trim();
}
if (entry[9] && entry[9].value) {
envelope.messageId = (getStrValue(entry[9]) || '').toString().trim();
}
return envelope;
},
getStructuredParams(arr) {
let key;
let params = {};
[].concat(arr || []).forEach((val, j) => {
if (j % 2) {
params[key] = libmime.decodeWords(((val && val.value) || '').toString());
} else {
key = ((val && val.value) || '').toString().toLowerCase();
}
});
if (params.filename && !params['filename*'] && /^[a-z\-_0-9]+'[a-z]*'[^'\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]+/.test(params.filename)) {
// seems like encoded value
let [encoding, , encodedValue] = params.filename.split("'");
if (resolveCharset(encoding)) {
params['filename*'] = `${encoding}''${encodedValue}`;
}
}
// preprocess values
Object.keys(params).forEach(key => {
let actualKey;
let nr;
let value;
let match = key.match(/\*((\d+)\*?)?$/);
if (!match) {
// nothing to do here, does not seem like a continuation param
return;
}
actualKey = key.substr(0, match.index).toLowerCase();
nr = Number(match[2]) || 0;
if (!params[actualKey] || typeof params[actualKey] !== 'object') {
params[actualKey] = {
charset: false,
values: []
};
}
value = params[key];
if (nr === 0 && match[0].charAt(match[0].length - 1) === '*' && (match = value.match(/^([^']*)'[^']*'(.*)$/))) {
params[actualKey].charset = match[1] || 'utf-8';
value = match[2];
}
params[actualKey].values.push({ nr, value });
// remove the old reference
delete params[key];
});
// concatenate split rfc2231 strings and convert encoded strings to mime encoded words
Object.keys(params).forEach(key => {
let value;
if (params[key] && Array.isArray(params[key].values)) {
value = params[key].values
.sort((a, b) => a.nr - b.nr)
.map(val => (val && val.value) || '')
.join('');
if (params[key].charset) {
// convert "%AB" to "=?charset?Q?=AB?=" and then to unicode
params[key] = libmime.decodeWords(
'=?' +
params[key].charset +
'?Q?' +
value
// fix invalidly encoded chars
.replace(/[=?_\s]/g, s => {
let c = s.charCodeAt(0).toString(16);
if (s === ' ') {
return '_';
} else {
return '%' + (c.length < 2 ? '0' : '') + c;
}
})
// change from urlencoding to percent encoding
.replace(/%/g, '=') +
'?='
);
} else {
params[key] = libmime.decodeWords(value);
}
}
});
return params;
},
parseBodystructure(entry) {
let walk = (node, path) => {
path = path || [];
let curNode = {},
i = 0,
part = 0;
if (path.length) {
curNode.part = path.join('.');
}
// multipart
if (Array.isArray(node[0])) {
curNode.childNodes = [];
while (Array.isArray(node[i])) {
curNode.childNodes.push(walk(node[i], path.concat(++part)));
i++;
}
// multipart type
curNode.type = 'multipart/' + ((node[i++] || {}).value || '').toString().toLowerCase();
// extension data (not available for BODY requests)
// body parameter parenthesized list
if (i < node.length - 1) {
if (node[i]) {
curNode.parameters = tools.getStructuredParams(node[i]);
}
i++;
}
} else {
// content type
curNode.type = [((node[i++] || {}).value || '').toString().toLowerCase(), ((node[i++] || {}).value || '').toString().toLowerCase()].join('/');
// body parameter parenthesized list
if (node[i]) {
curNode.parameters = tools.getStructuredParams(node[i]);
}
i++;
// id
if (node[i]) {
curNode.id = ((node[i] || {}).value || '').toString();
}
i++;
// description
if (node[i]) {
curNode.description = ((node[i] || {}).value || '').toString();
}
i++;
// encoding
if (node[i]) {
curNode.encoding = ((node[i] || {}).value || '').toString().toLowerCase();
}
i++;
// size
if (node[i]) {
curNode.size = Number((node[i] || {}).value || 0) || 0;
}
i++;
if (curNode.type === 'message/rfc822') {
// message/rfc adds additional envelope, bodystructure and line count values
// envelope
if (node[i]) {
curNode.envelope = tools.parseEnvelope([].concat(node[i] || []));
}
i++;
if (node[i]) {
curNode.childNodes = [
// rfc822 bodyparts share the same path, difference is between MIME and HEADER
// path.MIME returns message/rfc822 header
// path.HEADER returns inlined message header
walk(node[i], path)
];
}
i++;
// line count
if (node[i]) {
curNode.lineCount = Number((node[i] || {}).value || 0) || 0;
}
i++;
}
if (/^text\//.test(curNode.type)) {
// text/* adds additional line count value
// NB! some less known servers do not include the line count value
// length should be 12+
if (node.length === 11 && Array.isArray(node[i + 1]) && !Array.isArray(node[i + 2])) {
// invalid structure, disposition params are shifted
} else {
// correct structure, line count number is provided
if (node[i]) {
// line count
curNode.lineCount = Number((node[i] || {}).value || 0) || 0;
}
i++;
}
}
// extension data (not available for BODY requests)
// md5
if (i < node.length - 1) {
if (node[i]) {
curNode.md5 = ((node[i] || {}).value || '').toString().toLowerCase();
}
i++;
}
}
// the following are shared extension values (for both multipart and non-multipart parts)
// not available for BODY requests
// body disposition
if (i < node.length - 1) {
if (Array.isArray(node[i]) && node[i].length) {
curNode.disposition = ((node[i][0] || {}).value || '').toString().toLowerCase();
if (Array.isArray(node[i][1])) {
curNode.dispositionParameters = tools.getStructuredParams(node[i][1]);
}
}
i++;
}
// body language
if (i < node.length - 1) {
if (node[i]) {
curNode.language = [].concat(node[i] || []).map(val => ((val && val.value) || '').toString().toLowerCase());
}
i++;
}
// body location
// NB! defined as a "string list" in RFC3501 but replaced in errata document with "string"
// Errata: http://www.rfc-editor.org/errata_search.php?rfc=3501
if (i < node.length - 1) {
if (node[i]) {
curNode.location = ((node[i] || {}).value || '').toString();
}
i++;
}
return curNode;
};
return walk(entry);
},
isDate(obj) {
return Object.prototype.toString.call(obj) === '[object Date]';
},
toValidDate(value) {
if (!value) {
return null;
}
if (typeof value === 'string') {
value = new Date(value);
}
if (!tools.isDate(value) || value.toString() === 'Invalid Date') {
return null;
}
return value;
},
formatDate(value) {
value = tools.toValidDate(value);
if (!value) {
return;
}
let dateParts = value.toISOString().substr(0, 10).split('-');
dateParts.reverse();
let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
dateParts[1] = months[Number(dateParts[1]) - 1];
return dateParts.join('-');
},
formatDateTime(value) {
value = tools.toValidDate(value);
if (!value) {
return;
}
let dateStr = tools.formatDate(value).replace(/^0/, ' '); //starts with date-day-fixed with leading 0 replaced by SP
let timeStr = value.toISOString().substr(11, 8);
return `${dateStr} ${timeStr} +0000`;
},
formatFlag(flag) {
switch (flag.toLowerCase()) {
case '\\recent':
// can not set or remove
return false;
case '\\seen':
case '\\answered':
case '\\flagged':
case '\\deleted':
case '\\draft':
// can not set or remove
return flag.toLowerCase().replace(/^\\./, c => c.toUpperCase());
}
return flag;
},
canUseFlag(mailbox, flag) {
return !mailbox || !mailbox.permanentFlags || mailbox.permanentFlags.has('\\*') || mailbox.permanentFlags.has(flag);
},
expandRange(range) {
return range.split(',').flatMap(entry => {
entry = entry.trim();
let colon = entry.indexOf(':');
if (colon < 0) {
return Number(entry) || 0;
}
let first = Number(entry.substr(0, colon)) || 0;
let second = Number(entry.substr(colon + 1)) || 0;
if (first === second) {
return first;
}
let list = [];
if (first < second) {
for (let i = first; i <= second; i++) {
list.push(i);
}
} else {
for (let i = first; i >= second; i--) {
list.push(i);
}
}
return list;
});
},
getDecoder(charset) {
charset = (charset || 'ascii').toString().trim().toLowerCase();
if (/^jis|^iso-?2022-?jp|^EUCJP/i.test(charset)) {
// special case not supported by iconv-lite
return new JPDecoder(charset);
}
return iconv.decodeStream(charset);
},
packMessageRange(list) {
if (!Array.isArray(list)) {
list = [].concat(list || []);
}
if (!list.length) {
return '';
}
list.sort((a, b) => a - b);
let last = list[list.length - 1];
let result = [[last]];
for (let i = list.length - 2; i >= 0; i--) {
if (list[i] === list[i + 1] - 1) {
result[0].unshift(list[i]);
continue;
}
result.unshift([list[i]]);
}
result = result.map(item => {
if (item.length === 1) {
return item[0];
}
return item.shift() + ':' + item.pop();
});
return result.join(',');
}
};
module.exports = tools;