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