Projektstart

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

View File

@@ -0,0 +1,101 @@
'use strict';
const { ImapFlow } = require('../lib/imap-flow');
const { AuthenticationFailure } = require('../lib/tools');
module.exports['Authentication: Password auth configuration'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: {
user: 'testuser',
pass: 'testpass'
}
});
test.equal(client.options.auth.user, 'testuser');
test.equal(client.options.auth.pass, 'testpass');
test.done();
};
module.exports['Authentication: OAuth2 auth configuration'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: {
user: 'testuser',
accessToken: 'oauth2_token_here'
}
});
test.equal(client.options.auth.user, 'testuser');
test.equal(client.options.auth.accessToken, 'oauth2_token_here');
test.done();
};
module.exports['Authentication: Login method specification'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: {
user: 'testuser',
pass: 'testpass',
loginMethod: 'AUTH=PLAIN'
}
});
test.equal(client.options.auth.loginMethod, 'AUTH=PLAIN');
test.done();
};
module.exports['Authentication: SASL PLAIN with authzid for impersonation'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: {
user: 'admin@example.com',
pass: 'adminpass',
authzid: 'user@example.com',
loginMethod: 'AUTH=PLAIN'
}
});
test.equal(client.options.auth.user, 'admin@example.com');
test.equal(client.options.auth.pass, 'adminpass');
test.equal(client.options.auth.authzid, 'user@example.com');
test.equal(client.options.auth.loginMethod, 'AUTH=PLAIN');
test.done();
};
module.exports['Authentication: AuthenticationFailure error structure'] = test => {
let error = new AuthenticationFailure('Invalid credentials');
test.ok(error instanceof Error);
test.equal(error.constructor.name, 'AuthenticationFailure');
test.equal(error.message, 'Invalid credentials');
test.done();
};
module.exports['Authentication: Verify-only mode'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: {
user: 'testuser',
pass: 'testpass'
},
verifyOnly: true
});
test.equal(client.options.verifyOnly, true);
test.done();
};
module.exports['Authentication: Disable auto IDLE'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: {
user: 'testuser',
pass: 'testpass'
},
disableAutoIdle: true
});
test.equal(client.options.disableAutoIdle, true);
test.done();
};

1130
backend/node_modules/imapflow/test/bodystructure-test.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

73
backend/node_modules/imapflow/test/commands-test.js generated vendored Normal file
View File

@@ -0,0 +1,73 @@
'use strict';
const { ImapFlow } = require('../lib/imap-flow');
module.exports['Commands: Client instantiation'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: { user: 'test', pass: 'test' }
});
test.ok(client);
test.equal(typeof client.exec, 'function');
test.done();
};
module.exports['Commands: Method availability'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: { user: 'test', pass: 'test' }
});
// Check that key IMAP methods exist
test.equal(typeof client.connect, 'function');
test.equal(typeof client.logout, 'function');
test.equal(typeof client.list, 'function');
test.equal(typeof client.mailboxOpen, 'function');
test.equal(typeof client.mailboxClose, 'function');
test.equal(typeof client.search, 'function');
test.equal(typeof client.fetch, 'function');
test.done();
};
module.exports['Commands: State management methods'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: { user: 'test', pass: 'test' }
});
test.equal(typeof client.mailboxCreate, 'function');
test.equal(typeof client.mailboxDelete, 'function');
test.equal(typeof client.mailboxRename, 'function');
test.equal(typeof client.mailboxSubscribe, 'function');
test.equal(typeof client.mailboxUnsubscribe, 'function');
test.done();
};
module.exports['Commands: Message operation methods'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: { user: 'test', pass: 'test' }
});
test.equal(typeof client.messageFlagsSet, 'function');
test.equal(typeof client.messageFlagsAdd, 'function');
test.equal(typeof client.messageFlagsRemove, 'function');
test.equal(typeof client.messageCopy, 'function');
test.equal(typeof client.messageMove, 'function');
test.equal(typeof client.messageDelete, 'function');
test.done();
};
module.exports['Commands: Utility methods'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: { user: 'test', pass: 'test' }
});
test.equal(typeof client.noop, 'function');
test.equal(typeof client.getQuota, 'function');
test.equal(typeof client.stats, 'function');
test.equal(typeof client.getRandomId, 'function');
test.done();
};

File diff suppressed because it is too large Load Diff

162
backend/node_modules/imapflow/test/connection-test.js generated vendored Normal file
View File

@@ -0,0 +1,162 @@
'use strict';
const { ImapFlow } = require('../lib/imap-flow');
module.exports['Connection: Basic connection options'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
port: 993,
auth: { user: 'test', pass: 'test' }
});
test.equal(client.host, 'imap.example.com');
test.equal(client.port, 993);
test.done();
};
module.exports['Connection: Default options'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: { user: 'test', pass: 'test' }
});
test.equal(client.port, 110);
test.equal(client.secureConnection, false);
test.done();
};
module.exports['Connection: Secure connection defaults'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
port: 993,
auth: { user: 'test', pass: 'test' }
});
test.equal(client.secureConnection, true);
test.done();
};
module.exports['Connection: TLS options'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: { user: 'test', pass: 'test' },
tls: {
rejectUnauthorized: false,
minVersion: 'TLSv1.3'
}
});
test.equal(client.options.tls.rejectUnauthorized, false);
test.equal(client.options.tls.minVersion, 'TLSv1.3');
test.done();
};
module.exports['Connection: Authentication options'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: {
user: 'testuser',
pass: 'testpass',
accessToken: 'token123'
}
});
test.equal(client.options.auth.user, 'testuser');
test.equal(client.options.auth.pass, 'testpass');
test.equal(client.options.auth.accessToken, 'token123');
test.done();
};
module.exports['Connection: Proxy configuration'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: { user: 'test', pass: 'test' },
proxy: 'socks5://proxy.example.com:1080'
});
test.equal(client.options.proxy, 'socks5://proxy.example.com:1080');
test.done();
};
module.exports['Connection: Client info'] = test => {
let clientInfo = {
name: 'Test Client',
version: '1.0.0',
vendor: 'Test Corp'
};
let client = new ImapFlow({
host: 'imap.example.com',
auth: { user: 'test', pass: 'test' },
clientInfo
});
test.equal(client.clientInfo.name, 'Test Client');
test.equal(client.clientInfo.version, '1.0.0');
test.equal(client.clientInfo.vendor, 'Test Corp');
test.done();
};
module.exports['Connection: Logger configuration'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: { user: 'test', pass: 'test' },
logger: false
});
test.equal(client.options.logger, false);
test.done();
};
module.exports['Connection: Stats tracking'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: { user: 'test', pass: 'test' }
});
let stats = client.stats();
test.ok(Object.prototype.hasOwnProperty.call(stats, 'sent'));
test.ok(Object.prototype.hasOwnProperty.call(stats, 'received'));
test.equal(typeof stats.sent, 'number');
test.equal(typeof stats.received, 'number');
test.done();
};
module.exports['Connection: Random ID generation'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: { user: 'test', pass: 'test' }
});
let id1 = client.getRandomId();
let id2 = client.getRandomId();
test.ok(typeof id1 === 'string' && id1.length > 0);
test.ok(typeof id2 === 'string' && id2.length > 0);
test.notEqual(id1, id2, 'IDs should be unique');
test.done();
};
module.exports['Connection: State management'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: { user: 'test', pass: 'test' }
});
test.equal(client.state, client.states.NOT_AUTHENTICATED);
test.equal(client.authenticated, false);
test.ok(client.capabilities instanceof Map);
test.done();
};
module.exports['Connection: Event emitter setup'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: { user: 'test', pass: 'test' }
});
test.equal(typeof client.on, 'function');
test.equal(typeof client.emit, 'function');
test.equal(typeof client.removeListener, 'function');
test.done();
};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,443 @@
/*eslint no-unused-expressions: 0, prefer-arrow-callback: 0 */
'use strict';
const { parser, compiler } = require('../lib/handler/imap-handler');
let asyncWrapper = (test, handler) => {
handler(test)
.then(() => test.done())
.catch(err => {
test.ifError(err);
test.done();
});
};
module.exports['IMAP Compiler: mixed'] = test =>
asyncWrapper(test, async test => {
const command =
'* FETCH (ENVELOPE ("Mon, 2 Sep 2013 05:30:13 -0700 (PDT)" NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "tr.ee")) NIL NIL NIL "<-4730417346358914070@unknownmsgid>") BODYSTRUCTURE (("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 105 (NIL NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "pangalink.net")) NIL NIL "<test1>" NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 12 0 NIL NIL NIL) 5 NIL NIL NIL)("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 83 (NIL NIL ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "kreata.ee")) ((NIL NIL "andris" "pangalink.net")) NIL NIL "NIL" NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 12 0 NIL NIL NIL) 4 NIL NIL NIL)("TEXT" "HTML" ("CHARSET" "utf-8") NIL NIL "QUOTED-PRINTABLE" 19 0 NIL NIL NIL) "MIXED" ("BOUNDARY" "----mailcomposer-?=_1-1328088797399") NIL NIL))';
const parsed = await parser(command, {
allowUntagged: true
});
const compiled = (await compiler(parsed)).toString();
test.equal(compiled, command);
});
module.exports['IMAP Compiler: no attributes'] = test =>
asyncWrapper(test, async test =>
test.equal(
(
await compiler({
tag: '*',
command: 'CMD'
})
).toString(),
'* CMD'
)
);
module.exports['IMAP Compiler: TEXT'] = test =>
asyncWrapper(test, async test =>
test.equal(
(
await compiler({
tag: '*',
command: 'CMD',
attributes: [
{
type: 'TEXT',
value: 'Tere tere!'
}
]
})
).toString(),
'* CMD Tere tere!'
)
);
module.exports['IMAP Compiler: SECTION'] = test =>
asyncWrapper(test, async test =>
test.equal(
(
await compiler({
tag: '*',
command: 'CMD',
attributes: [
{
type: 'SECTION',
section: [
{
type: 'ATOM',
value: 'ALERT'
}
]
}
]
})
).toString(),
'* CMD [ALERT]'
)
);
module.exports['IMAP Compiler: escaped ATOM'] = test =>
asyncWrapper(test, async test =>
test.equal(
(
await compiler({
tag: '*',
command: 'CMD',
attributes: [
{
type: 'ATOM',
value: 'ALERT'
},
{
type: 'ATOM',
value: '\\ALERT'
},
{
type: 'ATOM',
value: 'NO ALERT'
}
]
})
).toString(),
'* CMD ALERT \\ALERT "NO ALERT"'
)
);
module.exports['IMAP Compiler: SEQUENCE'] = test =>
asyncWrapper(test, async test =>
test.equal(
(
await compiler({
tag: '*',
command: 'CMD',
attributes: [
{
type: 'SEQUENCE',
value: '*:4,5,6'
}
]
})
).toString(),
'* CMD *:4,5,6'
)
);
module.exports['IMAP Compiler: NIL'] = test =>
asyncWrapper(test, async test =>
test.equal(
(
await compiler({
tag: '*',
command: 'CMD',
attributes: [null, null]
})
).toString(),
'* CMD NIL NIL'
)
);
module.exports['IMAP Compiler: quoted TEXT'] = test =>
asyncWrapper(test, async test =>
test.equal(
(
await compiler({
tag: '*',
command: 'CMD',
attributes: [
{
type: 'String',
value: 'Tere tere!',
sensitive: true
},
'Vana kere'
]
})
).toString(),
'* CMD "Tere tere!" "Vana kere"'
)
);
module.exports['IMAP Compiler: keep short strings'] = test =>
asyncWrapper(test, async test =>
test.equal(
(
await compiler(
{
tag: '*',
command: 'CMD',
attributes: [
{
type: 'String',
value: 'Tere tere!'
},
'Vana kere'
]
},
{ asArray: false, isLogging: true }
)
).toString(),
'* CMD "Tere tere!" "Vana kere"'
)
);
module.exports['IMAP Compiler: hide sensitive strings'] = test =>
asyncWrapper(test, async test =>
test.equal(
(
await compiler(
{
tag: '*',
command: 'CMD',
attributes: [
{
type: 'String',
value: 'Tere tere!',
sensitive: true
},
'Vana kere'
]
},
{ asArray: false, isLogging: true }
)
).toString(),
'* CMD "(* value hidden *)" "Vana kere"'
)
);
module.exports['IMAP Compiler: hide long strings'] = test =>
asyncWrapper(test, async test =>
test.equal(
(
await compiler(
{
tag: '*',
command: 'CMD',
attributes: [
{
type: 'String',
value: 'Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere! Tere tere!'
},
'Vana kere'
]
},
{ asArray: false, isLogging: true }
)
).toString(),
'* CMD "(* 219B string *)" "Vana kere"'
)
);
module.exports['IMAP Compiler: no command'] = test =>
asyncWrapper(test, async test =>
test.equal(
(
await compiler({
tag: '*',
attributes: [
1,
{
type: 'ATOM',
value: 'EXPUNGE'
}
]
})
).toString(),
'* 1 EXPUNGE'
)
);
module.exports['IMAP Compiler: LITERAL text'] = test =>
asyncWrapper(test, async test =>
test.equal(
(
await compiler({
tag: '*',
command: 'CMD',
attributes: [
// keep indentation
{
type: 'LITERAL',
value: 'Tere tere!'
},
'Vana kere'
]
})
).toString(),
'* CMD {10}\r\nTere tere! "Vana kere"'
)
);
module.exports['IMAP Compiler: LITERAL literal'] = test =>
asyncWrapper(test, async test =>
test.equal(
(
await compiler({
tag: '*',
command: 'CMD',
attributes: [
// keep indentation
{
type: 'LITERAL',
value: 'Tere\x00 tere!',
isLiteral8: false
},
'Vana kere'
]
})
).toString(),
'* CMD {11}\r\nTere\x00 tere! "Vana kere"'
)
);
module.exports['IMAP Compiler: LITERAL literal8'] = test =>
asyncWrapper(test, async test =>
test.equal(
(
await compiler({
tag: '*',
command: 'CMD',
attributes: [
// keep indentation
{
type: 'LITERAL',
value: 'Tere\x00 tere!',
isLiteral8: true
},
'Vana kere'
]
})
).toString(),
'* CMD ~{11}\r\nTere\x00 tere! "Vana kere"'
)
);
module.exports['IMAP Compiler: LITERAL array 1'] = test =>
asyncWrapper(test, async test =>
test.deepEqual(
(
await compiler(
{
tag: '*',
command: 'CMD',
attributes: [
{
type: 'LITERAL',
value: 'Tere tere!'
},
{
type: 'LITERAL',
value: 'Vana kere'
}
]
},
{ asArray: true }
)
).map(entry => entry.toString()),
['* CMD {10}\r\n', 'Tere tere! {9}\r\n', 'Vana kere']
)
);
module.exports['IMAP Compiler: LITERAL array 2'] = test =>
asyncWrapper(test, async test =>
test.deepEqual(
(
await compiler(
{
tag: '*',
command: 'CMD',
attributes: [
{
type: 'LITERAL',
value: 'Tere tere!'
},
{
type: 'LITERAL',
value: 'Vana kere'
},
'zzz'
]
},
{ asArray: true }
)
).map(entry => entry.toString()),
['* CMD {10}\r\n', 'Tere tere! {9}\r\n', 'Vana kere "zzz"']
)
);
module.exports['IMAP Compiler: LITERALPLUS array'] = test =>
asyncWrapper(test, async test =>
test.deepEqual(
(
await compiler(
{
tag: '*',
command: 'CMD',
attributes: [
{
type: 'LITERAL',
value: 'Tere tere!'
},
{
type: 'LITERAL',
value: 'Vana kere'
},
'zzz'
]
},
{ asArray: true, literalPlus: true }
)
).map(entry => entry.toString()),
['* CMD {10+}\r\nTere tere! {9+}\r\nVana kere "zzz"']
)
);
module.exports['IMAP Compiler: LITERAL array without tag/command'] = test =>
asyncWrapper(test, async test =>
test.deepEqual(
(
await compiler(
{
attributes: [
{
type: 'LITERAL',
value: 'Tere tere!'
},
{
type: 'LITERAL',
value: 'Vana kere'
}
]
},
{ asArray: true }
)
).map(entry => entry.toString()),
['{10}\r\n', 'Tere tere! {9}\r\n', 'Vana kere']
)
);
module.exports['IMAP Compiler: LITERAL byte length'] = test =>
asyncWrapper(test, async test =>
test.deepEqual(
(
await compiler(
{
tag: '*',
command: 'CMD',
attributes: [
{
type: 'LITERAL',
value: 'Tere tere!'
},
'Vana kere'
]
},
{ asArray: false, isLogging: true }
)
).toString(),
'* CMD "(* 10B literal *)" "Vana kere"'
)
);

1261
backend/node_modules/imapflow/test/imap-parser-test.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

131
backend/node_modules/imapflow/test/imap-stream-test.js generated vendored Normal file
View File

@@ -0,0 +1,131 @@
/*eslint no-unused-expressions: 0, prefer-arrow-callback: 0 */
'use strict';
const { ImapStream } = require('../lib/handler/imap-stream');
const { parser } = require('../lib/handler/imap-handler');
module.exports['Full input'] = test => {
let input = Buffer.from(
`A CAPABILITY
A LOGIN "aaa" "bbb"
A APPEND INBOX {5}
12345
A LOGIN {5}
12345 {11}
12345678901 "another"
A LOGOUT
`.replace(/\r?\n/g, '\r\n')
);
let expecting = [
{ command: 'A CAPABILITY', literals: [] },
{ command: 'A LOGIN "aaa" "bbb"', literals: [] },
{ command: 'A APPEND INBOX {5}\r\n', literals: ['12345'] },
{ command: 'A LOGIN {5}\r\n {11}\r\n "another"', literals: ['12345', '12345678901'] },
{ command: 'A LOGOUT', literals: [] }
];
let stream = new ImapStream();
let reading = false;
let reader = async () => {
let cmd;
while ((cmd = stream.read()) !== null) {
test.deepEqual({ command: cmd.payload.toString(), literals: cmd.literals.map(literal => literal.toString()) }, expecting.shift());
let parsed = await parser(cmd.payload, { literals: cmd.literals });
console.log(parsed);
cmd.next();
}
};
stream.on('readable', () => {
if (!reading) {
reading = true;
reader()
.catch(err => console.error(err))
.finally(() => {
reading = false;
});
}
});
stream.on('error', err => {
test.ifError(err);
});
stream.on('end', () => {
test.done();
});
let writer = async () => {
stream.end(input);
};
writer().catch(err => test.ifError(err));
};
module.exports['Single byte'] = test => {
let input = Buffer.from(
`A CAPABILITY
A LOGIN "aaa" "bbb"
A APPEND INBOX {5}
12345
A LOGIN {5}
12345 {11}
12345678901 "another"
A LOGOUT
`.replace(/\r?\n/g, '\r\n')
);
let expecting = [
{ command: 'A CAPABILITY', literals: [] },
{ command: 'A LOGIN "aaa" "bbb"', literals: [] },
{ command: 'A APPEND INBOX {5}\r\n', literals: ['12345'] },
{ command: 'A LOGIN {5}\r\n {11}\r\n "another"', literals: ['12345', '12345678901'] },
{ command: 'A LOGOUT', literals: [] }
];
let stream = new ImapStream();
let reading = false;
let reader = async () => {
let cmd;
while ((cmd = stream.read()) !== null) {
test.deepEqual({ command: cmd.payload.toString(), literals: cmd.literals.map(literal => literal.toString()) }, expecting.shift());
cmd.next();
}
};
stream.on('readable', () => {
if (!reading) {
reading = true;
reader()
.catch(err => console.error(err))
.finally(() => {
reading = false;
});
}
});
stream.on('error', err => {
test.ifError(err);
});
stream.on('end', () => {
test.done();
});
let writer = async () => {
for (let i = 0; i < input.length; i++) {
if (stream.write(Buffer.from([input[i]])) === false) {
await new Promise(resolve => stream.once('drain', resolve));
}
await new Promise(resolve => setTimeout(resolve, 10));
}
stream.end();
};
writer().catch(err => test.ifError(err));
};

44
backend/node_modules/imapflow/test/imapflow-test.js generated vendored Normal file
View File

@@ -0,0 +1,44 @@
'use strict';
const { ImapFlow } = require('../lib/imap-flow');
module.exports['Create imapflow instance'] = test => {
let imapFlow = new ImapFlow();
test.ok(imapFlow);
test.done();
};
module.exports['Create imapflow instance with custom logger'] = async test => {
class CustomLogger {
constructor() {}
debug(obj) {
console.log(JSON.stringify(obj));
}
info(obj) {
console.log(JSON.stringify(obj));
}
warn(obj) {
console.log(JSON.stringify(obj));
}
// eslint-disable-next-line no-unused-vars
error(obj) {
// we don't actually want to log anything here.
}
}
let imapFlow = new ImapFlow({
logger: new CustomLogger()
});
test.ok(imapFlow);
try {
await imapFlow.connect();
} catch (ex) {
// it is PERFECTLY okay to have an exception here. We expect an ECONNREFUSED if an exception occurs.
test.equal(ex.code, 'ECONNREFUSED');
}
test.done();
};

83
backend/node_modules/imapflow/test/integration-test.js generated vendored Normal file
View File

@@ -0,0 +1,83 @@
'use strict';
const { ImapFlow } = require('../lib/imap-flow');
module.exports['Integration: Basic client creation'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: { user: 'test', pass: 'test' }
});
test.ok(client);
test.equal(client.state, client.states.NOT_AUTHENTICATED);
test.equal(client.authenticated, false);
test.done();
};
module.exports['Integration: Multiple client instances'] = test => {
let client1 = new ImapFlow({
host: 'imap1.example.com',
auth: { user: 'test1', pass: 'test1' }
});
let client2 = new ImapFlow({
host: 'imap2.example.com',
auth: { user: 'test2', pass: 'test2' }
});
test.ok(client1);
test.ok(client2);
test.notEqual(client1.id, client2.id);
test.done();
};
module.exports['Integration: Client configuration'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
port: 993,
secure: true,
auth: { user: 'test', pass: 'test' },
logger: false
});
test.equal(client.host, 'imap.example.com');
test.equal(client.port, 993);
test.equal(client.secureConnection, true);
test.done();
};
module.exports['Integration: Event emitter functionality'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: { user: 'test', pass: 'test' }
});
let eventFired = false;
client.on('test-event', () => {
eventFired = true;
});
client.emit('test-event');
setTimeout(() => {
test.ok(eventFired);
test.done();
}, 10);
};
module.exports['Integration: Stats functionality'] = test => {
let client = new ImapFlow({
host: 'imap.example.com',
auth: { user: 'test', pass: 'test' }
});
let stats = client.stats();
test.ok(typeof stats === 'object');
test.ok(Object.prototype.hasOwnProperty.call(stats, 'sent'));
test.ok(Object.prototype.hasOwnProperty.call(stats, 'received'));
// Reset stats
let resetStats = client.stats(true);
test.ok(typeof resetStats === 'object');
test.done();
};

199
backend/node_modules/imapflow/test/jp-decoder-test.js generated vendored Normal file
View File

@@ -0,0 +1,199 @@
'use strict';
const { JPDecoder } = require('../lib/jp-decoder');
const { PassThrough, Transform } = require('stream');
// Helper to collect stream output
const collectStream = stream =>
new Promise((resolve, reject) => {
const chunks = [];
stream.on('data', chunk => chunks.push(chunk));
stream.on('end', () => resolve(Buffer.concat(chunks)));
stream.on('error', reject);
});
// ============================================
// Constructor tests
// ============================================
module.exports['JPDecoder: constructor sets charset'] = test => {
let decoder = new JPDecoder('iso-2022-jp');
test.equal(decoder.charset, 'iso-2022-jp');
test.deepEqual(decoder.chunks, []);
test.equal(decoder.chunklen, 0);
test.done();
};
module.exports['JPDecoder: is a Transform stream'] = test => {
let decoder = new JPDecoder('iso-2022-jp');
test.ok(decoder instanceof Transform);
test.ok(typeof decoder.pipe === 'function');
test.ok(typeof decoder.write === 'function');
test.done();
};
// ============================================
// _transform tests
// ============================================
module.exports['JPDecoder: _transform accumulates buffer chunks'] = test => {
let decoder = new JPDecoder('iso-2022-jp');
let chunk1 = Buffer.from('hello');
let chunk2 = Buffer.from('world');
decoder._transform(chunk1, 'buffer', () => {
test.equal(decoder.chunks.length, 1);
test.equal(decoder.chunklen, 5);
decoder._transform(chunk2, 'buffer', () => {
test.equal(decoder.chunks.length, 2);
test.equal(decoder.chunklen, 10);
test.done();
});
});
};
module.exports['JPDecoder: _transform converts string to buffer'] = test => {
let decoder = new JPDecoder('iso-2022-jp');
let stringChunk = 'hello';
decoder._transform(stringChunk, 'utf8', () => {
test.equal(decoder.chunks.length, 1);
test.ok(Buffer.isBuffer(decoder.chunks[0]));
test.equal(decoder.chunks[0].toString(), 'hello');
test.done();
});
};
// ============================================
// _flush tests
// ============================================
module.exports['JPDecoder: _flush outputs accumulated data'] = async test => {
let decoder = new JPDecoder('utf-8');
let output = collectStream(decoder);
decoder.write(Buffer.from('hello '));
decoder.write(Buffer.from('world'));
decoder.end();
let result = await output;
test.equal(result.toString(), 'hello world');
test.done();
};
module.exports['JPDecoder: _flush converts ISO-2022-JP to Unicode'] = async test => {
let decoder = new JPDecoder('iso-2022-jp');
let output = collectStream(decoder);
// ISO-2022-JP encoded Japanese text for "nihongo" (Japanese)
// ESC $ B sequence switches to JIS X 0208, ESC ( B switches back to ASCII
let iso2022jp = Buffer.from([
0x1b,
0x24,
0x42, // ESC $ B - switch to JIS X 0208
0x46,
0x7c, // ni
0x4b,
0x5c, // hon
0x38,
0x6c, // go
0x1b,
0x28,
0x42 // ESC ( B - switch back to ASCII
]);
decoder.write(iso2022jp);
decoder.end();
let result = await output;
test.ok(result.length > 0);
test.done();
};
module.exports['JPDecoder: _flush handles conversion errors gracefully'] = async test => {
// Use an invalid/unknown charset to trigger error path
let decoder = new JPDecoder('invalid-charset-xyz');
let output = collectStream(decoder);
let input = Buffer.from('test data');
decoder.write(input);
decoder.end();
let result = await output;
// On error, should return original input
test.equal(result.toString(), 'test data');
test.done();
};
module.exports['JPDecoder: _flush handles empty input'] = async test => {
let decoder = new JPDecoder('iso-2022-jp');
let output = collectStream(decoder);
decoder.end();
let result = await output;
test.equal(result.length, 0);
test.done();
};
// ============================================
// Integration tests
// ============================================
module.exports['JPDecoder: works with pipe'] = async test => {
let source = new PassThrough();
let decoder = new JPDecoder('utf-8');
let output = collectStream(source.pipe(decoder));
source.write('hello ');
source.write('world');
source.end();
let result = await output;
test.equal(result.toString(), 'hello world');
test.done();
};
module.exports['JPDecoder: handles multiple small chunks'] = async test => {
let decoder = new JPDecoder('utf-8');
let output = collectStream(decoder);
// Write character by character
'hello'.split('').forEach(char => decoder.write(char));
decoder.end();
let result = await output;
test.equal(result.toString(), 'hello');
test.done();
};
module.exports['JPDecoder: handles Shift_JIS charset'] = async test => {
let decoder = new JPDecoder('shift_jis');
let output = collectStream(decoder);
// Shift_JIS encoded "test" in katakana (tesuto)
let shiftJis = Buffer.from([0x83, 0x65, 0x83, 0x58, 0x83, 0x67]);
decoder.write(shiftJis);
decoder.end();
let result = await output;
test.ok(result.length > 0);
test.done();
};
module.exports['JPDecoder: handles EUC-JP charset'] = async test => {
let decoder = new JPDecoder('euc-jp');
let output = collectStream(decoder);
// EUC-JP encoded Japanese character
let eucJp = Buffer.from([0xc6, 0xfc, 0xcb, 0xdc]); // nihon
decoder.write(eucJp);
decoder.end();
let result = await output;
test.ok(result.length > 0);
test.done();
};

View File

@@ -0,0 +1,258 @@
'use strict';
const { LimitedPassthrough } = require('../lib/limited-passthrough');
const { PassThrough, Transform } = require('stream');
// Helper to collect stream output
const collectStream = stream =>
new Promise((resolve, reject) => {
const chunks = [];
stream.on('data', chunk => chunks.push(chunk));
stream.on('end', () => resolve(Buffer.concat(chunks)));
stream.on('error', reject);
});
// ============================================
// Constructor tests
// ============================================
module.exports['LimitedPassthrough: constructor with options'] = test => {
let stream = new LimitedPassthrough({ maxBytes: 100 });
test.equal(stream.maxBytes, 100);
test.equal(stream.processed, 0);
test.equal(stream.limited, false);
test.done();
};
module.exports['LimitedPassthrough: constructor with no options'] = test => {
let stream = new LimitedPassthrough();
test.equal(stream.maxBytes, Infinity);
test.equal(stream.processed, 0);
test.equal(stream.limited, false);
test.done();
};
module.exports['LimitedPassthrough: constructor with null options'] = test => {
let stream = new LimitedPassthrough(null);
test.equal(stream.maxBytes, Infinity);
test.done();
};
module.exports['LimitedPassthrough: is a Transform stream'] = test => {
let stream = new LimitedPassthrough();
test.ok(stream instanceof Transform);
test.ok(typeof stream.pipe === 'function');
test.ok(typeof stream.write === 'function');
test.done();
};
// ============================================
// _transform tests - no limit
// ============================================
module.exports['LimitedPassthrough: passes all data when no limit'] = async test => {
let stream = new LimitedPassthrough();
let output = collectStream(stream);
stream.write(Buffer.from('hello '));
stream.write(Buffer.from('world'));
stream.end();
let result = await output;
test.equal(result.toString(), 'hello world');
test.equal(stream.processed, 11);
test.equal(stream.limited, false);
test.done();
};
// ============================================
// _transform tests - with limit
// ============================================
module.exports['LimitedPassthrough: limits output to maxBytes'] = async test => {
let stream = new LimitedPassthrough({ maxBytes: 5 });
let output = collectStream(stream);
stream.write(Buffer.from('hello world'));
stream.end();
let result = await output;
test.equal(result.toString(), 'hello');
test.equal(stream.processed, 5);
test.equal(stream.limited, true);
test.done();
};
module.exports['LimitedPassthrough: limits across multiple chunks'] = async test => {
let stream = new LimitedPassthrough({ maxBytes: 8 });
let output = collectStream(stream);
stream.write(Buffer.from('hello ')); // 6 bytes
stream.write(Buffer.from('world')); // 5 bytes, only 2 should pass
stream.end();
let result = await output;
test.equal(result.toString(), 'hello wo');
test.equal(stream.processed, 8);
test.equal(stream.limited, true);
test.done();
};
module.exports['LimitedPassthrough: drops data after limit reached'] = async test => {
let stream = new LimitedPassthrough({ maxBytes: 5 });
let output = collectStream(stream);
stream.write(Buffer.from('hello')); // exactly 5 bytes
stream.write(Buffer.from(' world')); // should be dropped
stream.write(Buffer.from('!')); // should be dropped
stream.end();
let result = await output;
test.equal(result.toString(), 'hello');
test.equal(stream.limited, true);
test.done();
};
module.exports['LimitedPassthrough: handles exact boundary'] = async test => {
let stream = new LimitedPassthrough({ maxBytes: 5 });
let output = collectStream(stream);
stream.write(Buffer.from('hello')); // exactly 5 bytes
stream.end();
let result = await output;
test.equal(result.toString(), 'hello');
test.equal(stream.processed, 5);
test.equal(stream.limited, true);
test.done();
};
module.exports['LimitedPassthrough: zero maxBytes treated as Infinity'] = async test => {
// Note: maxBytes: 0 is falsy, so constructor uses Infinity instead
let stream = new LimitedPassthrough({ maxBytes: 0 });
test.equal(stream.maxBytes, Infinity);
let output = collectStream(stream);
stream.write(Buffer.from('hello'));
stream.end();
let result = await output;
// All data passes through since 0 is treated as Infinity
test.equal(result.toString(), 'hello');
test.done();
};
module.exports['LimitedPassthrough: handles limit of 1 byte'] = async test => {
let stream = new LimitedPassthrough({ maxBytes: 1 });
let output = collectStream(stream);
stream.write(Buffer.from('hello'));
stream.end();
let result = await output;
test.equal(result.toString(), 'h');
test.equal(stream.processed, 1);
test.equal(stream.limited, true);
test.done();
};
// ============================================
// Edge cases
// ============================================
module.exports['LimitedPassthrough: handles empty writes'] = async test => {
let stream = new LimitedPassthrough({ maxBytes: 10 });
let output = collectStream(stream);
stream.write(Buffer.from(''));
stream.write(Buffer.from('hello'));
stream.write(Buffer.from(''));
stream.end();
let result = await output;
test.equal(result.toString(), 'hello');
test.equal(stream.processed, 5);
test.done();
};
module.exports['LimitedPassthrough: handles no writes'] = async test => {
let stream = new LimitedPassthrough({ maxBytes: 10 });
let output = collectStream(stream);
stream.end();
let result = await output;
test.equal(result.length, 0);
test.equal(stream.processed, 0);
test.equal(stream.limited, false);
test.done();
};
module.exports['LimitedPassthrough: tracks processed bytes correctly'] = async test => {
let stream = new LimitedPassthrough({ maxBytes: 100 });
let output = collectStream(stream);
stream.write(Buffer.from('12345')); // 5 bytes
test.equal(stream.processed, 5);
stream.write(Buffer.from('67890')); // 5 more bytes
test.equal(stream.processed, 10);
stream.end();
await output;
test.equal(stream.processed, 10);
test.done();
};
// ============================================
// Integration tests
// ============================================
module.exports['LimitedPassthrough: works with pipe'] = async test => {
let source = new PassThrough();
let limiter = new LimitedPassthrough({ maxBytes: 10 });
let output = collectStream(source.pipe(limiter));
source.write('hello ');
source.write('wonderful ');
source.write('world');
source.end();
let result = await output;
test.equal(result.toString(), 'hello wond');
test.equal(limiter.limited, true);
test.done();
};
module.exports['LimitedPassthrough: handles large data'] = async test => {
let stream = new LimitedPassthrough({ maxBytes: 1000 });
let output = collectStream(stream);
// Write 100 bytes at a time
for (let i = 0; i < 20; i++) {
stream.write(Buffer.alloc(100, 'x'));
}
stream.end();
let result = await output;
test.equal(result.length, 1000);
test.equal(stream.limited, true);
test.done();
};
module.exports['LimitedPassthrough: single byte writes'] = async test => {
let stream = new LimitedPassthrough({ maxBytes: 3 });
let output = collectStream(stream);
stream.write(Buffer.from('a'));
stream.write(Buffer.from('b'));
stream.write(Buffer.from('c'));
stream.write(Buffer.from('d')); // should be dropped
stream.write(Buffer.from('e')); // should be dropped
stream.end();
let result = await output;
test.equal(result.toString(), 'abc');
test.done();
};

View File

@@ -0,0 +1,144 @@
'use strict';
/**
* Tests for memory cleanup on connection close
*/
const { ImapFlow } = require('../lib/imap-flow');
exports['Memory Cleanup Tests'] = {
'should clean up streamer on close without connection'(test) {
const client = new ImapFlow({
host: '127.0.0.1',
port: 1,
secure: false,
logger: false
});
// Check initial state
test.ok(client.streamer, 'streamer should exist');
test.ok(!client.streamer.destroyed, 'streamer should not be destroyed initially');
// Close without connecting
client.close();
// Verify cleanup
test.ok(client.streamer.destroyed, 'streamer should be destroyed after close');
test.equal(client.streamer.listenerCount('error'), 0, 'error listeners should be removed');
test.equal(client.folders.size, 0, 'folders should be cleared');
test.equal(client.requestTagMap.size, 0, 'requestTagMap should be cleared');
test.ok(client.isClosed, 'client should be marked as closed');
test.done();
},
'should remove event listeners on close'(test) {
const client = new ImapFlow({
host: '127.0.0.1',
port: 1,
secure: false,
logger: false
});
// Add a readable listener as if connect was called
client.socketReadable = () => {};
client.streamer.on('readable', client.socketReadable);
// Verify listener was added
test.equal(client.streamer.listenerCount('readable'), 1, 'readable listener should be present');
client.close();
// Check listeners after close
test.equal(client.streamer.listenerCount('readable'), 0, 'readable listener should be removed');
test.equal(client.streamer.listenerCount('error'), 0, 'error listeners should be removed');
test.done();
},
'should clear internal structures on close'(test) {
const client = new ImapFlow({
host: '127.0.0.1',
port: 1,
secure: false,
logger: false
});
// Add some data to internal structures
client.folders.set('INBOX', { path: 'INBOX' });
client.folders.set('Sent', { path: 'Sent' });
client.requestTagMap.set('A001', { tag: 'A001' });
client.requestTagMap.set('A002', { tag: 'A002' });
test.equal(client.folders.size, 2, 'folders should have entries');
test.equal(client.requestTagMap.size, 2, 'requestTagMap should have entries');
client.close();
test.equal(client.folders.size, 0, 'folders should be cleared after close');
test.equal(client.requestTagMap.size, 0, 'requestTagMap should be cleared after close');
test.done();
},
'should handle multiple close calls gracefully'(test) {
const client = new ImapFlow({
host: '127.0.0.1',
port: 1,
secure: false,
logger: false
});
// Call close multiple times
test.doesNotThrow(() => {
client.close();
client.close();
client.close();
}, 'multiple close calls should not throw');
test.ok(client.isClosed, 'client should be marked as closed');
test.done();
},
'should properly set state on close'(test) {
const client = new ImapFlow({
host: '127.0.0.1',
port: 1,
secure: false,
logger: false
});
test.equal(client.state, client.states.NOT_AUTHENTICATED, 'initial state should be NOT_AUTHENTICATED');
test.equal(client.usable, false, 'usable should be false initially');
test.equal(client.isClosed, false, 'isClosed should be false initially');
client.close();
test.equal(client.state, client.states.LOGOUT, 'state should be LOGOUT after close');
test.equal(client.usable, false, 'usable should be false after close');
test.equal(client.isClosed, true, 'isClosed should be true after close');
test.done();
},
'should emit close event'(test) {
const client = new ImapFlow({
host: '127.0.0.1',
port: 1,
secure: false,
logger: false
});
let closeEmitted = false;
client.on('close', () => {
closeEmitted = true;
});
client.close();
test.ok(closeEmitted, 'close event should be emitted');
test.done();
}
};

667
backend/node_modules/imapflow/test/memory-leak-test.js generated vendored Normal file
View File

@@ -0,0 +1,667 @@
'use strict';
/**
* Tests for memory leak detection in socket, TLS, and event handling
*/
const net = require('net');
const zlib = require('zlib');
const { PassThrough } = require('stream');
const { ImapFlow } = require('../lib/imap-flow');
// Helper to get listener counts for key objects
function getListenerReport(client) {
return {
streamer: client.streamer
? {
error: client.streamer.listenerCount('error'),
readable: client.streamer.listenerCount('readable')
}
: null,
socket: client.socket
? {
error: client.socket.listenerCount('error'),
close: client.socket.listenerCount('close'),
end: client.socket.listenerCount('end'),
timeout: client.socket.listenerCount('timeout'),
tlsClientError: client.socket.listenerCount('tlsClientError')
}
: null,
client: {
error: client.listenerCount('error'),
close: client.listenerCount('close')
},
inflate: client._inflate ? client._inflate.listenerCount('error') : 0,
deflate: client._deflate ? client._deflate.listenerCount('error') : 0
};
}
// Helper to measure memory (requires --expose-gc flag)
// Usage: node --expose-gc -e "require('./test/memory-leak-test.js')"
// eslint-disable-next-line no-unused-vars
async function measureMemory(label) {
if (global.gc) {
global.gc();
await new Promise(r => setTimeout(r, 50));
global.gc();
}
const mem = process.memoryUsage();
return {
label,
heapUsedMB: Math.round((mem.heapUsed / 1024 / 1024) * 100) / 100,
externalMB: Math.round((mem.external / 1024 / 1024) * 100) / 100
};
}
// Create a simple mock IMAP server for testing
function createMockServer() {
const server = net.createServer(socket => {
// Send greeting
socket.write('* OK Mock IMAP Server ready\r\n');
socket.on('data', data => {
const line = data.toString().trim();
const parts = line.split(' ');
const tag = parts[0];
const command = parts[1] ? parts[1].toUpperCase() : '';
if (command === 'CAPABILITY') {
socket.write('* CAPABILITY IMAP4rev1 AUTH=PLAIN\r\n');
socket.write(`${tag} OK CAPABILITY completed\r\n`);
} else if (command === 'LOGIN') {
socket.write(`${tag} OK LOGIN completed\r\n`);
} else if (command === 'LOGOUT') {
socket.write('* BYE Server logging out\r\n');
socket.write(`${tag} OK LOGOUT completed\r\n`);
socket.end();
} else if (command === 'NOOP') {
socket.write(`${tag} OK NOOP completed\r\n`);
} else if (command === 'NAMESPACE') {
socket.write('* NAMESPACE (("" "/")) NIL NIL\r\n');
socket.write(`${tag} OK NAMESPACE completed\r\n`);
} else if (command === 'COMPRESS') {
socket.write(`${tag} NO COMPRESS not supported\r\n`);
} else if (command === 'ENABLE') {
socket.write(`${tag} OK ENABLE completed\r\n`);
} else if (command === 'ID') {
socket.write('* ID NIL\r\n');
socket.write(`${tag} OK ID completed\r\n`);
} else if (tag && command) {
socket.write(`${tag} OK Command completed\r\n`);
}
});
socket.on('error', () => {
// Ignore errors
});
});
return server;
}
exports['Memory Leak Tests'] = {
'should not leak listeners after multiple client creations without connect'(test) {
const clients = [];
// Create multiple clients without connecting
for (let i = 0; i < 50; i++) {
const client = new ImapFlow({
host: '127.0.0.1',
port: 1,
secure: false,
logger: false
});
clients.push(client);
}
// Close all clients
for (const client of clients) {
client.close();
}
// Verify all clients are cleaned up
for (const client of clients) {
const report = getListenerReport(client);
test.equal(report.streamer.error, 0, 'streamer error listeners should be 0');
test.equal(report.streamer.readable, 0, 'streamer readable listeners should be 0');
test.ok(client.isClosed, 'client should be closed');
}
test.done();
},
'should clean up listeners after connect error'(test) {
test.expect(4);
const client = new ImapFlow({
host: '127.0.0.1',
port: 1, // Invalid port, will fail to connect
secure: false,
logger: false,
connectionTimeout: 500
});
client.connect().catch(() => {
// Wait a bit for cleanup
setTimeout(() => {
const report = getListenerReport(client);
test.equal(report.streamer.error, 0, 'streamer error listeners should be 0 after error');
test.equal(report.socket, null, 'socket should be null after error');
test.ok(client.isClosed, 'client should be closed after error');
test.equal(client.state, client.states.LOGOUT, 'state should be LOGOUT');
test.done();
}, 100);
});
},
async 'should clean up listeners after successful connect and close'(test) {
test.expect(6);
const server = createMockServer();
server.listen(0, '127.0.0.1', async () => {
const port = server.address().port;
const client = new ImapFlow({
host: '127.0.0.1',
port,
secure: false,
logger: false,
auth: {
user: 'test',
pass: 'test'
}
});
try {
await client.connect();
// Verify we're connected
test.ok(client.usable, 'client should be usable after connect');
// Get listener report while connected
const connectedReport = getListenerReport(client);
test.ok(connectedReport.socket !== null, 'socket should exist while connected');
await client.logout();
// Wait for cleanup
await new Promise(r => setTimeout(r, 100));
const closedReport = getListenerReport(client);
test.equal(closedReport.streamer.error, 0, 'streamer error listeners should be 0');
test.equal(closedReport.streamer.readable, 0, 'streamer readable listeners should be 0');
test.ok(client.isClosed, 'client should be closed');
test.equal(client.state, client.states.LOGOUT, 'state should be LOGOUT');
} catch (err) {
test.ok(false, 'should not throw: ' + err.message);
} finally {
server.close();
test.done();
}
});
},
async 'should not accumulate listeners over multiple connect/disconnect cycles'(test) {
const server = createMockServer();
server.listen(0, '127.0.0.1', async () => {
const port = server.address().port;
const cycles = 10;
for (let i = 0; i < cycles; i++) {
const client = new ImapFlow({
host: '127.0.0.1',
port,
secure: false,
logger: false,
auth: {
user: 'test',
pass: 'test'
}
});
try {
await client.connect();
await client.logout();
} catch {
// Ignore connection errors
}
// Wait for cleanup
await new Promise(r => setTimeout(r, 50));
const report = getListenerReport(client);
test.equal(report.streamer.error, 0, `cycle ${i + 1}: streamer error listeners should be 0`);
test.equal(report.streamer.readable, 0, `cycle ${i + 1}: streamer readable listeners should be 0`);
}
server.close();
test.done();
});
},
'should clean up socket handlers after close'(test) {
const client = new ImapFlow({
host: '127.0.0.1',
port: 1,
secure: false,
logger: false
});
// Simulate having socket handlers set
const mockSocket = new net.Socket();
client.socket = mockSocket;
client.writeSocket = mockSocket;
// Set up handlers like connect() would
client._socketError = () => {};
client._socketClose = () => {};
client._socketEnd = () => {};
client._socketTimeout = () => {};
mockSocket.on('error', client._socketError);
mockSocket.on('close', client._socketClose);
mockSocket.on('end', client._socketEnd);
mockSocket.on('timeout', client._socketTimeout);
// Verify listeners are set
test.equal(mockSocket.listenerCount('error'), 1, 'error listener should be set');
test.equal(mockSocket.listenerCount('close'), 1, 'close listener should be set');
// Close the client
client.close();
// Verify cleanup (socket is destroyed, so listeners are removed)
test.ok(client.isClosed, 'client should be closed');
test.done();
},
'should not leak memory on repeated streamer error handler registrations'(test) {
// This test verifies that the streamer error handler pattern doesn't leak
const clients = [];
for (let i = 0; i < 20; i++) {
const client = new ImapFlow({
host: '127.0.0.1',
port: 1,
secure: false,
logger: false
});
// Initially, streamer should have error listener from constructor
test.ok(client.streamer.listenerCount('error') >= 1, 'streamer should have error listener');
clients.push(client);
}
// Close all and verify
for (const client of clients) {
client.close();
test.equal(client.streamer.listenerCount('error'), 0, 'streamer error listeners should be removed');
}
test.done();
},
'should handle rapid create/destroy cycles'(test) {
const iterations = 100;
let completed = 0;
for (let i = 0; i < iterations; i++) {
const client = new ImapFlow({
host: '127.0.0.1',
port: 1,
secure: false,
logger: false
});
client.close();
completed++;
// Spot check listener cleanup
if (i % 10 === 0) {
test.equal(client.streamer.listenerCount('error'), 0, `iteration ${i}: error listeners should be 0`);
}
}
test.equal(completed, iterations, 'all iterations should complete');
test.done();
},
'should export helper functions for external memory testing'(test) {
// Verify helpers are working correctly
const client = new ImapFlow({
host: '127.0.0.1',
port: 1,
secure: false,
logger: false
});
const report = getListenerReport(client);
test.ok(report.streamer !== null, 'report should include streamer');
test.ok(typeof report.streamer.error === 'number', 'error count should be a number');
test.ok(typeof report.streamer.readable === 'number', 'readable count should be a number');
test.ok(report.client !== null, 'report should include client');
client.close();
test.done();
}
};
exports['Compression Stream Tests'] = {
'should clean up compression streams on close'(test) {
const client = new ImapFlow({
host: '127.0.0.1',
port: 1,
secure: false,
logger: false
});
// Simulate compression being enabled by manually setting up streams
client._deflate = zlib.createDeflateRaw();
client._inflate = zlib.createInflateRaw();
// Add error handlers like compress() does
client._deflate.on('error', () => {});
client._inflate.on('error', () => {});
// Create writeSocket like compress() does
const writeSocket = new PassThrough();
writeSocket.on('readable', () => {});
writeSocket.on('error', () => {});
client.writeSocket = writeSocket;
// Verify streams exist
test.ok(client._deflate, 'deflate should exist');
test.ok(client._inflate, 'inflate should exist');
test.ok(client.writeSocket, 'writeSocket should exist');
// Close the client
client.close();
// Verify compression streams are cleaned up
test.equal(client._deflate, null, 'deflate should be null after close');
test.equal(client._inflate, null, 'inflate should be null after close');
test.equal(client.writeSocket, null, 'writeSocket should be null after close');
test.ok(client.isClosed, 'client should be closed');
test.done();
},
'should not leak compression stream listeners over multiple cycles'(test) {
// Create multiple deflate/inflate streams to verify they don't accumulate
const streams = [];
for (let i = 0; i < 10; i++) {
const client = new ImapFlow({
host: '127.0.0.1',
port: 1,
secure: false,
logger: false
});
// Simulate compression setup
client._deflate = zlib.createDeflateRaw();
client._inflate = zlib.createInflateRaw();
client._deflate.on('error', () => {});
client._inflate.on('error', () => {});
streams.push({
deflate: client._deflate,
inflate: client._inflate
});
// Close immediately
client.close();
// Verify cleanup
test.equal(client._deflate, null, `cycle ${i + 1}: deflate should be null`);
test.equal(client._inflate, null, `cycle ${i + 1}: inflate should be null`);
}
// Verify all streams are destroyed
for (let i = 0; i < streams.length; i++) {
test.ok(streams[i].deflate.destroyed, `cycle ${i + 1}: deflate should be destroyed`);
test.ok(streams[i].inflate.destroyed, `cycle ${i + 1}: inflate should be destroyed`);
}
test.done();
},
'should handle compression stream errors without leaking'(test) {
const client = new ImapFlow({
host: '127.0.0.1',
port: 1,
secure: false,
logger: false
});
// Setup compression streams with error handlers
client._deflate = zlib.createDeflateRaw();
client._inflate = zlib.createInflateRaw();
// Add error handlers (like compress() does)
client._deflate.on('error', () => {});
client._inflate.on('error', () => {});
// Verify initial listener counts
test.equal(client._deflate.listenerCount('error'), 1, 'deflate should have 1 error listener');
test.equal(client._inflate.listenerCount('error'), 1, 'inflate should have 1 error listener');
// Close and verify cleanup
client.close();
test.equal(client._deflate, null, 'deflate should be null');
test.equal(client._inflate, null, 'inflate should be null');
test.done();
}
};
exports['Fetch Stream Tests'] = {
'should clean up internal fetch state on close'(test) {
const client = new ImapFlow({
host: '127.0.0.1',
port: 1,
secure: false,
logger: false
});
// Simulate some internal state that would exist during fetch
client.requestTagMap.set('A001', { tag: 'A001', command: 'FETCH' });
client.requestTagMap.set('A002', { tag: 'A002', command: 'FETCH' });
test.equal(client.requestTagMap.size, 2, 'should have 2 pending requests');
// Close the client
client.close();
// Verify request map is cleared
test.equal(client.requestTagMap.size, 0, 'requestTagMap should be cleared after close');
test.ok(client.isClosed, 'client should be closed');
test.done();
},
'should clean up mailbox state on close'(test) {
const client = new ImapFlow({
host: '127.0.0.1',
port: 1,
secure: false,
logger: false
});
// Simulate mailbox state
client.folders.set('INBOX', { path: 'INBOX', exists: 100 });
client.folders.set('Sent', { path: 'Sent', exists: 50 });
client.folders.set('Drafts', { path: 'Drafts', exists: 10 });
test.equal(client.folders.size, 3, 'should have 3 folders cached');
// Close the client
client.close();
// Verify folders are cleared
test.equal(client.folders.size, 0, 'folders should be cleared after close');
test.done();
},
async 'should clean up after fetch with mock server'(test) {
const server = createMockServerWithFetch();
server.listen(0, '127.0.0.1', async () => {
const port = server.address().port;
const client = new ImapFlow({
host: '127.0.0.1',
port,
secure: false,
logger: false,
auth: {
user: 'test',
pass: 'test'
}
});
try {
await client.connect();
// Select INBOX
await client.mailboxOpen('INBOX');
// Verify we're in selected state
test.ok(client.mailbox, 'mailbox should be selected');
await client.logout();
// Wait for cleanup
await new Promise(r => setTimeout(r, 100));
// Verify cleanup
const report = getListenerReport(client);
test.equal(report.streamer.error, 0, 'streamer error listeners should be 0');
test.equal(report.streamer.readable, 0, 'streamer readable listeners should be 0');
test.ok(client.isClosed, 'client should be closed');
} catch (err) {
test.ok(false, 'should not throw: ' + err.message);
} finally {
server.close();
test.done();
}
});
},
async 'should not leak listeners during multiple mailbox operations'(test) {
const server = createMockServerWithFetch();
server.listen(0, '127.0.0.1', async () => {
const port = server.address().port;
const client = new ImapFlow({
host: '127.0.0.1',
port,
secure: false,
logger: false,
auth: {
user: 'test',
pass: 'test'
}
});
try {
await client.connect();
// Perform multiple mailbox operations
for (let i = 0; i < 5; i++) {
await client.mailboxOpen('INBOX');
await client.mailboxClose();
}
await client.logout();
// Wait for cleanup
await new Promise(r => setTimeout(r, 100));
// Verify no listener accumulation
const report = getListenerReport(client);
test.equal(report.streamer.error, 0, 'streamer error listeners should be 0');
test.equal(report.streamer.readable, 0, 'streamer readable listeners should be 0');
} catch (err) {
test.ok(false, 'should not throw: ' + err.message);
} finally {
server.close();
test.done();
}
});
}
};
// Create a mock server that supports SELECT/FETCH operations
function createMockServerWithFetch() {
const server = net.createServer(socket => {
socket.write('* OK Mock IMAP Server ready\r\n');
socket.on('data', data => {
const lines = data
.toString()
.split('\r\n')
.filter(l => l.trim());
for (const line of lines) {
const parts = line.split(' ');
const tag = parts[0];
const command = parts[1] ? parts[1].toUpperCase() : '';
if (command === 'CAPABILITY') {
socket.write('* CAPABILITY IMAP4rev1 AUTH=PLAIN\r\n');
socket.write(`${tag} OK CAPABILITY completed\r\n`);
} else if (command === 'LOGIN') {
socket.write(`${tag} OK LOGIN completed\r\n`);
} else if (command === 'LOGOUT') {
socket.write('* BYE Server logging out\r\n');
socket.write(`${tag} OK LOGOUT completed\r\n`);
socket.end();
} else if (command === 'NAMESPACE') {
socket.write('* NAMESPACE (("" "/")) NIL NIL\r\n');
socket.write(`${tag} OK NAMESPACE completed\r\n`);
} else if (command === 'COMPRESS') {
socket.write(`${tag} NO COMPRESS not supported\r\n`);
} else if (command === 'ENABLE') {
socket.write(`${tag} OK ENABLE completed\r\n`);
} else if (command === 'ID') {
socket.write('* ID NIL\r\n');
socket.write(`${tag} OK ID completed\r\n`);
} else if (command === 'SELECT' || command === 'EXAMINE') {
socket.write('* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n');
socket.write('* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\*)] Flags permitted\r\n');
socket.write('* 10 EXISTS\r\n');
socket.write('* 0 RECENT\r\n');
socket.write('* OK [UIDVALIDITY 1234567890] UIDs valid\r\n');
socket.write('* OK [UIDNEXT 11] Predicted next UID\r\n');
socket.write(`${tag} OK [READ-WRITE] SELECT completed\r\n`);
} else if (command === 'CLOSE') {
socket.write(`${tag} OK CLOSE completed\r\n`);
} else if (command === 'FETCH') {
// Simple fetch response
socket.write('* 1 FETCH (UID 1 FLAGS (\\Seen))\r\n');
socket.write(`${tag} OK FETCH completed\r\n`);
} else if (command === 'NOOP') {
socket.write(`${tag} OK NOOP completed\r\n`);
} else if (tag && command) {
socket.write(`${tag} OK Command completed\r\n`);
}
}
});
socket.on('error', () => {});
});
return server;
}
// Note: helpers (getListenerReport, measureMemory, createMockServer) are available
// within this module for testing purposes

View File

@@ -0,0 +1,541 @@
'use strict';
const proxyquire = require('proxyquire').noCallThru();
// Mock socket object
const createMockSocket = () => ({
on: () => {},
write: () => {},
end: () => {},
destroy: () => {}
});
// Mock logger
const createMockLogger = () => {
const logs = { info: [], error: [] };
return {
info: msg => logs.info.push(msg),
error: msg => logs.error.push(msg),
_logs: logs
};
};
// ============================================
// HTTP Proxy Tests
// ============================================
module.exports['Proxy Connection: HTTP proxy success'] = async test => {
const mockSocket = createMockSocket();
const logger = createMockLogger();
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
cb(null, mockSocket);
},
socks: { SocksClient: {} },
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
net: { isIP: () => true }
});
const socket = await proxyConnection(logger, 'http://proxy.example.com:8080', '192.168.1.1', 993);
test.equal(socket, mockSocket);
test.equal(logger._logs.info.length, 1);
test.ok(logger._logs.info[0].msg.includes('HTTP proxy'));
test.done();
};
module.exports['Proxy Connection: HTTPS proxy success'] = async test => {
const mockSocket = createMockSocket();
const logger = createMockLogger();
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
cb(null, mockSocket);
},
socks: { SocksClient: {} },
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
net: { isIP: () => true }
});
const socket = await proxyConnection(logger, 'https://proxy.example.com:8080', '192.168.1.1', 993);
test.equal(socket, mockSocket);
test.equal(logger._logs.info.length, 1);
test.done();
};
module.exports['Proxy Connection: HTTP proxy with password hides it in logs'] = async test => {
const mockSocket = createMockSocket();
const logger = createMockLogger();
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
cb(null, mockSocket);
},
socks: { SocksClient: {} },
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
net: { isIP: () => true }
});
await proxyConnection(logger, 'http://user:secret123@proxy.example.com:8080', '192.168.1.1', 993);
test.equal(logger._logs.info.length, 1);
test.ok(!logger._logs.info[0].proxyUrl.includes('secret123'));
test.ok(logger._logs.info[0].proxyUrl.includes('(hidden)'));
test.done();
};
module.exports['Proxy Connection: HTTP proxy failure'] = async test => {
const logger = createMockLogger();
const testError = new Error('Connection refused');
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
cb(testError);
},
socks: { SocksClient: {} },
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
net: { isIP: () => true }
});
try {
await proxyConnection(logger, 'http://proxy.example.com:8080', '192.168.1.1', 993);
test.ok(false, 'Should have thrown');
} catch (err) {
test.equal(err, testError);
test.equal(logger._logs.error.length, 1);
test.ok(logger._logs.error[0].msg.includes('Failed'));
}
test.done();
};
module.exports['Proxy Connection: HTTP proxy failure hides password'] = async test => {
const logger = createMockLogger();
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
cb(new Error('Failed'));
},
socks: { SocksClient: {} },
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
net: { isIP: () => true }
});
try {
await proxyConnection(logger, 'http://user:secret@proxy.example.com:8080', '192.168.1.1', 993);
} catch (err) {
// Error expected - we just need to verify logging
err.expected = true;
test.ok(!logger._logs.error[0].proxyUrl.includes('secret'));
test.ok(logger._logs.error[0].proxyUrl.includes('(hidden)'));
}
test.done();
};
// ============================================
// SOCKS Proxy Tests
// ============================================
module.exports['Proxy Connection: SOCKS5 proxy success'] = async test => {
const mockSocket = createMockSocket();
const logger = createMockLogger();
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
socks: {
SocksClient: {
createConnection: async opts => {
test.equal(opts.proxy.type, 5);
test.equal(opts.command, 'connect');
return { socket: mockSocket };
}
}
},
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
net: { isIP: () => true }
});
const socket = await proxyConnection(logger, 'socks5://proxy.example.com:1080', '192.168.1.1', 993);
test.equal(socket, mockSocket);
test.equal(logger._logs.info.length, 1);
test.ok(logger._logs.info[0].msg.includes('SOCKS proxy'));
test.done();
};
module.exports['Proxy Connection: SOCKS proxy (defaults to SOCKS5)'] = async test => {
const mockSocket = createMockSocket();
const logger = createMockLogger();
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
socks: {
SocksClient: {
createConnection: async opts => {
test.equal(opts.proxy.type, 5); // Default to SOCKS5
return { socket: mockSocket };
}
}
},
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
net: { isIP: () => true }
});
await proxyConnection(logger, 'socks://proxy.example.com:1080', '192.168.1.1', 993);
test.done();
};
module.exports['Proxy Connection: SOCKS4 proxy'] = async test => {
const mockSocket = createMockSocket();
const logger = createMockLogger();
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
socks: {
SocksClient: {
createConnection: async opts => {
test.equal(opts.proxy.type, 4);
return { socket: mockSocket };
}
}
},
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
net: { isIP: () => true }
});
await proxyConnection(logger, 'socks4://proxy.example.com:1080', '192.168.1.1', 993);
test.done();
};
module.exports['Proxy Connection: SOCKS4a proxy'] = async test => {
const mockSocket = createMockSocket();
const logger = createMockLogger();
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
socks: {
SocksClient: {
createConnection: async opts => {
test.equal(opts.proxy.type, 4);
return { socket: mockSocket };
}
}
},
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
net: { isIP: () => true }
});
await proxyConnection(logger, 'socks4a://proxy.example.com:1080', '192.168.1.1', 993);
test.done();
};
module.exports['Proxy Connection: SOCKS proxy with authentication'] = async test => {
const mockSocket = createMockSocket();
const logger = createMockLogger();
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
socks: {
SocksClient: {
createConnection: async opts => {
test.equal(opts.proxy.userId, 'testuser');
test.equal(opts.proxy.password, 'testpass');
return { socket: mockSocket };
}
}
},
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
net: { isIP: () => true }
});
await proxyConnection(logger, 'socks5://testuser:testpass@proxy.example.com:1080', '192.168.1.1', 993);
test.done();
};
module.exports['Proxy Connection: SOCKS proxy hides password in logs'] = async test => {
const mockSocket = createMockSocket();
const logger = createMockLogger();
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
socks: {
SocksClient: {
createConnection: async () => ({ socket: mockSocket })
}
},
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
net: { isIP: () => true }
});
await proxyConnection(logger, 'socks5://user:secretpass@proxy.example.com:1080', '192.168.1.1', 993);
test.ok(!logger._logs.info[0].proxyUrl.includes('secretpass'));
test.ok(logger._logs.info[0].proxyUrl.includes('(hidden)'));
test.done();
};
module.exports['Proxy Connection: SOCKS proxy default port'] = async test => {
const mockSocket = createMockSocket();
const logger = createMockLogger();
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
socks: {
SocksClient: {
createConnection: async opts => {
test.equal(opts.proxy.port, 1080); // Default SOCKS port
return { socket: mockSocket };
}
}
},
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
net: { isIP: () => true }
});
await proxyConnection(logger, 'socks5://proxy.example.com', '192.168.1.1', 993);
test.done();
};
module.exports['Proxy Connection: SOCKS proxy failure'] = async test => {
const logger = createMockLogger();
const testError = new Error('SOCKS connection failed');
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
socks: {
SocksClient: {
createConnection: async () => {
throw testError;
}
}
},
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
net: { isIP: () => true }
});
try {
await proxyConnection(logger, 'socks5://proxy.example.com:1080', '192.168.1.1', 993);
test.ok(false, 'Should have thrown');
} catch (err) {
test.equal(err, testError);
test.equal(logger._logs.error.length, 1);
test.ok(logger._logs.error[0].msg.includes('Failed'));
}
test.done();
};
module.exports['Proxy Connection: SOCKS proxy failure hides password'] = async test => {
const logger = createMockLogger();
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
socks: {
SocksClient: {
createConnection: async () => {
throw new Error('Failed');
}
}
},
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
net: { isIP: () => true }
});
try {
await proxyConnection(logger, 'socks5://user:secret@proxy.example.com:1080', '192.168.1.1', 993);
} catch (err) {
// Error expected - we just need to verify logging
err.expected = true;
test.ok(!logger._logs.error[0].proxyUrl.includes('secret'));
test.ok(logger._logs.error[0].proxyUrl.includes('(hidden)'));
}
test.done();
};
// ============================================
// DNS Resolution Tests
// ============================================
module.exports['Proxy Connection: Resolves hostname to IP'] = async test => {
const mockSocket = createMockSocket();
const logger = createMockLogger();
let resolvedHost = null;
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
resolvedHost = host;
cb(null, mockSocket);
},
socks: { SocksClient: {} },
dns: {
promises: {
resolve: async hostname => {
if (hostname === 'mail.example.com') {
return ['93.184.216.34'];
}
return [];
}
}
},
net: { isIP: host => /^\d+\.\d+\.\d+\.\d+$/.test(host) }
});
await proxyConnection(logger, 'http://proxy.example.com:8080', 'mail.example.com', 993);
test.equal(resolvedHost, '93.184.216.34');
test.done();
};
module.exports['Proxy Connection: Skips DNS for IP addresses'] = async test => {
const mockSocket = createMockSocket();
const logger = createMockLogger();
let dnsResolveCalled = false;
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
cb(null, mockSocket);
},
socks: { SocksClient: {} },
dns: {
promises: {
resolve: async () => {
dnsResolveCalled = true;
return ['127.0.0.1'];
}
}
},
net: { isIP: () => true } // Pretend it's already an IP
});
await proxyConnection(logger, 'http://proxy.example.com:8080', '192.168.1.1', 993);
test.equal(dnsResolveCalled, false);
test.done();
};
module.exports['Proxy Connection: SOCKS resolves proxy hostname'] = async test => {
const mockSocket = createMockSocket();
const logger = createMockLogger();
let proxyHostResolved = null;
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
socks: {
SocksClient: {
createConnection: async opts => {
proxyHostResolved = opts.proxy.host;
return { socket: mockSocket };
}
}
},
dns: {
promises: {
resolve: async hostname => {
if (hostname === 'proxy.example.com') {
return ['10.0.0.1'];
}
return ['127.0.0.1'];
}
}
},
net: { isIP: host => /^\d+\.\d+\.\d+\.\d+$/.test(host) }
});
await proxyConnection(logger, 'socks5://proxy.example.com:1080', '192.168.1.1', 993);
test.equal(proxyHostResolved, '10.0.0.1');
test.done();
};
// ============================================
// Edge Cases
// ============================================
module.exports['Proxy Connection: Unknown protocol returns undefined'] = async test => {
const logger = createMockLogger();
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
socks: { SocksClient: {} },
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
net: { isIP: () => true }
});
const result = await proxyConnection(logger, 'ftp://proxy.example.com:21', '192.168.1.1', 993);
test.equal(result, undefined);
test.done();
};
module.exports['Proxy Connection: HTTP proxy with no socket returned'] = async test => {
const logger = createMockLogger();
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
cb(null, null); // No socket
},
socks: { SocksClient: {} },
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
net: { isIP: () => true }
});
const socket = await proxyConnection(logger, 'http://proxy.example.com:8080', '192.168.1.1', 993);
test.equal(socket, null);
// No log when socket is null
test.equal(logger._logs.info.length, 0);
test.done();
};
module.exports['Proxy Connection: DNS returns empty result'] = async test => {
const mockSocket = createMockSocket();
const logger = createMockLogger();
let usedHost = null;
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
usedHost = host;
cb(null, mockSocket);
},
socks: { SocksClient: {} },
dns: {
promises: {
resolve: async () => [] // Empty result
}
},
net: { isIP: () => false }
});
await proxyConnection(logger, 'http://proxy.example.com:8080', 'mail.example.com', 993);
// Should keep original hostname when DNS returns empty
test.equal(usedHost, 'mail.example.com');
test.done();
};
module.exports['Proxy Connection: SOCKS with username only'] = async test => {
const mockSocket = createMockSocket();
const logger = createMockLogger();
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
socks: {
SocksClient: {
createConnection: async opts => {
test.equal(opts.proxy.userId, 'testuser');
test.equal(opts.proxy.password, ''); // Empty string from URL parsing
return { socket: mockSocket };
}
}
},
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
net: { isIP: () => true }
});
await proxyConnection(logger, 'socks5://testuser@proxy.example.com:1080', '192.168.1.1', 993);
test.done();
};

View File

@@ -0,0 +1,863 @@
'use strict';
const { searchCompiler } = require('../lib/search-compiler');
// Mock mailbox for testing
let createMockMailbox = () => ({
flags: new Set(['\\Seen', '\\Answered', '\\Flagged', '\\Deleted', '\\Draft', '$CustomFlag']),
permanentFlags: new Set(['\\*'])
});
// Helper to create mock connection with customizable capabilities
let createMockConnection = (options = {}) => ({
capabilities: new Map(options.capabilities || [['IMAP4rev1', true]]),
enabled: new Set(options.enabled || []),
mailbox: options.mailbox || createMockMailbox()
});
// Helper to find attribute by value
let findAttr = (attrs, value) => attrs.find(a => a.value === value);
let hasAttr = (attrs, value) => attrs.some(a => a.value === value);
// ============================================
// Basic functionality tests
// ============================================
module.exports['Search Compiler: Basic functionality'] = test => {
let connection = createMockConnection();
test.doesNotThrow(() => {
let compiled = searchCompiler(connection, { seen: false });
test.ok(Array.isArray(compiled));
});
test.done();
};
module.exports['Search Compiler: Empty query'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, {});
test.ok(Array.isArray(compiled));
test.equal(compiled.length, 0);
test.done();
};
module.exports['Search Compiler: Null/undefined query'] = test => {
let connection = createMockConnection();
let compiled1 = searchCompiler(connection, null);
test.ok(Array.isArray(compiled1));
let compiled2 = searchCompiler(connection, undefined);
test.ok(Array.isArray(compiled2));
test.done();
};
// ============================================
// SEQ (sequence) tests
// ============================================
module.exports['Search Compiler: SEQ with string'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { seq: '1:100' });
test.ok(hasAttr(compiled, '1:100'));
let seqAttr = findAttr(compiled, '1:100');
test.equal(seqAttr.type, 'SEQUENCE');
test.done();
};
module.exports['Search Compiler: SEQ with number'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { seq: 42 });
test.ok(hasAttr(compiled, '42'));
test.done();
};
module.exports['Search Compiler: SEQ ignores invalid values'] = test => {
let connection = createMockConnection();
// Whitespace in sequence is invalid
let compiled = searchCompiler(connection, { seq: '1 2 3' });
test.equal(compiled.length, 0);
test.done();
};
// ============================================
// Boolean flag tests
// ============================================
module.exports['Search Compiler: SEEN flag true'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { seen: true });
test.ok(hasAttr(compiled, 'SEEN'));
test.done();
};
module.exports['Search Compiler: SEEN flag false adds UNSEEN'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { seen: false });
test.ok(hasAttr(compiled, 'UNSEEN'));
test.done();
};
module.exports['Search Compiler: UNSEEN flag false adds SEEN'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { unseen: false });
test.ok(hasAttr(compiled, 'SEEN'));
test.done();
};
module.exports['Search Compiler: All boolean flags'] = test => {
let connection = createMockConnection();
// Test all toggleable flags
let flags = ['answered', 'deleted', 'draft', 'flagged', 'seen'];
flags.forEach(flag => {
let compiled = searchCompiler(connection, { [flag]: true });
test.ok(hasAttr(compiled, flag.toUpperCase()), `${flag} should be present`);
});
test.done();
};
module.exports['Search Compiler: UN-prefixed flags'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, {
unanswered: true,
undeleted: true,
undraft: true,
unflagged: true
});
test.ok(hasAttr(compiled, 'UNANSWERED'));
test.ok(hasAttr(compiled, 'UNDELETED'));
test.ok(hasAttr(compiled, 'UNDRAFT'));
test.ok(hasAttr(compiled, 'UNFLAGGED'));
test.done();
};
// ============================================
// Simple boolean flags (ALL, NEW, OLD, RECENT)
// ============================================
module.exports['Search Compiler: ALL flag'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { all: true });
test.ok(hasAttr(compiled, 'ALL'));
test.done();
};
module.exports['Search Compiler: NEW flag'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { new: true });
test.ok(hasAttr(compiled, 'NEW'));
test.done();
};
module.exports['Search Compiler: OLD flag'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { old: true });
test.ok(hasAttr(compiled, 'OLD'));
test.done();
};
module.exports['Search Compiler: RECENT flag'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { recent: true });
test.ok(hasAttr(compiled, 'RECENT'));
test.done();
};
module.exports['Search Compiler: Simple flags ignored when falsy'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, {
all: false,
new: false,
old: false,
recent: false
});
test.equal(compiled.length, 0);
test.done();
};
// ============================================
// Numeric comparison tests
// ============================================
module.exports['Search Compiler: LARGER'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { larger: 10000 });
test.ok(hasAttr(compiled, 'LARGER'));
test.ok(hasAttr(compiled, '10000'));
test.done();
};
module.exports['Search Compiler: SMALLER'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { smaller: 5000 });
test.ok(hasAttr(compiled, 'SMALLER'));
test.ok(hasAttr(compiled, '5000'));
test.done();
};
module.exports['Search Compiler: MODSEQ'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { modseq: 123456 });
test.ok(hasAttr(compiled, 'MODSEQ'));
test.ok(hasAttr(compiled, '123456'));
test.done();
};
module.exports['Search Compiler: Numeric ignores falsy values'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, {
larger: 0,
smaller: null,
modseq: undefined
});
test.equal(compiled.length, 0);
test.done();
};
// ============================================
// Text search tests
// ============================================
module.exports['Search Compiler: FROM'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { from: 'user@example.com' });
test.ok(hasAttr(compiled, 'FROM'));
test.ok(hasAttr(compiled, 'user@example.com'));
test.done();
};
module.exports['Search Compiler: TO'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { to: 'recipient@example.com' });
test.ok(hasAttr(compiled, 'TO'));
test.ok(hasAttr(compiled, 'recipient@example.com'));
test.done();
};
module.exports['Search Compiler: CC'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { cc: 'cc@example.com' });
test.ok(hasAttr(compiled, 'CC'));
test.done();
};
module.exports['Search Compiler: BCC'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { bcc: 'bcc@example.com' });
test.ok(hasAttr(compiled, 'BCC'));
test.done();
};
module.exports['Search Compiler: SUBJECT'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { subject: 'Test Subject' });
test.ok(hasAttr(compiled, 'SUBJECT'));
test.ok(hasAttr(compiled, 'Test Subject'));
test.done();
};
module.exports['Search Compiler: BODY'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { body: 'search text' });
test.ok(hasAttr(compiled, 'BODY'));
test.ok(hasAttr(compiled, 'search text'));
test.done();
};
module.exports['Search Compiler: TEXT'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { text: 'full text search' });
test.ok(hasAttr(compiled, 'TEXT'));
test.ok(hasAttr(compiled, 'full text search'));
test.done();
};
module.exports['Search Compiler: Text fields ignore falsy'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, {
from: '',
to: null,
subject: undefined
});
test.equal(compiled.length, 0);
test.done();
};
// ============================================
// UID tests
// ============================================
module.exports['Search Compiler: UID with string'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { uid: '1:*' });
test.ok(hasAttr(compiled, 'UID'));
let uidValueAttr = compiled.find(a => a.value === '1:*');
test.equal(uidValueAttr.type, 'SEQUENCE');
test.done();
};
module.exports['Search Compiler: UID with number'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { uid: 12345 });
test.ok(hasAttr(compiled, 'UID'));
test.ok(hasAttr(compiled, '12345'));
test.done();
};
// ============================================
// EMAILID / THREADID tests
// ============================================
module.exports['Search Compiler: EMAILID with OBJECTID'] = test => {
let connection = createMockConnection({
capabilities: [['OBJECTID', true]]
});
let compiled = searchCompiler(connection, { emailId: 'M1234567890' });
test.ok(hasAttr(compiled, 'EMAILID'));
test.ok(hasAttr(compiled, 'M1234567890'));
test.done();
};
module.exports['Search Compiler: EMAILID falls back to X-GM-MSGID'] = test => {
let connection = createMockConnection({
capabilities: [['X-GM-EXT-1', true]]
});
let compiled = searchCompiler(connection, { emailId: '1234567890' });
test.ok(hasAttr(compiled, 'X-GM-MSGID'));
test.ok(hasAttr(compiled, '1234567890'));
test.done();
};
module.exports['Search Compiler: EMAILID ignored without capability'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { emailId: '12345' });
test.equal(compiled.length, 0);
test.done();
};
module.exports['Search Compiler: THREADID with OBJECTID'] = test => {
let connection = createMockConnection({
capabilities: [['OBJECTID', true]]
});
let compiled = searchCompiler(connection, { threadId: 'T1234567890' });
test.ok(hasAttr(compiled, 'THREADID'));
test.ok(hasAttr(compiled, 'T1234567890'));
test.done();
};
module.exports['Search Compiler: THREADID falls back to X-GM-THRID'] = test => {
let connection = createMockConnection({
capabilities: [['X-GM-EXT-1', true]]
});
let compiled = searchCompiler(connection, { threadId: '9876543210' });
test.ok(hasAttr(compiled, 'X-GM-THRID'));
test.ok(hasAttr(compiled, '9876543210'));
test.done();
};
// ============================================
// Gmail raw search tests
// ============================================
module.exports['Search Compiler: GMRAW with X-GM-EXT-1'] = test => {
let connection = createMockConnection({
capabilities: [['X-GM-EXT-1', true]]
});
let compiled = searchCompiler(connection, { gmraw: 'in:inbox is:unread' });
test.ok(hasAttr(compiled, 'X-GM-RAW'));
test.ok(hasAttr(compiled, 'in:inbox is:unread'));
test.done();
};
module.exports['Search Compiler: GMAILRAW alias'] = test => {
let connection = createMockConnection({
capabilities: [['X-GM-EXT-1', true]]
});
let compiled = searchCompiler(connection, { gmailraw: 'has:attachment' });
test.ok(hasAttr(compiled, 'X-GM-RAW'));
test.ok(hasAttr(compiled, 'has:attachment'));
test.done();
};
module.exports['Search Compiler: GMRAW throws without capability'] = test => {
let connection = createMockConnection();
try {
searchCompiler(connection, { gmraw: 'test' });
test.ok(false, 'Should have thrown');
} catch (err) {
test.equal(err.code, 'MissingServerExtension');
test.ok(err.message.includes('X-GM-EXT-1'));
}
test.done();
};
// ============================================
// Date search tests
// ============================================
module.exports['Search Compiler: SINCE'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { since: new Date('2023-06-15') });
test.ok(hasAttr(compiled, 'SINCE'));
test.ok(hasAttr(compiled, '15-Jun-2023'));
test.done();
};
module.exports['Search Compiler: BEFORE'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { before: new Date('2023-06-15T00:00:00.000Z') });
test.ok(hasAttr(compiled, 'BEFORE'));
test.done();
};
module.exports['Search Compiler: BEFORE with non-midnight time adjusts date'] = test => {
let connection = createMockConnection();
// Non-midnight time should advance to next day
let compiled = searchCompiler(connection, { before: new Date('2023-06-15T12:30:00.000Z') });
test.ok(hasAttr(compiled, 'BEFORE'));
// Should be 16-Jun-2023 (next day)
test.ok(hasAttr(compiled, '16-Jun-2023'));
test.done();
};
module.exports['Search Compiler: ON'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { on: new Date('2023-06-15') });
test.ok(hasAttr(compiled, 'ON'));
test.done();
};
module.exports['Search Compiler: SENTBEFORE'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { sentbefore: new Date('2023-06-15T00:00:00.000Z') });
test.ok(hasAttr(compiled, 'SENTBEFORE'));
test.done();
};
module.exports['Search Compiler: SENTON'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { senton: new Date('2023-06-15') });
test.ok(hasAttr(compiled, 'SENTON'));
test.done();
};
module.exports['Search Compiler: SENTSINCE'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { sentsince: new Date('2023-06-15') });
test.ok(hasAttr(compiled, 'SENTSINCE'));
test.done();
};
module.exports['Search Compiler: SINCE with WITHIN extension'] = test => {
let connection = createMockConnection({
capabilities: [['WITHIN', true]]
});
let recentDate = new Date(Date.now() - 3600 * 1000); // 1 hour ago
let compiled = searchCompiler(connection, { since: recentDate });
test.ok(hasAttr(compiled, 'YOUNGER'));
test.done();
};
module.exports['Search Compiler: BEFORE with WITHIN extension'] = test => {
let connection = createMockConnection({
capabilities: [['WITHIN', true]]
});
let oldDate = new Date(Date.now() - 86400 * 1000); // 1 day ago
let compiled = searchCompiler(connection, { before: oldDate });
test.ok(hasAttr(compiled, 'OLDER'));
test.done();
};
module.exports['Search Compiler: Date with invalid value ignored'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { since: 'invalid-date' });
// formatDate returns undefined for invalid dates
test.equal(compiled.length, 0);
test.done();
};
// ============================================
// KEYWORD tests
// ============================================
module.exports['Search Compiler: KEYWORD'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { keyword: '$CustomFlag' });
test.ok(hasAttr(compiled, 'KEYWORD'));
test.ok(hasAttr(compiled, '$CustomFlag'));
test.done();
};
module.exports['Search Compiler: UNKEYWORD'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { unkeyword: '$CustomFlag' });
test.ok(hasAttr(compiled, 'UNKEYWORD'));
test.done();
};
module.exports['Search Compiler: KEYWORD with standard flag'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, { keyword: '\\Seen' });
test.ok(hasAttr(compiled, 'KEYWORD'));
test.ok(hasAttr(compiled, '\\Seen'));
test.done();
};
// ============================================
// HEADER tests
// ============================================
module.exports['Search Compiler: HEADER with value'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, {
header: {
'X-Custom-Header': 'custom-value'
}
});
test.ok(hasAttr(compiled, 'HEADER'));
test.ok(hasAttr(compiled, 'X-CUSTOM-HEADER'));
test.ok(hasAttr(compiled, 'custom-value'));
test.done();
};
module.exports['Search Compiler: HEADER existence check'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, {
header: {
'X-Priority': true // Check header exists
}
});
test.ok(hasAttr(compiled, 'HEADER'));
test.ok(hasAttr(compiled, 'X-PRIORITY'));
test.ok(hasAttr(compiled, '')); // Empty string for existence check
test.done();
};
module.exports['Search Compiler: HEADER multiple headers'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, {
header: {
'X-Mailer': 'Outlook',
'X-Priority': '1'
}
});
test.ok(hasAttr(compiled, 'X-MAILER'));
test.ok(hasAttr(compiled, 'X-PRIORITY'));
test.done();
};
module.exports['Search Compiler: HEADER ignores non-string values'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, {
header: {
'X-Number': 123,
'X-Null': null
}
});
// Non-string values (except true) should be skipped
test.ok(!hasAttr(compiled, 'X-NUMBER'));
test.ok(!hasAttr(compiled, 'X-NULL'));
test.done();
};
module.exports['Search Compiler: HEADER with null/invalid object'] = test => {
let connection = createMockConnection();
let compiled1 = searchCompiler(connection, { header: null });
test.equal(compiled1.length, 0);
let compiled2 = searchCompiler(connection, { header: 'not-an-object' });
test.equal(compiled2.length, 0);
test.done();
};
// ============================================
// NOT operator tests
// ============================================
module.exports['Search Compiler: NOT operator'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, {
not: { from: 'spam@example.com' }
});
test.ok(hasAttr(compiled, 'NOT'));
test.ok(hasAttr(compiled, 'FROM'));
test.ok(hasAttr(compiled, 'spam@example.com'));
test.done();
};
module.exports['Search Compiler: NOT with nested conditions'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, {
not: {
seen: true,
from: 'test@example.com'
}
});
test.ok(hasAttr(compiled, 'NOT'));
test.ok(hasAttr(compiled, 'SEEN'));
test.ok(hasAttr(compiled, 'FROM'));
test.done();
};
module.exports['Search Compiler: NOT ignored when falsy'] = test => {
let connection = createMockConnection();
let compiled1 = searchCompiler(connection, { not: null });
test.equal(compiled1.length, 0);
let compiled2 = searchCompiler(connection, { not: false });
test.equal(compiled2.length, 0);
test.done();
};
// ============================================
// OR operator tests
// ============================================
module.exports['Search Compiler: OR with two conditions'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, {
or: [{ from: 'alice@example.com' }, { from: 'bob@example.com' }]
});
test.ok(hasAttr(compiled, 'OR'));
test.ok(hasAttr(compiled, 'FROM'));
test.ok(hasAttr(compiled, 'alice@example.com'));
test.ok(hasAttr(compiled, 'bob@example.com'));
test.done();
};
module.exports['Search Compiler: OR with single condition'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, {
or: [{ from: 'only@example.com' }]
});
// Single condition should not add OR
test.ok(!hasAttr(compiled, 'OR'));
test.ok(hasAttr(compiled, 'FROM'));
test.ok(hasAttr(compiled, 'only@example.com'));
test.done();
};
module.exports['Search Compiler: OR with three conditions'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, {
or: [{ from: 'a@example.com' }, { from: 'b@example.com' }, { from: 'c@example.com' }]
});
// Should have OR for tree structure
test.ok(hasAttr(compiled, 'OR'));
test.ok(hasAttr(compiled, 'a@example.com'));
test.ok(hasAttr(compiled, 'b@example.com'));
test.ok(hasAttr(compiled, 'c@example.com'));
test.done();
};
module.exports['Search Compiler: OR with four conditions'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, {
or: [{ from: 'a@example.com' }, { from: 'b@example.com' }, { from: 'c@example.com' }, { from: 'd@example.com' }]
});
test.ok(hasAttr(compiled, 'OR'));
test.ok(hasAttr(compiled, 'a@example.com'));
test.ok(hasAttr(compiled, 'd@example.com'));
test.done();
};
module.exports['Search Compiler: OR ignored when empty'] = test => {
let connection = createMockConnection();
let compiled1 = searchCompiler(connection, { or: [] });
test.equal(compiled1.length, 0);
let compiled2 = searchCompiler(connection, { or: null });
test.equal(compiled2.length, 0);
let compiled3 = searchCompiler(connection, { or: 'not-an-array' });
test.equal(compiled3.length, 0);
test.done();
};
module.exports['Search Compiler: OR with null entry in array'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, {
or: [{ from: 'test@example.com' }, null]
});
// Should still process valid entries
test.ok(hasAttr(compiled, 'FROM'));
test.done();
};
// ============================================
// Unicode / CHARSET tests
// ============================================
module.exports['Search Compiler: Unicode adds CHARSET UTF-8'] = test => {
let connection = createMockConnection({
enabled: new Set() // UTF8=ACCEPT not enabled
});
let compiled = searchCompiler(connection, { from: 'test@example.com' });
// No unicode, no charset
test.ok(!hasAttr(compiled, 'CHARSET'));
// With unicode
let compiled2 = searchCompiler(connection, { subject: 'Test' });
test.ok(!hasAttr(compiled2, 'CHARSET'));
test.done();
};
module.exports['Search Compiler: Unicode in subject adds CHARSET'] = test => {
let connection = createMockConnection({
enabled: new Set() // UTF8=ACCEPT not enabled
});
let compiled = searchCompiler(connection, { subject: 'Test' });
test.ok(!hasAttr(compiled, 'CHARSET'));
test.done();
};
module.exports['Search Compiler: Unicode text triggers CHARSET'] = test => {
let connection = createMockConnection({
enabled: new Set() // UTF8=ACCEPT not enabled
});
let compiled = searchCompiler(connection, { from: 'user@example.com' });
test.ok(!hasAttr(compiled, 'CHARSET'));
test.done();
};
module.exports['Search Compiler: Unicode skipped when UTF8=ACCEPT enabled'] = test => {
let connection = createMockConnection({
enabled: new Set(['UTF8=ACCEPT'])
});
let compiled = searchCompiler(connection, { subject: 'Test' });
test.ok(!hasAttr(compiled, 'CHARSET'));
test.done();
};
module.exports['Search Compiler: GMRAW with Unicode adds CHARSET'] = test => {
let connection = createMockConnection({
capabilities: [['X-GM-EXT-1', true]],
enabled: new Set()
});
let compiled = searchCompiler(connection, { gmraw: 'test query' });
// ASCII query, no charset needed
test.ok(!hasAttr(compiled, 'CHARSET'));
test.done();
};
module.exports['Search Compiler: HEADER with Unicode adds CHARSET'] = test => {
let connection = createMockConnection({
enabled: new Set()
});
let compiled = searchCompiler(connection, {
header: { Subject: 'ASCII only' }
});
test.ok(!hasAttr(compiled, 'CHARSET'));
test.done();
};
// ============================================
// Complex query tests
// ============================================
module.exports['Search Compiler: Complex combined query'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, {
seen: false,
from: 'sender@example.com',
since: new Date('2023-01-01'),
larger: 1000
});
test.ok(hasAttr(compiled, 'UNSEEN'));
test.ok(hasAttr(compiled, 'FROM'));
test.ok(hasAttr(compiled, 'SINCE'));
test.ok(hasAttr(compiled, 'LARGER'));
test.done();
};
module.exports['Search Compiler: OR combined with other criteria'] = test => {
let connection = createMockConnection();
let compiled = searchCompiler(connection, {
seen: true,
or: [{ from: 'a@example.com' }, { from: 'b@example.com' }]
});
test.ok(hasAttr(compiled, 'SEEN'));
test.ok(hasAttr(compiled, 'OR'));
test.done();
};

49
backend/node_modules/imapflow/test/special-use-test.js generated vendored Normal file
View File

@@ -0,0 +1,49 @@
'use strict';
const specialUse = require('../lib/special-use');
module.exports['Special Use: flags array'] = test => {
test.ok(Array.isArray(specialUse.flags));
test.ok(specialUse.flags.includes('\\Sent'));
test.ok(specialUse.flags.includes('\\Drafts'));
test.ok(specialUse.flags.includes('\\Trash'));
test.ok(specialUse.flags.includes('\\Archive'));
test.done();
};
module.exports['Special Use: names object'] = test => {
test.ok(typeof specialUse.names === 'object');
test.ok(specialUse.names['\\Sent']);
test.ok(Array.isArray(specialUse.names['\\Sent']));
test.ok(specialUse.names['\\Sent'].includes('sent'));
test.done();
};
module.exports['Special Use: Sent folder names'] = test => {
let sentNames = specialUse.names['\\Sent'];
test.ok(sentNames.includes('sent'));
test.ok(sentNames.includes('sent items'));
test.ok(sentNames.includes('sent messages'));
test.done();
};
module.exports['Special Use: Drafts folder names'] = test => {
let draftsNames = specialUse.names['\\Drafts'];
test.ok(draftsNames.includes('drafts'));
test.done();
};
module.exports['Special Use: Trash folder names'] = test => {
let trashNames = specialUse.names['\\Trash'];
test.ok(trashNames.includes('trash'));
test.ok(trashNames.includes('deleted items'));
test.ok(trashNames.includes('deleted messages'));
test.done();
};
module.exports['Special Use: Junk folder names'] = test => {
let junkNames = specialUse.names['\\Junk'];
test.ok(junkNames.includes('spam'));
test.ok(junkNames.includes('junk'));
test.done();
};

992
backend/node_modules/imapflow/test/tools-test.js generated vendored Normal file
View File

@@ -0,0 +1,992 @@
'use strict';
const tools = require('../lib/tools');
// Mock connection for testing
let createMockConnection = (options = {}) => ({
enabled: new Set(options.utf8 ? ['UTF8=ACCEPT'] : []),
namespace: options.namespace || null
});
// ============================================
// encodePath / decodePath tests
// ============================================
module.exports['Tools: encodePath with ASCII path'] = test => {
let connection = createMockConnection();
let result = tools.encodePath(connection, 'INBOX');
test.equal(result, 'INBOX');
test.done();
};
module.exports['Tools: encodePath with Unicode path (no UTF8)'] = test => {
let connection = createMockConnection({ utf8: false });
let result = tools.encodePath(connection, 'Sent/Gesendete');
// ASCII path should remain unchanged
test.equal(result, 'Sent/Gesendete');
test.done();
};
module.exports['Tools: encodePath with Unicode when UTF8=ACCEPT enabled'] = test => {
let connection = createMockConnection({ utf8: true });
let result = tools.encodePath(connection, 'Posteingang/Ordner');
// With UTF8=ACCEPT, path should remain unchanged
test.equal(result, 'Posteingang/Ordner');
test.done();
};
module.exports['Tools: encodePath with null/undefined'] = test => {
let connection = createMockConnection();
test.equal(tools.encodePath(connection, null), '');
test.equal(tools.encodePath(connection, undefined), '');
test.done();
};
module.exports['Tools: decodePath with ASCII path'] = test => {
let connection = createMockConnection();
let result = tools.decodePath(connection, 'INBOX');
test.equal(result, 'INBOX');
test.done();
};
module.exports['Tools: decodePath with ampersand'] = test => {
let connection = createMockConnection({ utf8: false });
// UTF-7-IMAP encoded string
let result = tools.decodePath(connection, 'Test&-Folder');
test.ok(typeof result === 'string');
test.done();
};
module.exports['Tools: decodePath with UTF8=ACCEPT enabled'] = test => {
let connection = createMockConnection({ utf8: true });
let result = tools.decodePath(connection, 'Test&Folder');
// With UTF8=ACCEPT, should not decode
test.equal(result, 'Test&Folder');
test.done();
};
module.exports['Tools: decodePath with null/undefined'] = test => {
let connection = createMockConnection();
test.equal(tools.decodePath(connection, null), '');
test.equal(tools.decodePath(connection, undefined), '');
test.done();
};
// ============================================
// normalizePath tests
// ============================================
module.exports['Tools: normalizePath with INBOX (case insensitive)'] = test => {
let connection = createMockConnection();
test.equal(tools.normalizePath(connection, 'inbox'), 'INBOX');
test.equal(tools.normalizePath(connection, 'INBOX'), 'INBOX');
test.equal(tools.normalizePath(connection, 'InBox'), 'INBOX');
test.done();
};
module.exports['Tools: normalizePath with array path'] = test => {
let connection = createMockConnection({
namespace: { delimiter: '/', prefix: '' }
});
let result = tools.normalizePath(connection, ['Folder', 'Subfolder']);
test.equal(result, 'Folder/Subfolder');
test.done();
};
module.exports['Tools: normalizePath with namespace prefix'] = test => {
let connection = createMockConnection({
namespace: { delimiter: '.', prefix: 'INBOX.' }
});
let result = tools.normalizePath(connection, 'Sent');
test.equal(result, 'INBOX.Sent');
test.done();
};
module.exports['Tools: normalizePath skip namespace'] = test => {
let connection = createMockConnection({
namespace: { delimiter: '.', prefix: 'INBOX.' }
});
let result = tools.normalizePath(connection, 'Sent', true);
test.equal(result, 'Sent');
test.done();
};
module.exports['Tools: normalizePath already has prefix'] = test => {
let connection = createMockConnection({
namespace: { delimiter: '.', prefix: 'INBOX.' }
});
let result = tools.normalizePath(connection, 'INBOX.Sent');
test.equal(result, 'INBOX.Sent');
test.done();
};
// ============================================
// comparePaths tests
// ============================================
module.exports['Tools: comparePaths equal paths'] = test => {
let connection = createMockConnection();
test.equal(tools.comparePaths(connection, 'INBOX', 'INBOX'), true);
test.equal(tools.comparePaths(connection, 'inbox', 'INBOX'), true);
test.done();
};
module.exports['Tools: comparePaths different paths'] = test => {
let connection = createMockConnection();
test.equal(tools.comparePaths(connection, 'INBOX', 'Sent'), false);
test.done();
};
module.exports['Tools: comparePaths with null/undefined'] = test => {
let connection = createMockConnection();
test.equal(tools.comparePaths(connection, null, 'INBOX'), false);
test.equal(tools.comparePaths(connection, 'INBOX', null), false);
test.equal(tools.comparePaths(connection, null, null), false);
test.done();
};
// ============================================
// updateCapabilities tests
// ============================================
module.exports['Tools: updateCapabilities with valid list'] = test => {
let list = [{ value: 'IMAP4rev1' }, { value: 'IDLE' }, { value: 'NAMESPACE' }];
let result = tools.updateCapabilities(list);
test.ok(result instanceof Map);
test.equal(result.get('IMAP4rev1'), true);
test.equal(result.get('IDLE'), true);
test.equal(result.get('NAMESPACE'), true);
test.done();
};
module.exports['Tools: updateCapabilities with APPENDLIMIT'] = test => {
let list = [{ value: 'APPENDLIMIT=52428800' }];
let result = tools.updateCapabilities(list);
test.equal(result.get('APPENDLIMIT'), 52428800);
test.done();
};
module.exports['Tools: updateCapabilities with empty/null list'] = test => {
test.ok(tools.updateCapabilities(null) instanceof Map);
test.ok(tools.updateCapabilities([]) instanceof Map);
test.ok(tools.updateCapabilities(undefined) instanceof Map);
test.done();
};
module.exports['Tools: updateCapabilities skips non-string values'] = test => {
let list = [{ value: 'IDLE' }, { value: 123 }, { value: null }];
let result = tools.updateCapabilities(list);
test.equal(result.get('IDLE'), true);
test.equal(result.size, 1);
test.done();
};
// ============================================
// getStatusCode tests
// ============================================
module.exports['Tools: getStatusCode with valid response'] = test => {
let response = {
attributes: [
{
section: [{ value: 'TRYCREATE' }]
}
]
};
test.equal(tools.getStatusCode(response), 'TRYCREATE');
test.done();
};
module.exports['Tools: getStatusCode with null/invalid response'] = test => {
test.equal(tools.getStatusCode(null), false);
test.equal(tools.getStatusCode({}), false);
test.equal(tools.getStatusCode({ attributes: [] }), false);
test.equal(tools.getStatusCode({ attributes: [{}] }), false);
test.done();
};
// ============================================
// getErrorText tests
// ============================================
module.exports['Tools: getErrorText with null response'] = async test => {
let result = await tools.getErrorText(null);
test.equal(result, false);
test.done();
};
module.exports['Tools: getErrorText with valid response'] = async test => {
let response = {
tag: '*',
command: 'OK',
attributes: [{ type: 'TEXT', value: 'Success' }]
};
let result = await tools.getErrorText(response);
test.ok(typeof result === 'string');
test.done();
};
// ============================================
// getFlagColor tests
// ============================================
module.exports['Tools: getFlagColor without Flagged'] = test => {
let flags = new Set(['\\Seen']);
test.equal(tools.getFlagColor(flags), null);
test.done();
};
module.exports['Tools: getFlagColor with Flagged only (red)'] = test => {
let flags = new Set(['\\Flagged']);
test.equal(tools.getFlagColor(flags), 'red');
test.done();
};
module.exports['Tools: getFlagColor with color bits'] = test => {
// bit0=1, bit1=0, bit2=0 => orange (index 1)
let flags = new Set(['\\Flagged', '$MailFlagBit0']);
test.equal(tools.getFlagColor(flags), 'orange');
// bit0=0, bit1=1, bit2=0 => yellow (index 2)
flags = new Set(['\\Flagged', '$MailFlagBit1']);
test.equal(tools.getFlagColor(flags), 'yellow');
// bit0=1, bit1=1, bit2=0 => green (index 3)
flags = new Set(['\\Flagged', '$MailFlagBit0', '$MailFlagBit1']);
test.equal(tools.getFlagColor(flags), 'green');
// bit0=0, bit1=0, bit2=1 => blue (index 4)
flags = new Set(['\\Flagged', '$MailFlagBit2']);
test.equal(tools.getFlagColor(flags), 'blue');
// bit0=1, bit1=0, bit2=1 => purple (index 5)
flags = new Set(['\\Flagged', '$MailFlagBit0', '$MailFlagBit2']);
test.equal(tools.getFlagColor(flags), 'purple');
// bit0=0, bit1=1, bit2=1 => grey (index 6)
flags = new Set(['\\Flagged', '$MailFlagBit1', '$MailFlagBit2']);
test.equal(tools.getFlagColor(flags), 'grey');
test.done();
};
// ============================================
// getColorFlags tests
// ============================================
module.exports['Tools: getColorFlags with valid color'] = test => {
// 'orange' is index 1, which is truthy
let result = tools.getColorFlags('orange');
test.ok(Array.isArray(result.add));
test.ok(Array.isArray(result.remove));
test.ok(result.add.includes('\\Flagged'));
test.done();
};
module.exports['Tools: getColorFlags with red (index 0)'] = test => {
// 'red' is index 0, which is falsy in JS - removes \\Flagged
let result = tools.getColorFlags('red');
test.ok(Array.isArray(result.add));
test.ok(Array.isArray(result.remove));
test.ok(result.remove.includes('\\Flagged'));
test.done();
};
module.exports['Tools: getColorFlags with null (remove flag)'] = test => {
let result = tools.getColorFlags(null);
test.ok(result.remove.includes('\\Flagged'));
test.done();
};
module.exports['Tools: getColorFlags with invalid color'] = test => {
let result = tools.getColorFlags('invalid-color');
test.equal(result, null);
test.done();
};
module.exports['Tools: getColorFlags sets correct bits'] = test => {
// orange = index 1 = bit0 set
let result = tools.getColorFlags('orange');
test.ok(result.add.includes('$MailFlagBit0'));
test.ok(result.remove.includes('$MailFlagBit1'));
test.ok(result.remove.includes('$MailFlagBit2'));
// green = index 3 = bit0 + bit1 set
result = tools.getColorFlags('green');
test.ok(result.add.includes('$MailFlagBit0'));
test.ok(result.add.includes('$MailFlagBit1'));
test.ok(result.remove.includes('$MailFlagBit2'));
test.done();
};
// ============================================
// isDate tests
// ============================================
module.exports['Tools: isDate with Date object'] = test => {
test.equal(tools.isDate(new Date()), true);
test.equal(tools.isDate(new Date('2023-01-01')), true);
test.done();
};
module.exports['Tools: isDate with non-Date'] = test => {
test.equal(tools.isDate('2023-01-01'), false);
test.equal(tools.isDate(12345), false);
test.equal(tools.isDate(null), false);
test.equal(tools.isDate({}), false);
test.done();
};
// ============================================
// formatDate tests
// ============================================
module.exports['Tools: formatDate with Date object'] = test => {
let date = new Date('2023-06-15T00:00:00.000Z');
let result = tools.formatDate(date);
test.equal(result, '15-Jun-2023');
test.done();
};
module.exports['Tools: formatDate with string'] = test => {
let result = tools.formatDate('2023-06-15');
test.equal(result, '15-Jun-2023');
test.done();
};
module.exports['Tools: formatDate with invalid date'] = test => {
let result = tools.formatDate('invalid');
test.equal(result, undefined);
test.done();
};
// ============================================
// formatDateTime tests
// ============================================
module.exports['Tools: formatDateTime with Date object'] = test => {
let date = new Date('2023-06-15T14:30:45.000Z');
let result = tools.formatDateTime(date);
test.ok(result.includes('Jun-2023'));
test.ok(result.includes('14:30:45'));
test.ok(result.includes('+0000'));
test.done();
};
module.exports['Tools: formatDateTime with null/undefined'] = test => {
test.equal(tools.formatDateTime(null), undefined);
test.equal(tools.formatDateTime(undefined), undefined);
test.done();
};
module.exports['Tools: formatDateTime with string'] = test => {
let result = tools.formatDateTime('2023-06-15T10:00:00Z');
test.ok(typeof result === 'string');
test.ok(result.includes('Jun-2023'));
test.done();
};
module.exports['Tools: formatDateTime with invalid date string'] = test => {
let result = tools.formatDateTime('invalid-date-string');
test.equal(result, undefined);
test.done();
};
// ============================================
// formatFlag tests
// ============================================
module.exports['Tools: formatFlag with standard flags'] = test => {
test.equal(tools.formatFlag('\\Seen'), '\\Seen');
test.equal(tools.formatFlag('\\SEEN'), '\\Seen');
test.equal(tools.formatFlag('\\answered'), '\\Answered');
test.equal(tools.formatFlag('\\flagged'), '\\Flagged');
test.equal(tools.formatFlag('\\deleted'), '\\Deleted');
test.equal(tools.formatFlag('\\draft'), '\\Draft');
test.done();
};
module.exports['Tools: formatFlag with Recent (cannot set)'] = test => {
test.equal(tools.formatFlag('\\Recent'), false);
test.equal(tools.formatFlag('\\recent'), false);
test.done();
};
module.exports['Tools: formatFlag with custom flags'] = test => {
test.equal(tools.formatFlag('$CustomFlag'), '$CustomFlag');
test.equal(tools.formatFlag('MyFlag'), 'MyFlag');
test.done();
};
// ============================================
// canUseFlag tests
// ============================================
module.exports['Tools: canUseFlag with no mailbox'] = test => {
test.equal(tools.canUseFlag(null, '\\Seen'), true);
test.done();
};
module.exports['Tools: canUseFlag with wildcard permanent flags'] = test => {
let mailbox = { permanentFlags: new Set(['\\*']) };
test.equal(tools.canUseFlag(mailbox, '\\Seen'), true);
test.equal(tools.canUseFlag(mailbox, '$CustomFlag'), true);
test.done();
};
module.exports['Tools: canUseFlag with specific permanent flags'] = test => {
let mailbox = { permanentFlags: new Set(['\\Seen', '\\Flagged']) };
test.equal(tools.canUseFlag(mailbox, '\\Seen'), true);
test.equal(tools.canUseFlag(mailbox, '\\Flagged'), true);
test.equal(tools.canUseFlag(mailbox, '\\Deleted'), false);
test.done();
};
module.exports['Tools: canUseFlag with no permanent flags'] = test => {
let mailbox = { permanentFlags: null };
test.equal(tools.canUseFlag(mailbox, '\\Seen'), true);
test.done();
};
// ============================================
// expandRange tests
// ============================================
module.exports['Tools: expandRange with single values'] = test => {
let result = tools.expandRange('1,2,3');
test.deepEqual(result, [1, 2, 3]);
test.done();
};
module.exports['Tools: expandRange with range'] = test => {
let result = tools.expandRange('1:5');
test.deepEqual(result, [1, 2, 3, 4, 5]);
test.done();
};
module.exports['Tools: expandRange with reverse range'] = test => {
let result = tools.expandRange('5:1');
test.deepEqual(result, [5, 4, 3, 2, 1]);
test.done();
};
module.exports['Tools: expandRange with mixed'] = test => {
let result = tools.expandRange('1,3:5,10');
test.deepEqual(result, [1, 3, 4, 5, 10]);
test.done();
};
module.exports['Tools: expandRange with same start/end'] = test => {
let result = tools.expandRange('5:5');
test.deepEqual(result, [5]);
test.done();
};
// ============================================
// packMessageRange tests
// ============================================
module.exports['Tools: packMessageRange with sequential numbers'] = test => {
let result = tools.packMessageRange([1, 2, 3, 4, 5]);
test.equal(result, '1:5');
test.done();
};
module.exports['Tools: packMessageRange with gaps'] = test => {
let result = tools.packMessageRange([1, 2, 3, 7, 8, 9]);
test.equal(result, '1:3,7:9');
test.done();
};
module.exports['Tools: packMessageRange with single values'] = test => {
let result = tools.packMessageRange([1, 5, 10]);
test.equal(result, '1,5,10');
test.done();
};
module.exports['Tools: packMessageRange with unsorted input'] = test => {
let result = tools.packMessageRange([5, 1, 3, 2, 4]);
test.equal(result, '1:5');
test.done();
};
module.exports['Tools: packMessageRange with empty array'] = test => {
test.equal(tools.packMessageRange([]), '');
test.done();
};
module.exports['Tools: packMessageRange with non-array'] = test => {
test.equal(tools.packMessageRange(5), '5');
test.equal(tools.packMessageRange(null), '');
test.done();
};
// ============================================
// processName tests
// ============================================
module.exports['Tools: processName with quoted string'] = test => {
test.equal(tools.processName('"John Doe"'), 'John Doe');
test.done();
};
module.exports['Tools: processName with unquoted string'] = test => {
test.equal(tools.processName('John Doe'), 'John Doe');
test.done();
};
module.exports['Tools: processName with null/undefined'] = test => {
test.equal(tools.processName(null), '');
test.equal(tools.processName(undefined), '');
test.done();
};
module.exports['Tools: processName with short quoted'] = test => {
// String too short to have quotes removed (less than 3 chars)
test.equal(tools.processName('""'), '""');
test.equal(tools.processName('"a"'), 'a');
test.done();
};
// ============================================
// getFolderTree tests
// ============================================
module.exports['Tools: getFolderTree with flat folders'] = test => {
let folders = [
{ name: 'INBOX', path: 'INBOX', flags: new Set(), parent: [] },
{ name: 'Sent', path: 'Sent', flags: new Set(), parent: [] }
];
let tree = tools.getFolderTree(folders);
test.ok(tree.root);
test.ok(Array.isArray(tree.folders));
test.equal(tree.folders.length, 2);
test.done();
};
module.exports['Tools: getFolderTree with nested folders'] = test => {
let folders = [
{ name: 'INBOX', path: 'INBOX', flags: new Set(['\\HasChildren']), parent: [] },
{ name: 'Work', path: 'INBOX/Work', flags: new Set(), parent: ['INBOX'] }
];
let tree = tools.getFolderTree(folders);
test.ok(tree.root);
test.equal(tree.folders.length, 1);
test.equal(tree.folders[0].name, 'INBOX');
test.ok(Array.isArray(tree.folders[0].folders));
test.done();
};
module.exports['Tools: getFolderTree with Noselect flag'] = test => {
let folders = [{ name: 'Archive', path: 'Archive', flags: new Set(['\\Noselect']), parent: [] }];
let tree = tools.getFolderTree(folders);
test.equal(tree.folders[0].disabled, true);
test.done();
};
module.exports['Tools: getFolderTree with specialUse'] = test => {
let folders = [{ name: 'Sent', path: 'Sent', flags: new Set(), parent: [], specialUse: '\\Sent' }];
let tree = tools.getFolderTree(folders);
test.equal(tree.folders[0].specialUse, '\\Sent');
test.done();
};
module.exports['Tools: getFolderTree with delimiter'] = test => {
let folders = [{ name: 'Folder', path: 'Folder', flags: new Set(), parent: [], delimiter: '/' }];
let tree = tools.getFolderTree(folders);
test.equal(tree.folders[0].delimiter, '/');
test.done();
};
module.exports['Tools: getFolderTree updates existing entries'] = test => {
let folders = [
{ name: 'INBOX', path: 'INBOX', flags: new Set(['\\HasChildren']), parent: [], listed: true },
{ name: 'INBOX', path: 'INBOX', flags: new Set(['\\HasChildren']), parent: [], subscribed: true }
];
let tree = tools.getFolderTree(folders);
// Should update the existing entry, not create duplicate
test.equal(tree.folders.length, 1);
test.done();
};
// ============================================
// parseEnvelope tests
// ============================================
module.exports['Tools: parseEnvelope with complete envelope'] = test => {
let entry = [
{ value: 'Mon, 15 Jun 2023 10:00:00 +0000' }, // date
{ value: 'Test Subject' }, // subject
[[{ value: 'Sender Name' }, null, { value: 'sender' }, { value: 'example.com' }]], // from
[[{ value: 'Sender Name' }, null, { value: 'sender' }, { value: 'example.com' }]], // sender
[[{ value: 'Reply Name' }, null, { value: 'reply' }, { value: 'example.com' }]], // reply-to
[[{ value: 'To Name' }, null, { value: 'to' }, { value: 'example.com' }]], // to
[[{ value: 'CC Name' }, null, { value: 'cc' }, { value: 'example.com' }]], // cc
[[{ value: 'BCC Name' }, null, { value: 'bcc' }, { value: 'example.com' }]], // bcc
{ value: '<reply-id@example.com>' }, // in-reply-to
{ value: '<message-id@example.com>' } // message-id
];
let result = tools.parseEnvelope(entry);
test.ok(result.date instanceof Date);
test.equal(result.subject, 'Test Subject');
test.equal(result.from[0].address, 'sender@example.com');
test.equal(result.to[0].address, 'to@example.com');
test.equal(result.messageId, '<message-id@example.com>');
test.done();
};
module.exports['Tools: parseEnvelope with minimal envelope'] = test => {
let entry = [
null, // date
null, // subject
[], // from
[], // sender
[], // reply-to
[], // to
[], // cc
[], // bcc
null, // in-reply-to
null // message-id
];
let result = tools.parseEnvelope(entry);
test.ok(typeof result === 'object');
test.equal(result.subject, undefined);
test.done();
};
module.exports['Tools: parseEnvelope with invalid date'] = test => {
let entry = [{ value: 'invalid-date' }, null, null, null, null, null, null, null, null, null];
let result = tools.parseEnvelope(entry);
test.equal(result.date, 'invalid-date');
test.done();
};
module.exports['Tools: parseEnvelope with Buffer value'] = test => {
let entry = [
{ value: Buffer.from('Mon, 15 Jun 2023 10:00:00 +0000') }, // date as Buffer
{ value: Buffer.from('Buffer Subject') }, // subject as Buffer
[[{ value: Buffer.from('Sender Name') }, null, { value: Buffer.from('sender') }, { value: Buffer.from('example.com') }]], // from with Buffers
[], // sender
[], // reply-to
[], // to
[], // cc
[], // bcc
null, // in-reply-to
{ value: Buffer.from('<msg-id@example.com>') } // message-id as Buffer
];
let result = tools.parseEnvelope(entry);
test.equal(result.subject, 'Buffer Subject');
test.equal(result.from[0].address, 'sender@example.com');
test.equal(result.messageId, '<msg-id@example.com>');
test.done();
};
module.exports['Tools: parseEnvelope with empty address parts'] = test => {
// When both local part and domain are null/empty, address should be empty string
let entry = [
null, // date
null, // subject
[[{ value: 'Group Name' }, null, null, null]], // from with no email parts (group syntax)
[], // sender
[], // reply-to
[], // to
[], // cc
[], // bcc
null, // in-reply-to
null // message-id
];
let result = tools.parseEnvelope(entry);
// Address '@' should be converted to empty string and filtered out if no name
test.ok(result.from);
// The entry has a name but no valid address, should still be included with empty address
test.equal(result.from.length, 1);
test.equal(result.from[0].name, 'Group Name');
test.equal(result.from[0].address, '');
test.done();
};
// ============================================
// getStructuredParams tests
// ============================================
module.exports['Tools: getStructuredParams with simple params'] = test => {
let arr = [{ value: 'charset' }, { value: 'utf-8' }, { value: 'name' }, { value: 'file.txt' }];
let result = tools.getStructuredParams(arr);
test.equal(result.charset, 'utf-8');
test.equal(result.name, 'file.txt');
test.done();
};
module.exports['Tools: getStructuredParams with null'] = test => {
let result = tools.getStructuredParams(null);
test.deepEqual(result, {});
test.done();
};
module.exports['Tools: getStructuredParams with continuation'] = test => {
// RFC 2231 continuation
let arr = [{ value: 'filename*0' }, { value: 'very' }, { value: 'filename*1' }, { value: 'long' }, { value: 'filename*2' }, { value: 'name.txt' }];
let result = tools.getStructuredParams(arr);
test.equal(result.filename, 'verylongname.txt');
test.done();
};
// ============================================
// parseBodystructure tests
// ============================================
module.exports['Tools: parseBodystructure with simple text'] = test => {
let entry = [
{ value: 'TEXT' },
{ value: 'PLAIN' },
[{ value: 'CHARSET' }, { value: 'UTF-8' }],
null, // id
null, // description
{ value: '7BIT' }, // encoding
{ value: '1234' }, // size
{ value: '50' } // lines
];
let result = tools.parseBodystructure(entry);
test.equal(result.type, 'text/plain');
test.equal(result.encoding, '7bit');
test.equal(result.size, 1234);
test.equal(result.parameters.charset, 'UTF-8');
test.done();
};
module.exports['Tools: parseBodystructure with multipart'] = test => {
let textPart = [{ value: 'TEXT' }, { value: 'PLAIN' }, null, null, null, { value: '7BIT' }, { value: '100' }, { value: '5' }];
let htmlPart = [{ value: 'TEXT' }, { value: 'HTML' }, null, null, null, { value: 'QUOTED-PRINTABLE' }, { value: '200' }, { value: '10' }];
let entry = [textPart, htmlPart, { value: 'ALTERNATIVE' }];
let result = tools.parseBodystructure(entry);
test.equal(result.type, 'multipart/alternative');
test.ok(Array.isArray(result.childNodes));
test.equal(result.childNodes.length, 2);
test.equal(result.childNodes[0].type, 'text/plain');
test.equal(result.childNodes[1].type, 'text/html');
test.done();
};
module.exports['Tools: parseBodystructure with attachment'] = test => {
let entry = [
{ value: 'APPLICATION' },
{ value: 'PDF' },
[{ value: 'NAME' }, { value: 'document.pdf' }],
null,
null,
{ value: 'BASE64' },
{ value: '50000' }
];
let result = tools.parseBodystructure(entry);
test.equal(result.type, 'application/pdf');
test.equal(result.parameters.name, 'document.pdf');
test.done();
};
module.exports['Tools: parseBodystructure with md5'] = test => {
// Non-text type with extension data including md5
let entry = [
{ value: 'APPLICATION' },
{ value: 'OCTET-STREAM' },
null, // params
null, // id
null, // description
{ value: 'BASE64' }, // encoding
{ value: '1000' }, // size
{ value: 'd41d8cd98f00b204e9800998ecf8427e' }, // md5
null, // disposition
null // language (to ensure we have enough elements)
];
let result = tools.parseBodystructure(entry);
test.equal(result.type, 'application/octet-stream');
test.equal(result.md5, 'd41d8cd98f00b204e9800998ecf8427e');
test.done();
};
module.exports['Tools: parseBodystructure with language'] = test => {
// Non-text type with language extension
let entry = [
{ value: 'APPLICATION' },
{ value: 'PDF' },
null, // params
null, // id
null, // description
{ value: 'BASE64' }, // encoding
{ value: '5000' }, // size
null, // md5
null, // disposition
[{ value: 'EN' }, { value: 'DE' }], // language (array of values)
null // location (to ensure enough elements)
];
let result = tools.parseBodystructure(entry);
test.equal(result.type, 'application/pdf');
test.ok(Array.isArray(result.language));
test.deepEqual(result.language, ['en', 'de']);
test.done();
};
module.exports['Tools: parseBodystructure with location'] = test => {
// Non-text type with location extension
let entry = [
{ value: 'IMAGE' },
{ value: 'PNG' },
null, // params
null, // id
null, // description
{ value: 'BASE64' }, // encoding
{ value: '10000' }, // size
null, // md5
null, // disposition
null, // language
{ value: 'http://example.com/image.png' }, // location
null // extra element to ensure we have enough
];
let result = tools.parseBodystructure(entry);
test.equal(result.type, 'image/png');
test.equal(result.location, 'http://example.com/image.png');
test.done();
};
module.exports['Tools: parseBodystructure with all extension fields'] = test => {
// Non-text type with all extension fields
let entry = [
{ value: 'APPLICATION' },
{ value: 'ZIP' },
[{ value: 'NAME' }, { value: 'archive.zip' }], // params
{ value: '<id123@example.com>' }, // id
{ value: 'A zip archive' }, // description
{ value: 'BASE64' }, // encoding
{ value: '50000' }, // size
{ value: 'abc123def456' }, // md5
[{ value: 'ATTACHMENT' }, [{ value: 'FILENAME' }, { value: 'archive.zip' }]], // disposition with params
[{ value: 'EN' }], // language
{ value: 'http://example.com/archive.zip' }, // location
null // extra element
];
let result = tools.parseBodystructure(entry);
test.equal(result.type, 'application/zip');
test.equal(result.parameters.name, 'archive.zip');
test.equal(result.id, '<id123@example.com>');
test.equal(result.description, 'A zip archive');
test.equal(result.encoding, 'base64');
test.equal(result.size, 50000);
test.equal(result.md5, 'abc123def456');
test.equal(result.disposition, 'attachment');
test.equal(result.dispositionParameters.filename, 'archive.zip');
test.deepEqual(result.language, ['en']);
test.equal(result.location, 'http://example.com/archive.zip');
test.done();
};
module.exports['Tools: parseBodystructure with message/rfc822'] = test => {
// message/rfc822 has special handling with envelope and nested bodystructure
let nestedBody = [
{ value: 'TEXT' },
{ value: 'PLAIN' },
[{ value: 'CHARSET' }, { value: 'UTF-8' }],
null,
null,
{ value: '7BIT' },
{ value: '500' },
{ value: '20' } // line count for text
];
let envelope = [
{ value: 'Mon, 15 Jun 2023 10:00:00 +0000' }, // date
{ value: 'Nested Subject' }, // subject
[[null, null, { value: 'sender' }, { value: 'example.com' }]], // from
[],
[],
[],
[],
[], // sender, reply-to, to, cc, bcc
null, // in-reply-to
{ value: '<nested@example.com>' } // message-id
];
let entry = [
{ value: 'MESSAGE' },
{ value: 'RFC822' },
null, // params
null, // id
null, // description
{ value: '7BIT' }, // encoding
{ value: '10000' }, // size
envelope, // envelope
nestedBody, // nested bodystructure
{ value: '100' }, // line count
null, // md5
null // disposition
];
let result = tools.parseBodystructure(entry);
test.equal(result.type, 'message/rfc822');
test.equal(result.size, 10000);
test.equal(result.lineCount, 100);
test.ok(result.envelope);
test.equal(result.envelope.subject, 'Nested Subject');
test.ok(result.childNodes);
test.equal(result.childNodes.length, 1);
test.equal(result.childNodes[0].type, 'text/plain');
test.done();
};
// ============================================
// getDecoder tests
// ============================================
module.exports['Tools: getDecoder with standard charset'] = test => {
let decoder = tools.getDecoder('utf-8');
test.ok(decoder);
test.ok(typeof decoder.write === 'function');
test.done();
};
module.exports['Tools: getDecoder with Japanese charset'] = test => {
let decoder = tools.getDecoder('iso-2022-jp');
test.ok(decoder);
test.equal(decoder.constructor.name, 'JPDecoder');
test.done();
};
module.exports['Tools: getDecoder with null/undefined'] = test => {
let decoder = tools.getDecoder(null);
test.ok(decoder);
test.done();
};
// ============================================
// AuthenticationFailure tests
// ============================================
module.exports['Tools: AuthenticationFailure error class'] = test => {
let error = new tools.AuthenticationFailure('Auth failed');
test.ok(error instanceof Error);
test.equal(error.authenticationFailed, true);
test.equal(error.message, 'Auth failed');
test.done();
};