Files
simple-mail-cleaner/backend/node_modules/imapflow/test/commands-integration-test.js
2026-01-22 15:49:12 +01:00

8217 lines
262 KiB
JavaScript

'use strict';
/* eslint-disable new-cap */
// BigInt() is a standard JS function but triggers new-cap rule
// ============================================
// Mock Connection Factory
// ============================================
const createMockConnection = (overrides = {}) => {
const states = {
NOT_AUTHENTICATED: 1,
AUTHENTICATED: 2,
SELECTED: 3,
LOGOUT: 4
};
const defaultMailbox = {
path: 'INBOX',
flags: new Set(['\\Seen', '\\Answered', '\\Flagged', '\\Deleted', '\\Draft']),
permanentFlags: new Set(['\\*']),
exists: 100,
recent: 5,
uidNext: 1000,
uidValidity: BigInt(12345),
noModseq: false
};
return {
states,
state: overrides.state || states.SELECTED,
id: 'test-connection-id',
capabilities: new Map(overrides.capabilities || [['IMAP4rev1', true]]),
enabled: new Set(overrides.enabled || []),
authCapabilities: new Map(),
mailbox: overrides.mailbox || { ...defaultMailbox },
namespace: overrides.namespace || { delimiter: '/', prefix: '' },
expectCapabilityUpdate: overrides.expectCapabilityUpdate || false,
log: {
warn: () => {},
info: () => {},
error: () => {},
debug: () => {},
trace: () => {}
},
close: overrides.close || (() => {}),
emit: overrides.emit || (() => {}),
currentSelectCommand: false,
messageFlagsAdd: overrides.messageFlagsAdd || (async () => {}),
run: overrides.run || (async () => {}),
exec:
overrides.exec ||
(async () => ({
next: () => {},
response: { attributes: [] }
})),
...overrides
};
};
// ============================================
// CAPABILITY Command Tests
// ============================================
const capabilityCommand = require('../lib/commands/capability');
module.exports['Commands: capability returns cached when available'] = async test => {
const connection = createMockConnection({
capabilities: new Map([
['IMAP4rev1', true],
['IDLE', true]
]),
expectCapabilityUpdate: false
});
const result = await capabilityCommand(connection);
test.ok(result instanceof Map);
test.equal(result.get('IDLE'), true);
test.done();
};
module.exports['Commands: capability fetches when empty'] = async test => {
const connection = createMockConnection({
capabilities: new Map(),
exec: async () => ({ next: () => {} })
});
const result = await capabilityCommand(connection);
test.ok(result instanceof Map);
test.done();
};
module.exports['Commands: capability fetches when update expected'] = async test => {
let execCalled = false;
const connection = createMockConnection({
capabilities: new Map([['IMAP4rev1', true]]),
expectCapabilityUpdate: true,
exec: async () => {
execCalled = true;
return { next: () => {} };
}
});
await capabilityCommand(connection);
test.equal(execCalled, true);
test.done();
};
module.exports['Commands: capability handles error'] = async test => {
const connection = createMockConnection({
capabilities: new Map(),
exec: async () => {
throw new Error('Command failed');
}
});
const result = await capabilityCommand(connection);
test.equal(result, false);
test.done();
};
// ============================================
// NOOP Command Tests
// ============================================
const noopCommand = require('../lib/commands/noop');
module.exports['Commands: noop success'] = async test => {
let execCalled = false;
const connection = createMockConnection({
exec: async cmd => {
test.equal(cmd, 'NOOP');
execCalled = true;
return { next: () => {} };
}
});
const result = await noopCommand(connection);
test.equal(result, true);
test.equal(execCalled, true);
test.done();
};
module.exports['Commands: noop handles error'] = async test => {
const connection = createMockConnection({
exec: async () => {
throw new Error('Command failed');
}
});
const result = await noopCommand(connection);
test.equal(result, false);
test.done();
};
// ============================================
// LOGIN Command Tests
// ============================================
const loginCommand = require('../lib/commands/login');
module.exports['Commands: login success'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 1, // NOT_AUTHENTICATED
exec: async (cmd, attrs) => {
execArgs = { cmd, attrs };
return { next: () => {} };
}
});
const result = await loginCommand(connection, 'testuser', 'testpass');
test.equal(result, 'testuser');
test.equal(execArgs.cmd, 'LOGIN');
test.equal(execArgs.attrs[0].value, 'testuser');
test.equal(execArgs.attrs[1].value, 'testpass');
test.equal(execArgs.attrs[1].sensitive, true);
test.done();
};
module.exports['Commands: login skips when already authenticated'] = async test => {
const connection = createMockConnection({
state: 2 // AUTHENTICATED
});
const result = await loginCommand(connection, 'testuser', 'testpass');
test.equal(result, undefined);
test.done();
};
module.exports['Commands: login handles error'] = async test => {
const connection = createMockConnection({
state: 1,
exec: async () => {
const err = new Error('Auth failed');
err.response = { attributes: [] };
throw err;
}
});
try {
await loginCommand(connection, 'testuser', 'wrongpass');
test.ok(false, 'Should have thrown');
} catch (err) {
test.equal(err.authenticationFailed, true);
}
test.done();
};
module.exports['Commands: login error includes serverResponseCode'] = async test => {
const connection = createMockConnection({
state: 1,
exec: async () => {
const err = new Error('Auth failed');
err.response = {
tag: 'A1',
command: 'NO',
attributes: [
{
type: 'SECTION',
section: [{ type: 'ATOM', value: 'AUTHENTICATIONFAILED' }]
},
{ type: 'TEXT', value: 'Authentication failed' }
]
};
throw err;
}
});
try {
await loginCommand(connection, 'testuser', 'wrongpass');
test.ok(false, 'Should have thrown');
} catch (err) {
test.equal(err.authenticationFailed, true);
test.equal(err.serverResponseCode, 'AUTHENTICATIONFAILED');
}
test.done();
};
// ============================================
// LOGOUT Command Tests
// ============================================
const logoutCommand = require('../lib/commands/logout');
module.exports['Commands: logout success'] = async test => {
let execCalled = false;
const connection = createMockConnection({
exec: async cmd => {
test.equal(cmd, 'LOGOUT');
execCalled = true;
return { next: () => {} };
}
});
const result = await logoutCommand(connection);
test.equal(result, true);
test.equal(execCalled, true);
test.done();
};
module.exports['Commands: logout handles error'] = async test => {
const connection = createMockConnection({
exec: async () => {
throw new Error('Command failed');
}
});
const result = await logoutCommand(connection);
test.equal(result, false);
test.done();
};
module.exports['Commands: logout returns early when already in LOGOUT state'] = async test => {
let execCalled = false;
const connection = createMockConnection({
state: 4, // LOGOUT
exec: async () => {
execCalled = true;
return { next: () => {} };
}
});
const result = await logoutCommand(connection);
test.equal(result, false);
test.equal(execCalled, false);
test.done();
};
module.exports['Commands: logout handles NOT_AUTHENTICATED state'] = async test => {
let closeCalled = false;
const connection = createMockConnection({
state: 1, // NOT_AUTHENTICATED (mock states: 1=NOT_AUTH, 2=AUTH, 3=SELECTED, 4=LOGOUT)
exec: async () => ({ next: () => {} }),
close: () => {
closeCalled = true;
}
});
const result = await logoutCommand(connection);
test.equal(result, false);
test.equal(connection.state, connection.states.LOGOUT);
test.equal(closeCalled, true);
test.done();
};
module.exports['Commands: logout handles NoConnection error'] = async test => {
const connection = createMockConnection({
exec: async () => {
const err = new Error('No connection');
err.code = 'NoConnection';
throw err;
}
});
const result = await logoutCommand(connection);
test.equal(result, true);
test.done();
};
// ============================================
// CLOSE Command Tests
// ============================================
const closeCommand = require('../lib/commands/close');
module.exports['Commands: close success'] = async test => {
let execCalled = false;
const connection = createMockConnection({
state: 3, // SELECTED
exec: async cmd => {
test.equal(cmd, 'CLOSE');
execCalled = true;
return { next: () => {} };
}
});
const result = await closeCommand(connection);
test.equal(result, true);
test.equal(execCalled, true);
test.done();
};
module.exports['Commands: close skips when not selected'] = async test => {
const connection = createMockConnection({
state: 2 // AUTHENTICATED, not SELECTED
});
const result = await closeCommand(connection);
test.equal(result, undefined);
test.done();
};
module.exports['Commands: close handles error'] = async test => {
const connection = createMockConnection({
state: 3,
exec: async () => {
throw new Error('Command failed');
}
});
const result = await closeCommand(connection);
test.equal(result, false);
test.done();
};
module.exports['Commands: close emits mailboxClose event'] = async test => {
let emittedMailbox = null;
const testMailbox = { path: 'INBOX', uidValidity: 12345n };
const connection = createMockConnection({
state: 3,
mailbox: testMailbox,
currentSelectCommand: { command: 'SELECT', arguments: [{ value: 'INBOX' }] },
exec: async () => ({ next: () => {} }),
emit: (event, data) => {
if (event === 'mailboxClose') {
emittedMailbox = data;
}
}
});
const result = await closeCommand(connection);
test.equal(result, true);
test.ok(emittedMailbox);
test.equal(emittedMailbox.path, 'INBOX');
test.equal(connection.mailbox, false);
test.equal(connection.currentSelectCommand, false);
test.equal(connection.state, 2); // AUTHENTICATED
test.done();
};
module.exports['Commands: close without mailbox does not emit event'] = async test => {
let eventEmitted = false;
const connection = createMockConnection({
state: 3,
mailbox: false, // No mailbox
exec: async () => ({ next: () => {} }),
emit: event => {
if (event === 'mailboxClose') {
eventEmitted = true;
}
}
});
const result = await closeCommand(connection);
test.equal(result, true);
test.equal(eventEmitted, false);
test.done();
};
// ============================================
// SEARCH Command Tests
// ============================================
const searchCommand = require('../lib/commands/search');
module.exports['Commands: search with ALL'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs, opts) => {
execArgs = { cmd, attrs };
// Simulate SEARCH response
if (opts && opts.untagged && opts.untagged.SEARCH) {
await opts.untagged.SEARCH({
attributes: [{ value: '1' }, { value: '2' }, { value: '3' }]
});
}
return { next: () => {} };
}
});
const result = await searchCommand(connection, true, {});
test.deepEqual(result, [1, 2, 3]);
test.equal(execArgs.cmd, 'SEARCH');
test.done();
};
module.exports['Commands: search with UID option'] = async test => {
let execCmd = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs, opts) => {
execCmd = cmd;
if (opts && opts.untagged && opts.untagged.SEARCH) {
await opts.untagged.SEARCH({ attributes: [{ value: '100' }] });
}
return { next: () => {} };
}
});
const result = await searchCommand(connection, { all: true }, { uid: true });
test.deepEqual(result, [100]);
test.equal(execCmd, 'UID SEARCH');
test.done();
};
module.exports['Commands: search with query object'] = async test => {
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs, opts) => {
// Check that search compiler was used
test.ok(attrs.some(a => a.value === 'FROM'));
if (opts && opts.untagged && opts.untagged.SEARCH) {
await opts.untagged.SEARCH({ attributes: [] });
}
return { next: () => {} };
}
});
const result = await searchCommand(connection, { from: 'test@example.com' }, {});
test.ok(Array.isArray(result));
test.done();
};
module.exports['Commands: search skips when not selected'] = async test => {
const connection = createMockConnection({
state: 2 // AUTHENTICATED
});
const result = await searchCommand(connection, { all: true }, {});
test.equal(result, false);
test.done();
};
module.exports['Commands: search handles error'] = async test => {
const connection = createMockConnection({
state: 3,
exec: async () => {
const err = new Error('Search failed');
err.response = { attributes: [] };
throw err;
}
});
const result = await searchCommand(connection, { all: true }, {});
test.equal(result, false);
test.done();
};
module.exports['Commands: search returns false for invalid query'] = async test => {
const connection = createMockConnection({ state: 3 });
const result = await searchCommand(connection, 'invalid-query', {});
test.equal(result, false);
test.done();
};
module.exports['Commands: search error with serverResponseCode'] = async test => {
let capturedErr = null;
const connection = createMockConnection({
state: 3,
exec: async () => {
const err = new Error('Search failed');
err.response = {
tag: 'A1',
command: 'NO',
attributes: [
{
type: 'SECTION',
section: [{ type: 'ATOM', value: 'CANNOT' }]
},
{ type: 'TEXT', value: 'Search not allowed' }
]
};
throw err;
},
log: {
warn: data => {
capturedErr = data.err;
},
info: () => {},
debug: () => {},
trace: () => {},
error: () => {}
}
});
const result = await searchCommand(connection, { all: true }, {});
test.equal(result, false);
test.ok(capturedErr);
test.equal(capturedErr.serverResponseCode, 'CANNOT');
test.done();
};
// ============================================
// STORE Command Tests
// ============================================
const storeCommand = require('../lib/commands/store');
module.exports['Commands: store add flags'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs) => {
execArgs = { cmd, attrs };
return { next: () => {} };
}
});
const result = await storeCommand(connection, '1:10', ['\\Seen'], { operation: 'add' });
test.equal(result, true);
test.equal(execArgs.cmd, 'STORE');
test.ok(execArgs.attrs[1].value.startsWith('+'));
test.done();
};
module.exports['Commands: store remove flags'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs) => {
execArgs = { cmd, attrs };
return { next: () => {} };
}
});
const result = await storeCommand(connection, '1:10', ['\\Seen'], { operation: 'remove' });
test.equal(result, true);
test.ok(execArgs.attrs[1].value.startsWith('-'));
test.done();
};
module.exports['Commands: store set flags'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs) => {
execArgs = { cmd, attrs };
return { next: () => {} };
}
});
const result = await storeCommand(connection, '1:10', ['\\Seen'], { operation: 'set' });
test.equal(result, true);
test.ok(!execArgs.attrs[1].value.startsWith('+'));
test.ok(!execArgs.attrs[1].value.startsWith('-'));
test.done();
};
module.exports['Commands: store with UID'] = async test => {
let execCmd = null;
const connection = createMockConnection({
state: 3,
exec: async cmd => {
execCmd = cmd;
return { next: () => {} };
}
});
await storeCommand(connection, '100', ['\\Flagged'], { uid: true });
test.equal(execCmd, 'UID STORE');
test.done();
};
module.exports['Commands: store with silent'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs) => {
execArgs = { cmd, attrs };
return { next: () => {} };
}
});
await storeCommand(connection, '1', ['\\Seen'], { silent: true });
test.ok(execArgs.attrs[1].value.includes('.SILENT'));
test.done();
};
module.exports['Commands: store with Gmail labels'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['X-GM-EXT-1', true]]),
exec: async (cmd, attrs) => {
execArgs = { cmd, attrs };
return { next: () => {} };
}
});
await storeCommand(connection, '1', ['Important'], { useLabels: true });
test.ok(execArgs.attrs[1].value.includes('X-GM-LABELS'));
test.done();
};
module.exports['Commands: store skips when labels not supported'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map() // No X-GM-EXT-1
});
const result = await storeCommand(connection, '1', ['Label'], { useLabels: true });
test.equal(result, false);
test.done();
};
module.exports['Commands: store skips when not selected'] = async test => {
const connection = createMockConnection({ state: 2 });
const result = await storeCommand(connection, '1:10', ['\\Seen'], {});
test.equal(result, false);
test.done();
};
module.exports['Commands: store skips when no range'] = async test => {
const connection = createMockConnection({ state: 3 });
const result = await storeCommand(connection, null, ['\\Seen'], {});
test.equal(result, false);
test.done();
};
module.exports['Commands: store with CONDSTORE'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 3,
enabled: new Set(['CONDSTORE']),
exec: async (cmd, attrs) => {
execArgs = { cmd, attrs };
return { next: () => {} };
}
});
await storeCommand(connection, '1', ['\\Seen'], { unchangedSince: 12345 });
test.ok(execArgs.attrs.some(a => Array.isArray(a) && a.some(x => x.value === 'UNCHANGEDSINCE')));
test.done();
};
module.exports['Commands: store handles error'] = async test => {
const connection = createMockConnection({
state: 3,
exec: async () => {
const err = new Error('Store failed');
err.response = { attributes: [] };
throw err;
}
});
const result = await storeCommand(connection, '1', ['\\Seen'], {});
test.equal(result, false);
test.done();
};
module.exports['Commands: store error with serverResponseCode'] = async test => {
let capturedErr = null;
const connection = createMockConnection({
state: 3,
exec: async () => {
const err = new Error('Store failed');
err.response = {
tag: '*',
command: 'NO',
attributes: [
{
type: 'SECTION',
section: [{ type: 'ATOM', value: 'CANNOT' }]
},
{ type: 'TEXT', value: 'Cannot modify flags' }
]
};
throw err;
},
log: {
warn: data => {
capturedErr = data.err;
}
}
});
const result = await storeCommand(connection, '1', ['\\Seen'], {});
test.equal(result, false);
test.ok(capturedErr);
test.equal(capturedErr.serverResponseCode, 'CANNOT');
test.done();
};
module.exports['Commands: store filters flags that cannot be used'] = async test => {
let execAttrs = null;
const connection = createMockConnection({
state: 3,
mailbox: {
permanentFlags: new Set(['\\Seen']) // Only \\Seen is allowed
},
exec: async (cmd, attrs) => {
execAttrs = attrs;
return { next: () => {} };
}
});
// Try to add \\Deleted which is not in permanentFlags
const result = await storeCommand(connection, '1', ['\\Seen', '\\Deleted'], { operation: 'add' });
test.equal(result, true);
test.ok(execAttrs);
// Flags list should only contain \\Seen
const flagsList = execAttrs[2];
test.equal(flagsList.length, 1);
test.equal(flagsList[0].value, '\\Seen');
test.done();
};
module.exports['Commands: store remove operation uses minus prefix'] = async test => {
let execAttrs = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs) => {
execAttrs = attrs;
return { next: () => {} };
}
});
const result = await storeCommand(connection, '1', ['\\Seen', '\\Deleted'], { operation: 'remove' });
test.equal(result, true);
test.ok(execAttrs);
// Remove operation should use -FLAGS prefix
test.equal(execAttrs[1].value, '-FLAGS');
const flagsList = execAttrs[2];
test.equal(flagsList.length, 2);
test.done();
};
module.exports['Commands: store returns false when no valid flags for add'] = async test => {
const connection = createMockConnection({
state: 3,
mailbox: {
permanentFlags: new Set() // No flags allowed
}
});
// All flags get filtered out
const result = await storeCommand(connection, '1', ['\\Seen', '\\Deleted'], { operation: 'add' });
test.equal(result, false);
test.done();
};
module.exports['Commands: store allows empty flags for set operation'] = async test => {
let execCalled = false;
const connection = createMockConnection({
state: 3,
mailbox: {
permanentFlags: new Set() // No flags allowed, all get filtered
},
exec: async () => {
execCalled = true;
return { next: () => {} };
}
});
// Set operation with empty flags should still proceed (to clear flags)
const result = await storeCommand(connection, '1', ['\\Seen'], { operation: 'set' });
test.equal(result, true);
test.equal(execCalled, true);
test.done();
};
module.exports['Commands: store returns false with empty flags for remove'] = async test => {
const connection = createMockConnection({
state: 3,
mailbox: {
permanentFlags: new Set()
}
});
// Remove with no valid flags should return false (nothing to remove)
const result = await storeCommand(connection, '1', [], { operation: 'remove' });
test.equal(result, false);
test.done();
};
module.exports['Commands: store default operation is add'] = async test => {
let execAttrs = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs) => {
execAttrs = attrs;
return { next: () => {} };
}
});
await storeCommand(connection, '1', ['\\Seen'], {}); // No operation specified
test.ok(execAttrs);
test.equal(execAttrs[1].value, '+FLAGS');
test.done();
};
module.exports['Commands: store with labels uses X-GM-LABELS'] = async test => {
let execAttrs = null;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['X-GM-EXT-1', true]]),
exec: async (cmd, attrs) => {
execAttrs = attrs;
return { next: () => {} };
}
});
await storeCommand(connection, '1', ['Important'], { useLabels: true, operation: 'add' });
test.ok(execAttrs);
test.equal(execAttrs[1].value, '+X-GM-LABELS');
test.done();
};
module.exports['Commands: store silent does not apply to labels'] = async test => {
let execAttrs = null;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['X-GM-EXT-1', true]]),
exec: async (cmd, attrs) => {
execAttrs = attrs;
return { next: () => {} };
}
});
// When using labels, silent flag should not add .SILENT suffix
await storeCommand(connection, '1', ['Important'], { useLabels: true, silent: true, operation: 'set' });
test.ok(execAttrs);
test.equal(execAttrs[1].value, 'X-GM-LABELS'); // Not X-GM-LABELS.SILENT
test.done();
};
// ============================================
// COPY Command Tests
// ============================================
const copyCommand = require('../lib/commands/copy');
module.exports['Commands: copy success'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs) => {
execArgs = { cmd, attrs };
return {
next: () => {},
response: { attributes: [] }
};
}
});
const result = await copyCommand(connection, '1:10', 'Archive', {});
test.ok(result);
test.equal(result.destination, 'Archive');
test.equal(execArgs.cmd, 'COPY');
test.done();
};
module.exports['Commands: copy with UID'] = async test => {
let execCmd = null;
const connection = createMockConnection({
state: 3,
exec: async cmd => {
execCmd = cmd;
return { next: () => {}, response: { attributes: [] } };
}
});
await copyCommand(connection, '100', 'Archive', { uid: true });
test.equal(execCmd, 'UID COPY');
test.done();
};
module.exports['Commands: copy with COPYUID response'] = async test => {
const connection = createMockConnection({
state: 3,
exec: async () => ({
next: () => {},
response: {
attributes: [
{
section: [{ value: 'COPYUID' }, { value: '12345' }, { value: '1:3' }, { value: '100:102' }]
}
]
}
})
});
const result = await copyCommand(connection, '1:3', 'Archive', {});
test.ok(result.uidValidity);
test.ok(result.uidMap instanceof Map);
test.equal(result.uidMap.get(1), 100);
test.equal(result.uidMap.get(2), 101);
test.equal(result.uidMap.get(3), 102);
test.done();
};
module.exports['Commands: copy skips when not selected'] = async test => {
const connection = createMockConnection({ state: 2 });
const result = await copyCommand(connection, '1:10', 'Archive', {});
test.equal(result, undefined);
test.done();
};
module.exports['Commands: copy skips when no range'] = async test => {
const connection = createMockConnection({ state: 3 });
const result = await copyCommand(connection, null, 'Archive', {});
test.equal(result, undefined);
test.done();
};
module.exports['Commands: copy skips when no destination'] = async test => {
const connection = createMockConnection({ state: 3 });
const result = await copyCommand(connection, '1:10', null, {});
test.equal(result, undefined);
test.done();
};
module.exports['Commands: copy handles error'] = async test => {
const connection = createMockConnection({
state: 3,
exec: async () => {
const err = new Error('Copy failed');
err.response = { attributes: [] };
throw err;
}
});
const result = await copyCommand(connection, '1:10', 'Archive', {});
test.equal(result, false);
test.done();
};
module.exports['Commands: copy error with serverResponseCode'] = async test => {
const connection = createMockConnection({
state: 3,
exec: async () => {
const err = new Error('Copy failed');
err.response = {
tag: '*',
command: 'NO',
attributes: [
{
type: 'SECTION',
section: [{ type: 'ATOM', value: 'TRYCREATE' }]
},
{ type: 'TEXT', value: 'Mailbox does not exist' }
]
};
throw err;
}
});
const result = await copyCommand(connection, '1:10', 'NonExistent', {});
test.equal(result, false);
test.done();
};
module.exports['Commands: copy with partial COPYUID response'] = async test => {
const connection = createMockConnection({
state: 3,
mailbox: { path: 'INBOX' },
exec: async () => ({
next: () => {},
response: {
attributes: [
{
type: 'ATOM',
section: [
{ type: 'ATOM', value: 'COPYUID' },
{ type: 'ATOM', value: '12345' }
// Missing source and destination UIDs
]
}
]
}
})
});
const result = await copyCommand(connection, '1:10', 'Archive', {});
test.ok(result);
test.equal(result.path, 'INBOX');
test.equal(result.destination, 'Archive');
test.equal(result.uidValidity, 12345n);
test.equal(result.uidMap, undefined);
test.done();
};
module.exports['Commands: copy with invalid uidValidity'] = async test => {
const connection = createMockConnection({
state: 3,
mailbox: { path: 'INBOX' },
exec: async () => ({
next: () => {},
response: {
attributes: [
{
type: 'ATOM',
section: [
{ type: 'ATOM', value: 'COPYUID' },
{ type: 'ATOM', value: 'invalid' } // Non-numeric uidValidity
]
}
]
}
})
});
const result = await copyCommand(connection, '1:10', 'Archive', {});
test.ok(result);
test.equal(result.uidValidity, undefined);
test.done();
};
module.exports['Commands: copy with mismatched UID counts'] = async test => {
const connection = createMockConnection({
state: 3,
mailbox: { path: 'INBOX' },
exec: async () => ({
next: () => {},
response: {
attributes: [
{
type: 'ATOM',
section: [
{ type: 'ATOM', value: 'COPYUID' },
{ type: 'ATOM', value: '12345' },
{ type: 'ATOM', value: '1:3' }, // 3 source UIDs
{ type: 'ATOM', value: '100:101' } // 2 destination UIDs
]
}
]
}
})
});
const result = await copyCommand(connection, '1:3', 'Archive', {});
test.ok(result);
test.equal(result.uidValidity, 12345n);
test.equal(result.uidMap, undefined); // Not set due to mismatch
test.done();
};
module.exports['Commands: copy with non-COPYUID response code'] = async test => {
const connection = createMockConnection({
state: 3,
mailbox: { path: 'INBOX' },
exec: async () => ({
next: () => {},
response: {
attributes: [
{
type: 'ATOM',
section: [{ type: 'ATOM', value: 'APPENDUID' }] // Not COPYUID
}
]
}
})
});
const result = await copyCommand(connection, '1:10', 'Archive', {});
test.ok(result);
test.equal(result.path, 'INBOX');
test.equal(result.destination, 'Archive');
test.equal(result.uidValidity, undefined);
test.equal(result.uidMap, undefined);
test.done();
};
// ============================================
// MOVE Command Tests
// ============================================
const moveCommand = require('../lib/commands/move');
module.exports['Commands: move with MOVE capability'] = async test => {
let execCmd = null;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['MOVE', true]]),
exec: async cmd => {
execCmd = cmd;
return { next: () => {}, response: { attributes: [] } };
}
});
await moveCommand(connection, '1:10', 'Archive', {});
test.equal(execCmd, 'MOVE');
test.done();
};
module.exports['Commands: move with UID and MOVE capability'] = async test => {
let execCmd = null;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['MOVE', true]]),
exec: async cmd => {
execCmd = cmd;
return { next: () => {}, response: { attributes: [] } };
}
});
await moveCommand(connection, '100', 'Archive', { uid: true });
test.equal(execCmd, 'UID MOVE');
test.done();
};
module.exports['Commands: move skips when not selected'] = async test => {
const connection = createMockConnection({ state: 2 });
const result = await moveCommand(connection, '1:10', 'Archive', {});
test.equal(result, undefined);
test.done();
};
module.exports['Commands: move skips when no range'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['MOVE', true]])
});
const result = await moveCommand(connection, null, 'Archive', {});
test.equal(result, undefined);
test.done();
};
module.exports['Commands: move skips when no destination'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['MOVE', true]])
});
const result = await moveCommand(connection, '1:10', null, {});
test.equal(result, undefined);
test.done();
};
module.exports['Commands: move fallback without MOVE capability'] = async test => {
let copyCalled = false;
let deleteCalled = false;
const connection = createMockConnection({
state: 3,
capabilities: new Map(), // No MOVE capability
messageCopy: async (range, dest) => {
copyCalled = true;
test.equal(range, '1:10');
test.equal(dest, 'Archive');
return { path: 'INBOX', destination: 'Archive' };
},
messageDelete: async (range, opts) => {
deleteCalled = true;
test.equal(range, '1:10');
test.equal(opts.silent, true);
return true;
}
});
const result = await moveCommand(connection, '1:10', 'Archive', {});
test.ok(copyCalled);
test.ok(deleteCalled);
test.equal(result.destination, 'Archive');
test.done();
};
module.exports['Commands: move fallback passes options'] = async test => {
let copyOpts = null;
let deleteOpts = null;
const connection = createMockConnection({
state: 3,
capabilities: new Map(), // No MOVE capability
messageCopy: async (range, dest, opts) => {
copyOpts = opts;
return { path: 'INBOX', destination: dest };
},
messageDelete: async (range, opts) => {
deleteOpts = opts;
return true;
}
});
await moveCommand(connection, '1:10', 'Archive', { uid: true });
test.equal(copyOpts.uid, true);
test.equal(deleteOpts.uid, true);
test.equal(deleteOpts.silent, true);
test.done();
};
module.exports['Commands: move with COPYUID response'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['MOVE', true]]),
exec: async () => ({
next: () => {},
response: {
attributes: [
{
section: [{ value: 'COPYUID' }, { value: '12345' }, { value: '1:3' }, { value: '100:102' }]
}
]
}
})
});
const result = await moveCommand(connection, '1:3', 'Archive', {});
test.ok(result.uidValidity);
test.equal(result.uidValidity, BigInt(12345));
test.ok(result.uidMap instanceof Map);
test.equal(result.uidMap.get(1), 100);
test.equal(result.uidMap.get(2), 101);
test.equal(result.uidMap.get(3), 102);
test.done();
};
module.exports['Commands: move handles COPYUID in untagged response'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['MOVE', true]]),
exec: async (cmd, attrs, opts) => {
// Simulate untagged OK with COPYUID
if (opts && opts.untagged && opts.untagged.OK) {
await opts.untagged.OK({
attributes: [
{
section: [{ value: 'COPYUID' }, { value: '99999' }, { value: '5:7' }, { value: '200:202' }]
}
]
});
}
return {
next: () => {},
response: { attributes: [] }
};
}
});
const result = await moveCommand(connection, '5:7', 'Archive', {});
test.ok(result.uidMap instanceof Map);
test.equal(result.uidMap.get(5), 200);
test.equal(result.uidMap.get(6), 201);
test.equal(result.uidMap.get(7), 202);
test.done();
};
module.exports['Commands: move returns correct map structure'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['MOVE', true]]),
exec: async () => ({
next: () => {},
response: { attributes: [] }
})
});
const result = await moveCommand(connection, '1:10', 'Archive', {});
test.equal(result.path, 'INBOX');
test.equal(result.destination, 'Archive');
test.done();
};
module.exports['Commands: move handles error'] = async test => {
let warnLogged = false;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['MOVE', true]]),
exec: async () => {
const err = new Error('Move failed');
err.response = { attributes: [] };
throw err;
},
log: {
warn: () => {
warnLogged = true;
},
debug: () => {},
trace: () => {}
}
});
const result = await moveCommand(connection, '1:10', 'Archive', {});
test.equal(result, false);
test.ok(warnLogged);
test.done();
};
module.exports['Commands: move handles error with status code'] = async test => {
let capturedErr = null;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['MOVE', true]]),
exec: async () => {
const err = new Error('Move failed');
// Provide response with TRYCREATE status code
err.response = {
tag: 'A1',
command: 'NO',
attributes: [
{
type: 'ATOM',
value: '',
section: [{ type: 'ATOM', value: 'TRYCREATE' }]
},
{ type: 'TEXT', value: 'Mailbox does not exist' }
]
};
throw err;
},
log: {
warn: msg => {
capturedErr = msg;
},
debug: () => {},
trace: () => {}
}
});
const result = await moveCommand(connection, '1:10', 'NonExistent', {});
test.equal(result, false);
test.ok(capturedErr);
test.done();
};
module.exports['Commands: move normalizes destination path'] = async test => {
let capturedAttrs = null;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['MOVE', true]]),
namespace: { delimiter: '/', prefix: 'INBOX/' },
exec: async (cmd, attrs) => {
capturedAttrs = attrs;
return { next: () => {}, response: { attributes: [] } };
}
});
await moveCommand(connection, '1:10', 'Archive', {});
// The destination should be normalized
test.ok(capturedAttrs);
test.done();
};
module.exports['Commands: move handles COPYUID with invalid uidValidity'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['MOVE', true]]),
exec: async () => ({
next: () => {},
response: {
attributes: [
{
section: [
{ type: 'ATOM', value: 'COPYUID' },
{ value: 'invalid' }, // Invalid uidValidity (NaN)
{ value: '1:5' },
{ value: '100:104' }
]
}
]
}
})
});
const result = await moveCommand(connection, '1:5', 'Archive', {});
test.ok(result);
test.equal(result.uidValidity, undefined);
test.done();
};
module.exports['Commands: move handles COPYUID with mismatched UID counts'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['MOVE', true]]),
exec: async () => ({
next: () => {},
response: {
attributes: [
{
section: [
{ type: 'ATOM', value: 'COPYUID' },
{ value: '12345' },
{ value: '1:5' }, // 5 source UIDs
{ value: '100:102' } // Only 3 destination UIDs - mismatch
]
}
]
}
})
});
const result = await moveCommand(connection, '1:5', 'Archive', {});
test.ok(result);
test.equal(result.uidValidity, BigInt(12345));
test.equal(result.uidMap, undefined); // Not set due to mismatch
test.done();
};
module.exports['Commands: move handles COPYUID with missing source/destination UIDs'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['MOVE', true]]),
exec: async () => ({
next: () => {},
response: {
attributes: [
{
section: [
{ type: 'ATOM', value: 'COPYUID' },
{ value: '12345' },
{ value: null }, // Missing source UIDs
{ value: '100:104' }
]
}
]
}
})
});
const result = await moveCommand(connection, '1:5', 'Archive', {});
test.ok(result);
test.equal(result.uidMap, undefined);
test.done();
};
// ============================================
// EXPUNGE Command Tests
// ============================================
const expungeCommand = require('../lib/commands/expunge');
module.exports['Commands: expunge success'] = async test => {
let execCalled = false;
const connection = createMockConnection({
state: 3,
exec: async cmd => {
test.equal(cmd, 'EXPUNGE');
execCalled = true;
return { next: () => {}, response: { attributes: [] } };
}
});
const result = await expungeCommand(connection, '1:*', {});
test.equal(result, true);
test.equal(execCalled, true);
test.done();
};
module.exports['Commands: expunge with UID range'] = async test => {
let execCmd = null;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['UIDPLUS', true]]),
exec: async cmd => {
execCmd = cmd;
return { next: () => {}, response: { attributes: [] } };
}
});
await expungeCommand(connection, '1:100', { uid: true });
test.equal(execCmd, 'UID EXPUNGE');
test.done();
};
module.exports['Commands: expunge skips when not selected'] = async test => {
const connection = createMockConnection({ state: 2 });
const result = await expungeCommand(connection, '1:*', {});
test.equal(result, undefined);
test.done();
};
module.exports['Commands: expunge skips when no range'] = async test => {
const connection = createMockConnection({ state: 3 });
const result = await expungeCommand(connection, null, {});
test.equal(result, undefined);
test.done();
};
module.exports['Commands: expunge handles error'] = async test => {
const connection = createMockConnection({
state: 3,
exec: async () => {
const err = new Error('Expunge failed');
err.response = { attributes: [] };
throw err;
}
});
const result = await expungeCommand(connection, '1:*', {});
test.equal(result, false);
test.done();
};
module.exports['Commands: expunge parses HIGHESTMODSEQ response'] = async test => {
const connection = createMockConnection({
state: 3,
mailbox: { highestModseq: 100n },
exec: async () => ({
next: () => {},
response: {
attributes: [
{
type: 'ATOM',
section: [
{ type: 'ATOM', value: 'HIGHESTMODSEQ' },
{ type: 'ATOM', value: '9122' }
]
}
]
}
})
});
const result = await expungeCommand(connection, '1:*', {});
test.equal(result, true);
test.equal(connection.mailbox.highestModseq, 9122n);
test.done();
};
module.exports['Commands: expunge does not update lower HIGHESTMODSEQ'] = async test => {
const connection = createMockConnection({
state: 3,
mailbox: { highestModseq: 10000n },
exec: async () => ({
next: () => {},
response: {
attributes: [
{
type: 'ATOM',
section: [
{ type: 'ATOM', value: 'HIGHESTMODSEQ' },
{ type: 'ATOM', value: '5000' }
]
}
]
}
})
});
const result = await expungeCommand(connection, '1:*', {});
test.equal(result, true);
test.equal(connection.mailbox.highestModseq, 10000n); // Should not be updated
test.done();
};
module.exports['Commands: expunge handles invalid HIGHESTMODSEQ value'] = async test => {
const connection = createMockConnection({
state: 3,
mailbox: { highestModseq: 100n },
exec: async () => ({
next: () => {},
response: {
attributes: [
{
type: 'ATOM',
section: [
{ type: 'ATOM', value: 'HIGHESTMODSEQ' },
{ type: 'ATOM', value: 'invalid' }
]
}
]
}
})
});
const result = await expungeCommand(connection, '1:*', {});
test.equal(result, true);
test.equal(connection.mailbox.highestModseq, 100n); // Should not be updated
test.done();
};
module.exports['Commands: expunge updates HIGHESTMODSEQ when mailbox has none'] = async test => {
const connection = createMockConnection({
state: 3,
mailbox: {}, // No highestModseq
exec: async () => ({
next: () => {},
response: {
attributes: [
{
type: 'ATOM',
section: [
{ type: 'ATOM', value: 'HIGHESTMODSEQ' },
{ type: 'ATOM', value: '500' }
]
}
]
}
})
});
const result = await expungeCommand(connection, '1:*', {});
test.equal(result, true);
test.equal(connection.mailbox.highestModseq, 500n);
test.done();
};
module.exports['Commands: expunge error with serverResponseCode'] = async test => {
let capturedErr = null;
const connection = createMockConnection({
state: 3,
exec: async () => {
const err = new Error('Expunge failed');
err.response = {
tag: '*',
command: 'NO',
attributes: [
{
type: 'SECTION',
section: [{ type: 'ATOM', value: 'CANNOT' }]
},
{ type: 'TEXT', value: 'Cannot expunge' }
]
};
throw err;
},
log: {
warn: data => {
capturedErr = data.err;
}
}
});
const result = await expungeCommand(connection, '1:*', {});
test.equal(result, false);
test.ok(capturedErr);
test.equal(capturedErr.serverResponseCode, 'CANNOT');
test.done();
};
module.exports['Commands: expunge without UID when UIDPLUS not available'] = async test => {
let execCmd = null;
let execAttrs = null;
const connection = createMockConnection({
state: 3,
capabilities: new Map(), // No UIDPLUS
exec: async (cmd, attrs) => {
execCmd = cmd;
execAttrs = attrs;
return { next: () => {}, response: { attributes: [] } };
}
});
await expungeCommand(connection, '1:100', { uid: true });
test.equal(execCmd, 'EXPUNGE'); // Falls back to EXPUNGE
test.equal(execAttrs, false); // No attributes for regular EXPUNGE
test.done();
};
module.exports['Commands: expunge with UID EXPUNGE includes range'] = async test => {
let execAttrs = null;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['UIDPLUS', true]]),
exec: async (cmd, attrs) => {
execAttrs = attrs;
return { next: () => {}, response: { attributes: [] } };
}
});
await expungeCommand(connection, '1:50', { uid: true });
test.ok(execAttrs);
test.equal(execAttrs[0].type, 'SEQUENCE');
test.equal(execAttrs[0].value, '1:50');
test.done();
};
module.exports['Commands: expunge with non-HIGHESTMODSEQ response code'] = async test => {
const connection = createMockConnection({
state: 3,
mailbox: { highestModseq: 100n },
exec: async () => ({
next: () => {},
response: {
attributes: [
{
type: 'ATOM',
section: [{ type: 'ATOM', value: 'OTHERCODE' }]
}
]
}
})
});
const result = await expungeCommand(connection, '1:*', {});
test.equal(result, true);
test.equal(connection.mailbox.highestModseq, 100n); // Should not be updated
test.done();
};
// ============================================
// CREATE Command Tests
// ============================================
const createCommand = require('../lib/commands/create');
module.exports['Commands: create success'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 2,
exec: async (cmd, attrs) => {
execArgs = { cmd, attrs };
return { next: () => {}, response: { attributes: [] } };
}
});
const result = await createCommand(connection, 'NewFolder');
test.ok(result);
test.equal(result.created, true);
test.equal(execArgs.cmd, 'CREATE');
test.done();
};
module.exports['Commands: create skips when not authenticated'] = async test => {
const connection = createMockConnection({ state: 1 });
const result = await createCommand(connection, 'NewFolder');
test.equal(result, undefined);
test.done();
};
module.exports['Commands: create handles ALREADYEXISTS'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async () => {
const err = new Error('Mailbox already exists');
err.response = {
tag: '*',
command: 'NO',
attributes: [
{
type: 'SECTION',
section: [{ type: 'ATOM', value: 'ALREADYEXISTS' }]
},
{ type: 'TEXT', value: 'Mailbox already exists' }
]
};
throw err;
}
});
const result = await createCommand(connection, 'ExistingFolder');
test.ok(result);
test.equal(result.created, false);
test.done();
};
module.exports['Commands: create throws on other errors'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async () => {
const err = new Error('Create failed');
err.response = {
tag: '*',
command: 'NO',
attributes: [{ type: 'TEXT', value: 'Create failed' }]
};
throw err;
}
});
try {
await createCommand(connection, 'NewFolder');
test.ok(false, 'Should have thrown');
} catch (err) {
test.ok(err.message.includes('Create failed'));
}
test.done();
};
// ============================================
// DELETE Command Tests
// ============================================
const deleteCommand = require('../lib/commands/delete');
module.exports['Commands: delete success'] = async test => {
let execCmd = null;
const connection = createMockConnection({
state: 2,
exec: async cmd => {
execCmd = cmd;
return { next: () => {} };
}
});
const result = await deleteCommand(connection, 'OldFolder');
test.ok(result);
test.equal(result.path, 'OldFolder');
test.equal(execCmd, 'DELETE');
test.done();
};
module.exports['Commands: delete skips when not authenticated'] = async test => {
const connection = createMockConnection({ state: 1 });
const result = await deleteCommand(connection, 'OldFolder');
test.equal(result, undefined);
test.done();
};
module.exports['Commands: delete throws on error'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async () => {
const err = new Error('Delete failed');
err.response = {
tag: '*',
command: 'NO',
attributes: [{ type: 'TEXT', value: 'Delete failed' }]
};
throw err;
}
});
try {
await deleteCommand(connection, 'OldFolder');
test.ok(false, 'Should have thrown');
} catch (err) {
test.ok(err.message.includes('Delete failed'));
}
test.done();
};
module.exports['Commands: delete closes mailbox when deleting current mailbox'] = async test => {
let closeCalled = false;
let execCmd = null;
const connection = createMockConnection({
state: 3, // SELECTED
mailbox: { path: 'FolderToDelete' },
run: async cmd => {
if (cmd === 'CLOSE') {
closeCalled = true;
}
},
exec: async cmd => {
execCmd = cmd;
return { next: () => {} };
}
});
const result = await deleteCommand(connection, 'FolderToDelete');
test.ok(closeCalled, 'CLOSE should be called');
test.equal(execCmd, 'DELETE');
test.ok(result);
test.equal(result.path, 'FolderToDelete');
test.done();
};
module.exports['Commands: delete does not close when deleting different mailbox'] = async test => {
let closeCalled = false;
const connection = createMockConnection({
state: 3, // SELECTED
mailbox: { path: 'INBOX' },
run: async cmd => {
if (cmd === 'CLOSE') {
closeCalled = true;
}
},
exec: async () => ({ next: () => {} })
});
const result = await deleteCommand(connection, 'OtherFolder');
test.ok(!closeCalled, 'CLOSE should not be called');
test.ok(result);
test.equal(result.path, 'OtherFolder');
test.done();
};
module.exports['Commands: delete works in SELECTED state'] = async test => {
let execCmd = null;
const connection = createMockConnection({
state: 3, // SELECTED
mailbox: { path: 'INBOX' },
exec: async cmd => {
execCmd = cmd;
return { next: () => {} };
}
});
const result = await deleteCommand(connection, 'SomeFolder');
test.ok(result);
test.equal(execCmd, 'DELETE');
test.done();
};
module.exports['Commands: delete error with serverResponseCode'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async () => {
const err = new Error('Delete failed');
err.response = {
tag: '*',
command: 'NO',
attributes: [
{
type: 'SECTION',
section: [{ type: 'ATOM', value: 'NONEXISTENT' }]
},
{ type: 'TEXT', value: 'Mailbox does not exist' }
]
};
throw err;
}
});
try {
await deleteCommand(connection, 'NonExistent');
test.ok(false, 'Should have thrown');
} catch (err) {
test.equal(err.serverResponseCode, 'NONEXISTENT');
}
test.done();
};
module.exports['Commands: delete normalizes path'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 2,
exec: async (cmd, args) => {
execArgs = args;
return { next: () => {} };
}
});
const result = await deleteCommand(connection, 'INBOX/Subfolder');
test.ok(result);
test.equal(result.path, 'INBOX/Subfolder');
test.ok(execArgs);
test.done();
};
// ============================================
// RENAME Command Tests
// ============================================
const renameCommand = require('../lib/commands/rename');
module.exports['Commands: rename success'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 2,
exec: async (cmd, attrs) => {
execArgs = { cmd, attrs };
return { next: () => {} };
}
});
const result = await renameCommand(connection, 'OldName', 'NewName');
test.ok(result);
test.equal(result.path, 'OldName');
test.equal(result.newPath, 'NewName');
test.equal(execArgs.cmd, 'RENAME');
test.done();
};
module.exports['Commands: rename skips when not authenticated'] = async test => {
const connection = createMockConnection({ state: 1 });
const result = await renameCommand(connection, 'OldName', 'NewName');
test.equal(result, undefined);
test.done();
};
module.exports['Commands: rename throws on error'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async () => {
const err = new Error('Rename failed');
err.response = {
tag: '*',
command: 'NO',
attributes: [{ type: 'TEXT', value: 'Rename failed' }]
};
throw err;
}
});
try {
await renameCommand(connection, 'OldName', 'NewName');
test.ok(false, 'Should have thrown');
} catch (err) {
test.ok(err.message.includes('Rename failed'));
}
test.done();
};
module.exports['Commands: rename closes mailbox when renaming current mailbox'] = async test => {
let closeCalled = false;
let execCmd = null;
const connection = createMockConnection({
state: 3, // SELECTED
mailbox: { path: 'OldFolder' },
run: async cmd => {
if (cmd === 'CLOSE') {
closeCalled = true;
}
},
exec: async cmd => {
execCmd = cmd;
return { next: () => {} };
}
});
const result = await renameCommand(connection, 'OldFolder', 'NewFolder');
test.ok(closeCalled, 'CLOSE should be called');
test.equal(execCmd, 'RENAME');
test.ok(result);
test.equal(result.path, 'OldFolder');
test.equal(result.newPath, 'NewFolder');
test.done();
};
module.exports['Commands: rename does not close when renaming different mailbox'] = async test => {
let closeCalled = false;
const connection = createMockConnection({
state: 3, // SELECTED
mailbox: { path: 'INBOX' },
run: async cmd => {
if (cmd === 'CLOSE') {
closeCalled = true;
}
},
exec: async () => ({ next: () => {} })
});
const result = await renameCommand(connection, 'OtherFolder', 'NewName');
test.ok(!closeCalled, 'CLOSE should not be called');
test.ok(result);
test.done();
};
module.exports['Commands: rename works in SELECTED state'] = async test => {
let execCmd = null;
const connection = createMockConnection({
state: 3, // SELECTED
mailbox: { path: 'INBOX' },
exec: async cmd => {
execCmd = cmd;
return { next: () => {} };
}
});
const result = await renameCommand(connection, 'SomeFolder', 'NewName');
test.ok(result);
test.equal(execCmd, 'RENAME');
test.done();
};
module.exports['Commands: rename error with serverResponseCode'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async () => {
const err = new Error('Rename failed');
err.response = {
tag: '*',
command: 'NO',
attributes: [
{
type: 'SECTION',
section: [{ type: 'ATOM', value: 'NONEXISTENT' }]
},
{ type: 'TEXT', value: 'Mailbox does not exist' }
]
};
throw err;
}
});
try {
await renameCommand(connection, 'NonExistent', 'NewName');
test.ok(false, 'Should have thrown');
} catch (err) {
test.equal(err.serverResponseCode, 'NONEXISTENT');
}
test.done();
};
module.exports['Commands: rename normalizes paths'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 2,
exec: async (cmd, args) => {
execArgs = args;
return { next: () => {} };
}
});
const result = await renameCommand(connection, 'INBOX/Old', 'INBOX/New');
test.ok(result);
test.equal(result.path, 'INBOX/Old');
test.equal(result.newPath, 'INBOX/New');
test.ok(execArgs);
test.equal(execArgs.length, 2);
test.done();
};
// ============================================
// SUBSCRIBE/UNSUBSCRIBE Command Tests
// ============================================
const subscribeCommand = require('../lib/commands/subscribe');
const unsubscribeCommand = require('../lib/commands/unsubscribe');
module.exports['Commands: subscribe success'] = async test => {
let execCmd = null;
const connection = createMockConnection({
state: 2,
exec: async cmd => {
execCmd = cmd;
return { next: () => {} };
}
});
const result = await subscribeCommand(connection, 'Folder');
test.equal(result, true);
test.equal(execCmd, 'SUBSCRIBE');
test.done();
};
module.exports['Commands: subscribe skips when not authenticated'] = async test => {
const connection = createMockConnection({ state: 1 });
const result = await subscribeCommand(connection, 'Folder');
test.equal(result, undefined);
test.done();
};
module.exports['Commands: unsubscribe success'] = async test => {
let execCmd = null;
const connection = createMockConnection({
state: 2,
exec: async cmd => {
execCmd = cmd;
return { next: () => {} };
}
});
const result = await unsubscribeCommand(connection, 'Folder');
test.equal(result, true);
test.equal(execCmd, 'UNSUBSCRIBE');
test.done();
};
module.exports['Commands: unsubscribe skips when not authenticated'] = async test => {
const connection = createMockConnection({ state: 1 });
const result = await unsubscribeCommand(connection, 'Folder');
test.equal(result, undefined);
test.done();
};
module.exports['Commands: subscribe works in SELECTED state'] = async test => {
let execCmd = null;
const connection = createMockConnection({
state: 3, // SELECTED
exec: async cmd => {
execCmd = cmd;
return { next: () => {} };
}
});
const result = await subscribeCommand(connection, 'Folder');
test.equal(result, true);
test.equal(execCmd, 'SUBSCRIBE');
test.done();
};
module.exports['Commands: subscribe returns false on error'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async () => {
const err = new Error('Subscribe failed');
err.response = {
tag: '*',
command: 'NO',
attributes: [{ type: 'TEXT', value: 'Subscribe failed' }]
};
throw err;
}
});
const result = await subscribeCommand(connection, 'Folder');
test.equal(result, false);
test.done();
};
module.exports['Commands: subscribe error with serverResponseCode'] = async test => {
let capturedErr = null;
const connection = createMockConnection({
state: 2,
exec: async () => {
const err = new Error('Subscribe failed');
err.response = {
tag: '*',
command: 'NO',
attributes: [
{
type: 'SECTION',
section: [{ type: 'ATOM', value: 'NONEXISTENT' }]
},
{ type: 'TEXT', value: 'Mailbox does not exist' }
]
};
throw err;
},
log: {
warn: data => {
capturedErr = data.err;
}
}
});
const result = await subscribeCommand(connection, 'NonExistent');
test.equal(result, false);
test.ok(capturedErr);
test.equal(capturedErr.serverResponseCode, 'NONEXISTENT');
test.done();
};
module.exports['Commands: subscribe normalizes path'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 2,
exec: async (cmd, args) => {
execArgs = args;
return { next: () => {} };
}
});
const result = await subscribeCommand(connection, 'INBOX/Subfolder');
test.equal(result, true);
test.ok(execArgs);
test.equal(execArgs.length, 1);
test.done();
};
module.exports['Commands: unsubscribe works in SELECTED state'] = async test => {
let execCmd = null;
const connection = createMockConnection({
state: 3, // SELECTED
exec: async cmd => {
execCmd = cmd;
return { next: () => {} };
}
});
const result = await unsubscribeCommand(connection, 'Folder');
test.equal(result, true);
test.equal(execCmd, 'UNSUBSCRIBE');
test.done();
};
module.exports['Commands: unsubscribe returns false on error'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async () => {
const err = new Error('Unsubscribe failed');
err.response = {
tag: '*',
command: 'NO',
attributes: [{ type: 'TEXT', value: 'Unsubscribe failed' }]
};
throw err;
}
});
const result = await unsubscribeCommand(connection, 'Folder');
test.equal(result, false);
test.done();
};
module.exports['Commands: unsubscribe error with serverResponseCode'] = async test => {
let capturedErr = null;
const connection = createMockConnection({
state: 2,
exec: async () => {
const err = new Error('Unsubscribe failed');
err.response = {
tag: '*',
command: 'NO',
attributes: [
{
type: 'SECTION',
section: [{ type: 'ATOM', value: 'NONEXISTENT' }]
},
{ type: 'TEXT', value: 'Mailbox does not exist' }
]
};
throw err;
},
log: {
warn: data => {
capturedErr = data.err;
}
}
});
const result = await unsubscribeCommand(connection, 'NonExistent');
test.equal(result, false);
test.ok(capturedErr);
test.equal(capturedErr.serverResponseCode, 'NONEXISTENT');
test.done();
};
module.exports['Commands: unsubscribe normalizes path'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 2,
exec: async (cmd, args) => {
execArgs = args;
return { next: () => {} };
}
});
const result = await unsubscribeCommand(connection, 'INBOX/Subfolder');
test.equal(result, true);
test.ok(execArgs);
test.equal(execArgs.length, 1);
test.done();
};
// ============================================
// ENABLE Command Tests
// ============================================
const enableCommand = require('../lib/commands/enable');
module.exports['Commands: enable success'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([
['ENABLE', true],
['CONDSTORE', true]
]),
exec: async () => ({ next: () => {} })
});
const result = await enableCommand(connection, ['CONDSTORE']);
// Returns Set of enabled extensions
test.ok(result instanceof Set);
test.done();
};
module.exports['Commands: enable skips when not authenticated'] = async test => {
const connection = createMockConnection({ state: 1 });
const result = await enableCommand(connection, ['CONDSTORE']);
test.equal(result, undefined);
test.done();
};
module.exports['Commands: enable skips when ENABLE not supported'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map() // No ENABLE capability
});
const result = await enableCommand(connection, ['CONDSTORE']);
test.equal(result, undefined);
test.done();
};
module.exports['Commands: enable handles error'] = async test => {
const connection = createMockConnection({
state: 2,
// Need to include CONDSTORE so the filter doesn't skip it
capabilities: new Map([
['ENABLE', true],
['CONDSTORE', true]
]),
exec: async () => {
throw new Error('Enable failed');
}
});
const result = await enableCommand(connection, ['CONDSTORE']);
test.equal(result, false);
test.done();
};
// ============================================
// COMPRESS Command Tests
// ============================================
const compressCommand = require('../lib/commands/compress');
module.exports['Commands: compress success'] = async test => {
let execCalled = false;
const connection = createMockConnection({
capabilities: new Map([['COMPRESS=DEFLATE', true]]),
exec: async () => {
execCalled = true;
return { next: () => {} };
}
});
const result = await compressCommand(connection);
test.equal(result, true);
test.equal(execCalled, true);
test.done();
};
module.exports['Commands: compress skips when not supported'] = async test => {
const connection = createMockConnection({
capabilities: new Map() // No COMPRESS=DEFLATE
});
const result = await compressCommand(connection);
// Returns false when not supported (not undefined)
test.equal(result, false);
test.done();
};
module.exports['Commands: compress handles error'] = async test => {
const connection = createMockConnection({
capabilities: new Map([['COMPRESS=DEFLATE', true]]),
exec: async () => {
throw new Error('Compress failed');
}
});
const result = await compressCommand(connection);
test.equal(result, false);
test.done();
};
// ============================================
// STARTTLS Command Tests
// ============================================
const starttlsCommand = require('../lib/commands/starttls');
module.exports['Commands: starttls success'] = async test => {
let execCalled = false;
const connection = createMockConnection({
capabilities: new Map([['STARTTLS', true]]),
exec: async () => {
execCalled = true;
return { next: () => {} };
}
});
const result = await starttlsCommand(connection);
test.equal(result, true);
test.equal(execCalled, true);
test.done();
};
module.exports['Commands: starttls skips when not supported'] = async test => {
const connection = createMockConnection({
capabilities: new Map() // No STARTTLS
});
const result = await starttlsCommand(connection);
// Returns false when not supported (not undefined)
test.equal(result, false);
test.done();
};
module.exports['Commands: starttls handles error'] = async test => {
const connection = createMockConnection({
capabilities: new Map([['STARTTLS', true]]),
exec: async () => {
throw new Error('STARTTLS failed');
}
});
const result = await starttlsCommand(connection);
test.equal(result, false);
test.done();
};
// ============================================
// FETCH Command Tests
// ============================================
const fetchCommand = require('../lib/commands/fetch');
module.exports['Commands: fetch basic query'] = async test => {
let execCalled = false;
let execCommand = '';
const connection = createMockConnection({
state: 3, // SELECTED
exec: async (cmd, attrs, opts) => {
execCalled = true;
execCommand = cmd;
// Simulate a FETCH response
if (opts && opts.untagged && opts.untagged.FETCH) {
await opts.untagged.FETCH({
command: '1',
attributes: [
{ value: '1' },
[{ type: 'ATOM', value: 'UID' }, { type: 'ATOM', value: '100' }, { type: 'ATOM', value: 'FLAGS' }, [{ value: '\\Seen' }]]
]
});
}
return { next: () => {} };
}
});
const result = await fetchCommand(connection, '1:*', { uid: true, flags: true });
test.equal(execCalled, true);
test.equal(execCommand, 'FETCH');
test.ok(result);
test.equal(result.count, 1);
test.ok(Array.isArray(result.list));
test.done();
};
module.exports['Commands: fetch with UID option'] = async test => {
let execCommand = '';
const connection = createMockConnection({
state: 3,
exec: async cmd => {
execCommand = cmd;
return { next: () => {} };
}
});
await fetchCommand(connection, '1:*', { uid: true }, { uid: true });
test.equal(execCommand, 'UID FETCH');
test.done();
};
module.exports['Commands: fetch skips when not selected'] = async test => {
const connection = createMockConnection({ state: 2 }); // AUTHENTICATED, not SELECTED
const result = await fetchCommand(connection, '1:*', { uid: true });
test.equal(result, undefined);
test.done();
};
module.exports['Commands: fetch skips when no range'] = async test => {
const connection = createMockConnection({ state: 3 });
const result = await fetchCommand(connection, null, { uid: true });
test.equal(result, undefined);
test.done();
};
module.exports['Commands: fetch with envelope query'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
await fetchCommand(connection, '1', { envelope: true });
test.ok(queryAttrs);
// Check that ENVELOPE is in the query
const hasEnvelope = JSON.stringify(queryAttrs).includes('ENVELOPE');
test.ok(hasEnvelope);
test.done();
};
module.exports['Commands: fetch with bodyStructure query'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
await fetchCommand(connection, '1', { bodyStructure: true });
test.ok(queryAttrs);
const hasBODYSTRUCTURE = JSON.stringify(queryAttrs).includes('BODYSTRUCTURE');
test.ok(hasBODYSTRUCTURE);
test.done();
};
module.exports['Commands: fetch with size query'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
await fetchCommand(connection, '1', { size: true });
test.ok(queryAttrs);
const hasRFC822SIZE = JSON.stringify(queryAttrs).includes('RFC822.SIZE');
test.ok(hasRFC822SIZE);
test.done();
};
module.exports['Commands: fetch with source query'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
await fetchCommand(connection, '1', { source: true });
test.ok(queryAttrs);
const hasBODYPEEK = JSON.stringify(queryAttrs).includes('BODY.PEEK');
test.ok(hasBODYPEEK);
test.done();
};
module.exports['Commands: fetch with source partial'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
await fetchCommand(connection, '1', { source: { start: 0, maxLength: 1024 } });
test.ok(queryAttrs);
// Partial should be set
const queryStr = JSON.stringify(queryAttrs);
test.ok(queryStr.includes('BODY.PEEK'));
test.done();
};
module.exports['Commands: fetch with BINARY capability'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['BINARY', true]]),
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
await fetchCommand(connection, '1', { source: true }, { binary: true });
test.ok(queryAttrs);
const hasBINARYPEEK = JSON.stringify(queryAttrs).includes('BINARY.PEEK');
test.ok(hasBINARYPEEK);
test.done();
};
module.exports['Commands: fetch with OBJECTID capability'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['OBJECTID', true]]),
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
await fetchCommand(connection, '1', { flags: true });
test.ok(queryAttrs);
const hasEMAILID = JSON.stringify(queryAttrs).includes('EMAILID');
test.ok(hasEMAILID);
test.done();
};
module.exports['Commands: fetch with X-GM-EXT-1 capability'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['X-GM-EXT-1', true]]),
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
await fetchCommand(connection, '1', { flags: true });
test.ok(queryAttrs);
const hasXGMMSGID = JSON.stringify(queryAttrs).includes('X-GM-MSGID');
test.ok(hasXGMMSGID);
test.done();
};
module.exports['Commands: fetch with threadId query'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['OBJECTID', true]]),
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
await fetchCommand(connection, '1', { threadId: true });
test.ok(queryAttrs);
const hasTHREADID = JSON.stringify(queryAttrs).includes('THREADID');
test.ok(hasTHREADID);
test.done();
};
module.exports['Commands: fetch with threadId and X-GM-EXT-1 fallback'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['X-GM-EXT-1', true]]), // No OBJECTID, but has X-GM-EXT-1
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
await fetchCommand(connection, '1', { threadId: true });
test.ok(queryAttrs);
const hasXGMTHRID = JSON.stringify(queryAttrs).includes('X-GM-THRID');
test.ok(hasXGMTHRID, 'Should use X-GM-THRID as fallback for threadId');
test.done();
};
module.exports['Commands: fetch with labels query'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['X-GM-EXT-1', true]]),
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
await fetchCommand(connection, '1', { labels: true });
test.ok(queryAttrs);
const hasXGMLABELS = JSON.stringify(queryAttrs).includes('X-GM-LABELS');
test.ok(hasXGMLABELS);
test.done();
};
module.exports['Commands: fetch with CONDSTORE enabled'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
enabled: new Set(['CONDSTORE']),
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
await fetchCommand(connection, '1', { flags: true });
test.ok(queryAttrs);
const hasMODSEQ = JSON.stringify(queryAttrs).includes('MODSEQ');
test.ok(hasMODSEQ);
test.done();
};
module.exports['Commands: fetch with headers array'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
await fetchCommand(connection, '1', { headers: ['Subject', 'From', 'To'] });
test.ok(queryAttrs);
const queryStr = JSON.stringify(queryAttrs);
test.ok(queryStr.includes('HEADER.FIELDS'));
test.done();
};
module.exports['Commands: fetch with headers true'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
await fetchCommand(connection, '1', { headers: true });
test.ok(queryAttrs);
const queryStr = JSON.stringify(queryAttrs);
test.ok(queryStr.includes('HEADER'));
test.done();
};
module.exports['Commands: fetch with bodyParts'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
await fetchCommand(connection, '1', { bodyParts: ['1', '2'] });
test.ok(queryAttrs);
const queryStr = JSON.stringify(queryAttrs);
test.ok(queryStr.includes('BODY.PEEK'));
test.done();
};
module.exports['Commands: fetch with bodyParts object'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
await fetchCommand(connection, '1', { bodyParts: [{ key: '1', start: 0, maxLength: 100 }] });
test.ok(queryAttrs);
const queryStr = JSON.stringify(queryAttrs);
test.ok(queryStr.includes('BODY.PEEK'));
test.done();
};
module.exports['Commands: fetch with bodyParts skips invalid'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
// Invalid entries: null, object without key, number
await fetchCommand(connection, '1', { bodyParts: [null, { noKey: true }, 123, '1'] });
test.ok(queryAttrs);
// Should still work - just skips invalid entries
test.done();
};
module.exports['Commands: fetch with changedSince'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
enabled: new Set(['CONDSTORE']),
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
await fetchCommand(connection, '1', { flags: true }, { changedSince: '12345' });
test.ok(queryAttrs);
const queryStr = JSON.stringify(queryAttrs);
test.ok(queryStr.includes('CHANGEDSINCE'));
test.done();
};
module.exports['Commands: fetch with changedSince and QRESYNC'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
enabled: new Set(['CONDSTORE', 'QRESYNC']),
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
await fetchCommand(connection, '1', { flags: true }, { changedSince: '12345', uid: true });
test.ok(queryAttrs);
const queryStr = JSON.stringify(queryAttrs);
test.ok(queryStr.includes('VANISHED'));
test.done();
};
module.exports['Commands: fetch with onUntaggedFetch callback'] = async test => {
let callbackCalled = false;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.FETCH) {
await opts.untagged.FETCH({
command: '1',
attributes: [
{ value: '1' },
[
{ type: 'ATOM', value: 'UID' },
{ type: 'ATOM', value: '100' }
]
]
});
}
return { next: () => {} };
}
});
await fetchCommand(
connection,
'1',
{ uid: true },
{
onUntaggedFetch: (msg, done) => {
callbackCalled = true;
done();
}
}
);
test.equal(callbackCalled, true);
test.done();
};
module.exports['Commands: fetch callback error propagates'] = async test => {
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.FETCH) {
await opts.untagged.FETCH({
command: '1',
attributes: [
{ value: '1' },
[
{ type: 'ATOM', value: 'UID' },
{ type: 'ATOM', value: '100' }
]
]
});
}
return { next: () => {} };
}
});
try {
await fetchCommand(
connection,
'1',
{ uid: true },
{
onUntaggedFetch: (msg, done) => {
done(new Error('Callback error'));
}
}
);
test.ok(false, 'Should have thrown');
} catch (err) {
test.equal(err.message, 'Callback error');
}
test.done();
};
module.exports['Commands: fetch handles error'] = async test => {
const connection = createMockConnection({
state: 3,
exec: async () => {
throw new Error('Fetch failed');
}
});
try {
await fetchCommand(connection, '1', { uid: true });
test.ok(false, 'Should have thrown');
} catch (err) {
test.equal(err.message, 'Fetch failed');
}
test.done();
};
module.exports['Commands: fetch retries on throttle error'] = async test => {
let attempts = 0;
const connection = createMockConnection({
state: 3,
exec: async () => {
attempts++;
if (attempts < 3) {
const err = new Error('Throttled');
err.code = 'ETHROTTLE';
err.throttleReset = 10; // 10ms for testing
throw err;
}
return { next: () => {} };
}
});
const result = await fetchCommand(connection, '1', { uid: true });
test.ok(result);
test.equal(attempts, 3);
test.done();
};
module.exports['Commands: fetch with all/fast/full query'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
await fetchCommand(connection, '1', { all: true, fast: true, full: true, internalDate: true });
test.ok(queryAttrs);
const queryStr = JSON.stringify(queryAttrs);
test.ok(queryStr.includes('ALL'));
test.ok(queryStr.includes('FAST'));
test.ok(queryStr.includes('FULL'));
test.ok(queryStr.includes('INTERNALDATE'));
test.done();
};
// ============================================
// LIST Command Tests
// ============================================
const listCommand = require('../lib/commands/list');
module.exports['Commands: list basic'] = async test => {
let execCalled = false;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs, opts) => {
execCalled = true;
// Simulate LIST response
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'INBOX' }]
});
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*');
test.equal(execCalled, true);
test.ok(Array.isArray(result));
test.done();
};
module.exports['Commands: list with XLIST capability'] = async test => {
let usedListCommand = '';
const connection = createMockConnection({
state: 3,
capabilities: new Map([['XLIST', true]]),
exec: async (cmd, attrs, opts) => {
// Capture the first LIST/XLIST command, not LSUB
if ((cmd === 'LIST' || cmd === 'XLIST') && !usedListCommand) {
usedListCommand = cmd;
}
if (opts && opts.untagged && opts.untagged[cmd]) {
await opts.untagged[cmd]({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'INBOX' }]
});
}
return { next: () => {} };
}
});
await listCommand(connection, '', '*');
test.equal(usedListCommand, 'XLIST');
test.done();
};
module.exports['Commands: list prefers LIST over XLIST when SPECIAL-USE available'] = async test => {
let usedListCommand = '';
const connection = createMockConnection({
state: 3,
capabilities: new Map([
['XLIST', true],
['SPECIAL-USE', true]
]),
exec: async (cmd, attrs, opts) => {
// Capture the first LIST/XLIST command, not LSUB
if ((cmd === 'LIST' || cmd === 'XLIST') && !usedListCommand) {
usedListCommand = cmd;
}
if (opts && opts.untagged && opts.untagged[cmd]) {
await opts.untagged[cmd]({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'INBOX' }]
});
}
return { next: () => {} };
}
});
await listCommand(connection, '', '*');
test.equal(usedListCommand, 'LIST');
test.done();
};
module.exports['Commands: list with statusQuery'] = async test => {
let listAttrs = null;
const connection = createMockConnection({
state: 3,
capabilities: new Map([
['LIST-STATUS', true],
['SPECIAL-USE', true]
]),
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST') {
listAttrs = attrs;
if (opts && opts.untagged && opts.untagged.LIST) {
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'INBOX' }]
});
}
if (opts && opts.untagged && opts.untagged.STATUS) {
await opts.untagged.STATUS({
attributes: [{ value: 'INBOX' }, [{ value: 'MESSAGES' }, { value: '10' }, { value: 'UNSEEN' }, { value: '5' }]]
});
}
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*', {
statusQuery: { messages: true, unseen: true }
});
test.ok(listAttrs);
const attrsStr = JSON.stringify(listAttrs);
test.ok(attrsStr.includes('RETURN'));
test.ok(attrsStr.includes('STATUS'));
test.ok(Array.isArray(result));
test.done();
};
module.exports['Commands: list with CONDSTORE status query'] = async test => {
let listAttrs = null;
const connection = createMockConnection({
state: 3,
capabilities: new Map([
['LIST-STATUS', true],
['CONDSTORE', true]
]),
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST') {
listAttrs = attrs;
if (opts && opts.untagged && opts.untagged.LIST) {
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'INBOX' }]
});
}
}
return { next: () => {} };
}
});
await listCommand(connection, '', '*', {
statusQuery: { highestModseq: true }
});
test.ok(listAttrs);
const attrsStr = JSON.stringify(listAttrs);
test.ok(attrsStr.includes('HIGHESTMODSEQ'));
test.done();
};
module.exports['Commands: list with listOnly option'] = async test => {
let lsubCalled = false;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs, opts) => {
if (cmd === 'LSUB') {
lsubCalled = true;
}
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'INBOX' }]
});
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*', { listOnly: true });
test.equal(lsubCalled, false);
test.ok(Array.isArray(result));
test.done();
};
module.exports['Commands: list with specialUseHints'] = async test => {
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'Sent Items' }]
});
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*', {
specialUseHints: { sent: 'Sent Items' }
});
test.ok(Array.isArray(result));
// The Sent Items folder should have specialUse set
const sentFolder = result.find(e => e.path === 'Sent Items');
test.ok(sentFolder);
test.equal(sentFolder.specialUse, '\\Sent');
test.done();
};
module.exports['Commands: list handles INBOX specially'] = async test => {
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs, opts) => {
if ((cmd === 'LIST' || cmd === 'LSUB') && opts && opts.untagged) {
const handler = opts.untagged[cmd];
if (handler) {
await handler({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'INBOX' }]
});
}
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*');
const inbox = result.find(e => e.path === 'INBOX');
test.ok(inbox);
test.equal(inbox.specialUse, '\\Inbox');
// INBOX should always be subscribed
test.equal(inbox.subscribed, true);
test.done();
};
module.exports['Commands: list runs separate INBOX query when using namespace'] = async test => {
let listCalls = 0;
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST') {
listCalls++;
if (opts && opts.untagged && opts.untagged.LIST) {
// First call is for the namespace, second for INBOX
if (listCalls === 1) {
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'INBOX/Subfolder' }]
});
} else {
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'INBOX' }]
});
}
}
}
return { next: () => {} };
}
});
await listCommand(connection, 'INBOX/', '*');
// Should have called LIST twice - once for namespace, once for INBOX
test.equal(listCalls, 2);
test.done();
};
module.exports['Commands: list handles LSUB merging'] = async test => {
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'Folder1' }]
});
}
if (cmd === 'LSUB' && opts && opts.untagged && opts.untagged.LSUB) {
await opts.untagged.LSUB({
attributes: [[{ value: '\\Subscribed' }], { value: '/' }, { value: 'Folder1' }]
});
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*');
const folder = result.find(e => e.path === 'Folder1');
test.ok(folder);
test.equal(folder.subscribed, true);
test.equal(folder.listed, true);
test.done();
};
module.exports['Commands: list handles error'] = async test => {
const connection = createMockConnection({
state: 3,
exec: async () => {
throw new Error('List failed');
}
});
try {
await listCommand(connection, '', '*');
test.ok(false, 'Should have thrown');
} catch (err) {
test.equal(err.message, 'List failed');
}
test.done();
};
module.exports['Commands: list handles empty attributes'] = async test => {
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
// Empty attributes - should be skipped
await opts.untagged.LIST({ attributes: [] });
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*');
test.ok(Array.isArray(result));
test.equal(result.length, 0);
test.done();
};
module.exports['Commands: list status fallback when LIST-STATUS not supported'] = async test => {
let statusCalls = 0;
const connection = createMockConnection({
state: 3,
capabilities: new Map(), // No LIST-STATUS
// eslint-disable-next-line no-unused-vars
run: async (cmd, path, query) => {
if (cmd === 'STATUS') {
statusCalls++;
return { messages: 10, unseen: 5, path };
}
},
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'INBOX' }]
});
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*', {
statusQuery: { messages: true, unseen: true }
});
test.ok(Array.isArray(result));
// STATUS should have been called for each folder
test.equal(statusCalls, 1);
test.done();
};
module.exports['Commands: list handles STATUS errors gracefully'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map(),
run: async cmd => {
if (cmd === 'STATUS') {
throw new Error('Status failed');
}
},
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'INBOX' }]
});
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*', {
statusQuery: { messages: true }
});
const inbox = result.find(e => e.path === 'INBOX');
test.ok(inbox);
// Status should have error property
test.ok(inbox.status);
test.ok(inbox.status.error);
test.done();
};
module.exports['Commands: list sorts by special use'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['SPECIAL-USE', true]]),
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
// Add folders out of order
await opts.untagged.LIST({
attributes: [[{ value: '\\Trash' }], { value: '/' }, { value: 'Trash' }]
});
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'INBOX' }]
});
await opts.untagged.LIST({
attributes: [[{ value: '\\Sent' }], { value: '/' }, { value: 'Sent' }]
});
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*');
// INBOX should be first (has \\Inbox special use)
test.equal(result[0].specialUse, '\\Inbox');
test.done();
};
module.exports['Commands: list handles delimiter in path'] = async test => {
const connection = createMockConnection({
state: 3,
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
await opts.untagged.LIST({
attributes: [
[{ value: '\\HasNoChildren' }],
{ value: '/' },
{ value: '/Leading/Slash' } // Path starts with delimiter
]
});
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*');
const folder = result.find(e => e.name === 'Slash');
test.ok(folder);
// Leading delimiter should be removed
test.equal(folder.path, 'Leading/Slash');
test.done();
};
module.exports['Commands: list skips Noselect folders for status'] = async test => {
let statusCalls = 0;
const connection = createMockConnection({
state: 3,
capabilities: new Map(),
run: async cmd => {
if (cmd === 'STATUS') {
statusCalls++;
return { messages: 10 };
}
},
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
await opts.untagged.LIST({
attributes: [[{ value: '\\Noselect' }], { value: '/' }, { value: 'Parent' }]
});
}
return { next: () => {} };
}
});
await listCommand(connection, '', '*', { statusQuery: { messages: true } });
// STATUS should not be called for Noselect folders
test.equal(statusCalls, 0);
test.done();
};
module.exports['Commands: list XLIST removes Inbox flag from non-INBOX'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['XLIST', true]]),
exec: async (cmd, attrs, opts) => {
if (cmd === 'XLIST' && opts && opts.untagged && opts.untagged.XLIST) {
// XLIST may have localised inbox name with \\Inbox flag
await opts.untagged.XLIST({
attributes: [
[{ value: '\\Inbox' }, { value: '\\HasNoChildren' }],
{ value: '/' },
{ value: 'Posteingang' } // German for inbox
]
});
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*');
const folder = result.find(e => e.path === 'Posteingang');
test.ok(folder);
// \\Inbox flag should be removed from flags set
test.equal(folder.flags.has('\\Inbox'), false);
// But it should have \\Inbox special use
test.equal(folder.specialUse, '\\Inbox');
test.done();
};
module.exports['Commands: list LSUB path with leading delimiter'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['SPECIAL-USE', true]]),
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'Folder1' }]
});
}
if (cmd === 'LSUB' && opts && opts.untagged && opts.untagged.LSUB) {
// LSUB returns path with leading delimiter
await opts.untagged.LSUB({
attributes: [
[{ value: '\\Subscribed' }],
{ value: '/' },
{ value: '/Folder1' } // Leading delimiter
]
});
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*');
const folder = result.find(e => e.path === 'Folder1');
test.ok(folder);
test.equal(folder.subscribed, true);
test.done();
};
module.exports['Commands: list sorts non-special-use after special-use'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['SPECIAL-USE', true]]),
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
// Regular folder first
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'ZFolder' }]
});
// Then INBOX (special use)
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'INBOX' }]
});
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*');
// INBOX (special use) should come before ZFolder (no special use)
const inboxIndex = result.findIndex(e => e.path === 'INBOX');
const zFolderIndex = result.findIndex(e => e.path === 'ZFolder');
test.ok(inboxIndex < zFolderIndex, 'Special use folders should sort before non-special-use');
test.done();
};
module.exports['Commands: list sorts alphabetically when no special use'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['SPECIAL-USE', true]]),
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
// Folders without special use in reverse alphabetical order
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'Zebra' }]
});
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'Alpha' }]
});
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'Middle' }]
});
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*');
// Should be sorted alphabetically
const alphaIndex = result.findIndex(e => e.path === 'Alpha');
const middleIndex = result.findIndex(e => e.path === 'Middle');
const zebraIndex = result.findIndex(e => e.path === 'Zebra');
test.ok(alphaIndex < middleIndex, 'Alpha should come before Middle');
test.ok(middleIndex < zebraIndex, 'Middle should come before Zebra');
test.done();
};
module.exports['Commands: list sorts nested folders by parent path'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['SPECIAL-USE', true]]),
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
// Nested folders
await opts.untagged.LIST({
attributes: [[{ value: '\\HasChildren' }], { value: '/' }, { value: 'B' }]
});
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'B/Nested' }]
});
await opts.untagged.LIST({
attributes: [[{ value: '\\HasChildren' }], { value: '/' }, { value: 'A' }]
});
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'A/Nested' }]
});
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*');
// A folders should come before B folders
const aIndex = result.findIndex(e => e.path === 'A');
const aNestedIndex = result.findIndex(e => e.path === 'A/Nested');
const bIndex = result.findIndex(e => e.path === 'B');
const bNestedIndex = result.findIndex(e => e.path === 'B/Nested');
test.ok(aIndex < bIndex, 'A should come before B');
test.ok(aNestedIndex < bIndex, 'A/Nested should come before B');
test.ok(bIndex < bNestedIndex || aNestedIndex < bNestedIndex, 'Parent folders sort correctly');
test.done();
};
module.exports['Commands: list handles LSUB with empty attributes'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['SPECIAL-USE', true]]),
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'TestFolder' }]
});
}
if (cmd === 'LSUB' && opts && opts.untagged && opts.untagged.LSUB) {
// Empty attributes
await opts.untagged.LSUB({
attributes: []
});
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*');
test.ok(result.length >= 1);
test.done();
};
module.exports['Commands: list handles STATUS NaN values in LSUB response'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([
['SPECIAL-USE', true],
['LIST-STATUS', true]
]),
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST' && opts && opts.untagged) {
if (opts.untagged.LIST) {
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'TestFolder' }]
});
}
if (opts.untagged.STATUS) {
await opts.untagged.STATUS({
attributes: [
{ value: 'TestFolder' },
[
{ value: 'MESSAGES' },
{ value: 'NaN' }, // Invalid number
{ value: 'RECENT' },
{ value: 'invalid' } // Invalid value
]
]
});
}
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*', { statusQuery: { messages: true, recent: true } });
const folder = result.find(e => e.path === 'TestFolder');
test.ok(folder);
// NaN values should be filtered out (value === false check)
test.equal(folder.status.messages, undefined);
test.equal(folder.status.recent, undefined);
test.done();
};
module.exports['Commands: list STATUS parses UIDVALIDITY UNSEEN HIGHESTMODSEQ'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([
['LIST-STATUS', true],
['CONDSTORE', true]
]),
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST') {
if (opts && opts.untagged && opts.untagged.LIST) {
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'TestFolder' }]
});
}
if (opts && opts.untagged && opts.untagged.STATUS) {
await opts.untagged.STATUS({
attributes: [
{ value: 'TestFolder' },
[
{ value: 'UIDVALIDITY' },
{ value: '123456789' },
{ value: 'UNSEEN' },
{ value: '42' },
{ value: 'HIGHESTMODSEQ' },
{ value: '999999999' }
]
]
});
}
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*', {
statusQuery: { uidValidity: true, unseen: true, highestModseq: true }
});
const folder = result.find(e => e.path === 'TestFolder');
test.ok(folder);
test.ok(folder.status);
test.equal(folder.status.uidValidity, BigInt(123456789));
test.equal(folder.status.unseen, 42);
test.equal(folder.status.highestModseq, BigInt(999999999));
test.done();
};
module.exports['Commands: list LSUB folder not in LIST entries'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['SPECIAL-USE', true]]),
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
// Only return INBOX in LIST
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'INBOX' }]
});
}
if (cmd === 'LSUB' && opts && opts.untagged && opts.untagged.LSUB) {
// Return a folder in LSUB that wasn't in LIST (hits else branch)
await opts.untagged.LSUB({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'SubscribedOnly' }]
});
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*');
// The subscribed-only folder should not be in results (else branch ignores it)
const subscribedOnly = result.find(e => e.path === 'SubscribedOnly');
test.equal(subscribedOnly, undefined);
// INBOX should still be there
const inbox = result.find(e => e.path === 'INBOX');
test.ok(inbox);
test.done();
};
module.exports['Commands: list sort b has specialUse a does not'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['SPECIAL-USE', true]]),
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
// First add a folder without special use
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'AAA_Regular' }]
});
// Then add INBOX which gets special use
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'INBOX' }]
});
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*');
// When sorting, INBOX has specialUse, AAA_Regular does not
// So the comparison should hit: !a.specialUse && b.specialUse returns 1
// This means INBOX should come first even though AAA_Regular is alphabetically first
test.ok(result.length >= 2);
test.equal(result[0].path, 'INBOX');
test.equal(result[0].specialUse, '\\Inbox');
test.done();
};
module.exports['Commands: list sort fallback path comparison'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['SPECIAL-USE', true]]),
exec: async (cmd, attrs, opts) => {
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
// Create folders where parent parts match but paths differ at the end
// A/B/C and A/B will have matching parts up to a point
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'Parent/Child/Deep' }]
});
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'Parent/Child' }]
});
}
return { next: () => {} };
}
});
const result = await listCommand(connection, '', '*');
// Parent/Child should come before Parent/Child/Deep
const childIndex = result.findIndex(e => e.path === 'Parent/Child');
const deepIndex = result.findIndex(e => e.path === 'Parent/Child/Deep');
test.ok(childIndex < deepIndex, 'Shorter path should sort before longer when parent matches');
test.done();
};
// ============================================
// SELECT Command Tests
// ============================================
const selectCommand = require('../lib/commands/select');
module.exports['Commands: select basic'] = async test => {
let execCalled = false;
let execCommand = '';
const connection = createMockConnection({
state: 2, // AUTHENTICATED
folders: new Map([['INBOX', { path: 'INBOX', delimiter: '/' }]]),
run: async () => [],
exec: async (cmd, attrs, opts) => {
execCalled = true;
execCommand = cmd;
// Simulate SELECT response
if (opts && opts.untagged) {
if (opts.untagged.FLAGS) {
await opts.untagged.FLAGS({
attributes: [[{ value: '\\Seen' }, { value: '\\Answered' }, { value: '\\Flagged' }]]
});
}
if (opts.untagged.EXISTS) {
await opts.untagged.EXISTS({ command: '100' });
}
if (opts.untagged.OK) {
await opts.untagged.OK({
attributes: [{ section: [{ type: 'ATOM', value: 'UIDVALIDITY' }, { value: '12345' }] }]
});
await opts.untagged.OK({
attributes: [{ section: [{ type: 'ATOM', value: 'UIDNEXT' }, { value: '1000' }] }]
});
await opts.untagged.OK({
attributes: [{ section: [{ type: 'ATOM', value: 'PERMANENTFLAGS' }, [{ value: '\\*' }]] }]
});
}
}
return {
next: () => {},
response: { attributes: [{ section: [{ type: 'ATOM', value: 'READ-WRITE' }] }] }
};
},
emit: () => {}
});
const result = await selectCommand(connection, 'INBOX');
test.equal(execCalled, true);
test.equal(execCommand, 'SELECT');
test.ok(result);
test.equal(result.path, 'INBOX');
test.equal(result.exists, 100);
test.equal(result.readOnly, false);
test.done();
};
module.exports['Commands: select with readOnly option uses EXAMINE'] = async test => {
let execCommand = '';
const connection = createMockConnection({
state: 2,
folders: new Map([['INBOX', { path: 'INBOX' }]]),
run: async () => [],
exec: async cmd => {
execCommand = cmd;
return {
next: () => {},
response: { attributes: [{ section: [{ type: 'ATOM', value: 'READ-ONLY' }] }] }
};
},
emit: () => {}
});
const result = await selectCommand(connection, 'INBOX', { readOnly: true });
test.equal(execCommand, 'EXAMINE');
test.equal(result.readOnly, true);
test.done();
};
module.exports['Commands: select skips when not authenticated'] = async test => {
const connection = createMockConnection({ state: 1 }); // NOT_AUTHENTICATED
const result = await selectCommand(connection, 'INBOX');
test.equal(result, undefined);
test.done();
};
module.exports['Commands: select fetches folder list if not cached'] = async test => {
let listCalled = false;
const connection = createMockConnection({
state: 2,
folders: new Map(), // Empty - will trigger LIST
run: async cmd => {
if (cmd === 'LIST') {
listCalled = true;
return [{ path: 'INBOX', delimiter: '/' }];
}
},
exec: async () => ({
next: () => {},
response: { attributes: [{ section: [{ type: 'ATOM', value: 'READ-WRITE' }] }] }
}),
emit: () => {}
});
await selectCommand(connection, 'INBOX');
test.equal(listCalled, true);
test.done();
};
module.exports['Commands: select throws when LIST fails'] = async test => {
const connection = createMockConnection({
state: 2,
folders: new Map(),
run: async () => null // LIST returns null
});
try {
await selectCommand(connection, 'INBOX');
test.ok(false, 'Should have thrown');
} catch (err) {
test.equal(err.message, 'Failed to fetch folders');
}
test.done();
};
module.exports['Commands: select with QRESYNC'] = async test => {
let execAttrs = null;
const connection = createMockConnection({
state: 2,
enabled: new Set(['QRESYNC']),
folders: new Map([['INBOX', { path: 'INBOX' }]]),
run: async () => [],
exec: async (cmd, attrs, opts) => {
execAttrs = attrs;
// Must return matching UIDVALIDITY and HIGHESTMODSEQ for QRESYNC to remain valid
if (opts && opts.untagged && opts.untagged.OK) {
await opts.untagged.OK({
attributes: [{ section: [{ type: 'ATOM', value: 'UIDVALIDITY' }, { value: '67890' }] }]
});
await opts.untagged.OK({
attributes: [{ section: [{ type: 'ATOM', value: 'HIGHESTMODSEQ' }, { value: '100' }] }]
});
}
return {
next: () => {},
response: { attributes: [{ section: [{ type: 'ATOM', value: 'READ-WRITE' }] }] }
};
},
emit: () => {},
untaggedVanished: async () => {},
untaggedFetch: async () => {}
});
const result = await selectCommand(connection, 'INBOX', {
changedSince: '12345',
uidValidity: BigInt(67890)
});
test.ok(execAttrs);
const attrsStr = JSON.stringify(execAttrs);
test.ok(attrsStr.includes('QRESYNC'));
test.equal(result.qresync, true);
test.done();
};
module.exports['Commands: select QRESYNC invalidated when UIDVALIDITY mismatch'] = async test => {
const connection = createMockConnection({
state: 2,
enabled: new Set(['QRESYNC']),
folders: new Map([['INBOX', { path: 'INBOX' }]]),
run: async () => [],
exec: async (cmd, attrs, opts) => {
// Return different UIDVALIDITY
if (opts && opts.untagged && opts.untagged.OK) {
await opts.untagged.OK({
attributes: [{ section: [{ type: 'ATOM', value: 'UIDVALIDITY' }, { value: '99999' }] }]
});
await opts.untagged.OK({
attributes: [{ section: [{ type: 'ATOM', value: 'HIGHESTMODSEQ' }, { value: '100' }] }]
});
}
return {
next: () => {},
response: { attributes: [{ section: [{ type: 'ATOM', value: 'READ-WRITE' }] }] }
};
},
emit: () => {}
});
const result = await selectCommand(connection, 'INBOX', {
changedSince: '12345',
uidValidity: BigInt(67890) // Different from server's 99999
});
// QRESYNC should be invalidated due to UIDVALIDITY mismatch
test.equal(result.qresync, false);
test.done();
};
module.exports['Commands: select QRESYNC invalidated when NOMODSEQ'] = async test => {
const connection = createMockConnection({
state: 2,
enabled: new Set(['QRESYNC']),
folders: new Map([['INBOX', { path: 'INBOX' }]]),
run: async () => [],
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.OK) {
await opts.untagged.OK({
attributes: [{ section: [{ type: 'ATOM', value: 'UIDVALIDITY' }, { value: '67890' }] }]
});
// NOMODSEQ present
await opts.untagged.OK({
attributes: [{ section: [{ type: 'ATOM', value: 'NOMODSEQ' }] }]
});
}
return {
next: () => {},
response: { attributes: [{ section: [{ type: 'ATOM', value: 'READ-WRITE' }] }] }
};
},
emit: () => {}
});
const result = await selectCommand(connection, 'INBOX', {
changedSince: '12345',
uidValidity: BigInt(67890)
});
test.equal(result.noModseq, true);
test.equal(result.qresync, false);
test.done();
};
module.exports['Commands: select parses HIGHESTMODSEQ'] = async test => {
const connection = createMockConnection({
state: 2,
folders: new Map([['INBOX', { path: 'INBOX' }]]),
run: async () => [],
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.OK) {
await opts.untagged.OK({
attributes: [{ section: [{ type: 'ATOM', value: 'HIGHESTMODSEQ' }, { value: '9876543210' }] }]
});
}
return {
next: () => {},
response: { attributes: [{ section: [{ type: 'ATOM', value: 'READ-WRITE' }] }] }
};
},
emit: () => {}
});
const result = await selectCommand(connection, 'INBOX');
test.equal(result.highestModseq, BigInt('9876543210'));
test.done();
};
module.exports['Commands: select parses MAILBOXID'] = async test => {
const connection = createMockConnection({
state: 2,
folders: new Map([['INBOX', { path: 'INBOX' }]]),
run: async () => [],
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.OK) {
await opts.untagged.OK({
attributes: [{ section: [{ type: 'ATOM', value: 'MAILBOXID' }, [{ value: 'abc123' }]] }]
});
}
return {
next: () => {},
response: { attributes: [{ section: [{ type: 'ATOM', value: 'READ-WRITE' }] }] }
};
},
emit: () => {}
});
const result = await selectCommand(connection, 'INBOX');
test.equal(result.mailboxId, 'abc123');
test.done();
};
module.exports['Commands: select emits mailboxOpen event'] = async test => {
let emittedEvents = [];
const connection = createMockConnection({
state: 2,
mailbox: false, // No current mailbox
folders: new Map([['INBOX', { path: 'INBOX' }]]),
run: async () => [],
exec: async () => ({
next: () => {},
response: { attributes: [{ section: [{ type: 'ATOM', value: 'READ-WRITE' }] }] }
}),
emit: event => {
emittedEvents.push(event);
}
});
await selectCommand(connection, 'INBOX');
test.ok(emittedEvents.includes('mailboxOpen'));
test.done();
};
module.exports['Commands: select emits mailboxClose when switching'] = async test => {
let emittedEvents = [];
const connection = createMockConnection({
state: 3, // Already SELECTED
mailbox: { path: 'OldFolder' },
folders: new Map([['INBOX', { path: 'INBOX' }]]),
run: async () => [],
exec: async () => ({
next: () => {},
response: { attributes: [{ section: [{ type: 'ATOM', value: 'READ-WRITE' }] }] }
}),
emit: event => {
emittedEvents.push(event);
}
});
await selectCommand(connection, 'INBOX');
test.ok(emittedEvents.includes('mailboxClose'));
test.ok(emittedEvents.includes('mailboxOpen'));
test.done();
};
module.exports['Commands: select handles error'] = async test => {
const connection = createMockConnection({
state: 2,
folders: new Map([['INBOX', { path: 'INBOX' }]]),
run: async () => [],
exec: async () => {
const err = new Error('Select failed');
err.response = { attributes: [] };
throw err;
},
emit: () => {}
});
try {
await selectCommand(connection, 'INBOX');
test.ok(false, 'Should have thrown');
} catch (err) {
test.equal(err.message, 'Select failed');
}
test.done();
};
module.exports['Commands: select resets state on error when SELECTED'] = async test => {
let emittedEvent = '';
const connection = createMockConnection({
state: 3, // SELECTED
mailbox: { path: 'CurrentFolder' },
folders: new Map([['INBOX', { path: 'INBOX' }]]),
run: async () => [],
exec: async () => {
const err = new Error('Select failed');
err.response = { attributes: [] };
throw err;
},
emit: event => {
emittedEvent = event;
}
});
try {
await selectCommand(connection, 'INBOX');
} catch (err) {
// Expected - error is intentionally ignored
err.expected = true;
}
test.equal(connection.state, 2); // Reset to AUTHENTICATED
test.equal(connection.mailbox, false);
test.equal(emittedEvent, 'mailboxClose');
test.done();
};
module.exports['Commands: select copies folder metadata'] = async test => {
const connection = createMockConnection({
state: 2,
folders: new Map([
[
'INBOX',
{
path: 'INBOX',
delimiter: '/',
specialUse: '\\Inbox',
subscribed: true,
listed: true
}
]
]),
run: async () => [],
exec: async () => ({
next: () => {},
response: { attributes: [{ section: [{ type: 'ATOM', value: 'READ-WRITE' }] }] }
}),
emit: () => {}
});
const result = await selectCommand(connection, 'INBOX');
test.equal(result.delimiter, '/');
test.equal(result.specialUse, '\\Inbox');
test.equal(result.subscribed, true);
test.equal(result.listed, true);
test.done();
};
module.exports['Commands: select handles VANISHED untagged'] = async test => {
let vanishedCalled = false;
const connection = createMockConnection({
state: 2,
enabled: new Set(['QRESYNC']),
folders: new Map([['INBOX', { path: 'INBOX' }]]),
run: async () => [],
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.VANISHED) {
await opts.untagged.VANISHED({ attributes: [] });
}
return {
next: () => {},
response: { attributes: [{ section: [{ type: 'ATOM', value: 'READ-WRITE' }] }] }
};
},
emit: () => {},
untaggedVanished: async () => {
vanishedCalled = true;
}
});
await selectCommand(connection, 'INBOX', { changedSince: '100', uidValidity: BigInt(123) });
test.equal(vanishedCalled, true);
test.done();
};
module.exports['Commands: select handles FETCH untagged'] = async test => {
let fetchCalled = false;
const connection = createMockConnection({
state: 2,
enabled: new Set(['QRESYNC']),
folders: new Map([['INBOX', { path: 'INBOX' }]]),
run: async () => [],
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.FETCH) {
await opts.untagged.FETCH({ command: '1', attributes: [] });
}
return {
next: () => {},
response: { attributes: [{ section: [{ type: 'ATOM', value: 'READ-WRITE' }] }] }
};
},
emit: () => {},
untaggedFetch: async () => {
fetchCalled = true;
}
});
await selectCommand(connection, 'INBOX', { changedSince: '100', uidValidity: BigInt(123) });
test.equal(fetchCalled, true);
test.done();
};
module.exports['Commands: select encodes path with special characters'] = async test => {
let execAttrs = null;
const connection = createMockConnection({
state: 2,
folders: new Map([['Test&Folder', { path: 'Test&Folder' }]]),
run: async () => [],
exec: async (cmd, attrs) => {
execAttrs = attrs;
return {
next: () => {},
response: { attributes: [{ section: [{ type: 'ATOM', value: 'READ-WRITE' }] }] }
};
},
emit: () => {}
});
await selectCommand(connection, 'Test&Folder');
// Path with & should use STRING type instead of ATOM
test.ok(execAttrs);
test.equal(execAttrs[0].type, 'STRING');
test.done();
};
module.exports['Commands: select handles empty OK attributes'] = async test => {
const connection = createMockConnection({
state: 2,
folders: new Map([['INBOX', { path: 'INBOX', delimiter: '/' }]]),
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.OK) {
// Empty attributes - should return early
await opts.untagged.OK({
attributes: []
});
}
if (opts && opts.untagged && opts.untagged.EXISTS) {
await opts.untagged.EXISTS({ command: '100' });
}
return {
next: () => {},
response: { attributes: [{ section: [{ type: 'ATOM', value: 'READ-WRITE' }] }] }
};
},
emit: () => {}
});
const result = await selectCommand(connection, 'INBOX');
test.ok(result);
test.done();
};
module.exports['Commands: select handles null FLAGS attributes'] = async test => {
const connection = createMockConnection({
state: 2,
folders: new Map([['INBOX', { path: 'INBOX', delimiter: '/' }]]),
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.FLAGS) {
// Null/undefined attributes - should return early
await opts.untagged.FLAGS({
attributes: null
});
}
if (opts && opts.untagged && opts.untagged.EXISTS) {
await opts.untagged.EXISTS({ command: '100' });
}
return {
next: () => {},
response: { attributes: [{ section: [{ type: 'ATOM', value: 'READ-WRITE' }] }] }
};
},
emit: () => {}
});
const result = await selectCommand(connection, 'INBOX');
test.ok(result);
test.equal(result.flags, undefined);
test.done();
};
module.exports['Commands: select handles NaN EXISTS'] = async test => {
const connection = createMockConnection({
state: 2,
folders: new Map([['INBOX', { path: 'INBOX', delimiter: '/' }]]),
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.EXISTS) {
// NaN command value - should return false
await opts.untagged.EXISTS({ command: 'invalid' });
}
return {
next: () => {},
response: { attributes: [{ section: [{ type: 'ATOM', value: 'READ-WRITE' }] }] }
};
},
emit: () => {}
});
const result = await selectCommand(connection, 'INBOX');
test.ok(result);
test.equal(result.exists, undefined);
test.done();
};
module.exports['Commands: select error with serverResponseCode'] = async test => {
const connection = createMockConnection({
state: 2,
folders: new Map([['INBOX', { path: 'INBOX', delimiter: '/' }]]),
exec: async () => {
const err = new Error('Select failed');
err.response = {
tag: 'A1',
command: 'NO',
attributes: [
{
type: 'SECTION',
section: [{ type: 'ATOM', value: 'NONEXISTENT' }]
},
{ type: 'TEXT', value: 'Mailbox does not exist' }
]
};
throw err;
},
emit: () => {}
});
try {
await selectCommand(connection, 'INBOX');
test.ok(false, 'Should have thrown');
} catch (err) {
test.equal(err.serverResponseCode, 'NONEXISTENT');
}
test.done();
};
// ============================================
// STATUS Command Tests
// ============================================
const statusCommand = require('../lib/commands/status');
module.exports['Commands: status basic'] = async test => {
let execCalled = false;
const connection = createMockConnection({
state: 2, // AUTHENTICATED
exec: async (cmd, attrs, opts) => {
execCalled = true;
if (opts && opts.untagged && opts.untagged.STATUS) {
await opts.untagged.STATUS({
attributes: [{ value: 'INBOX' }, [{ value: 'MESSAGES' }, { value: '100' }, { value: 'UNSEEN' }, { value: '10' }]]
});
}
return { next: () => {} };
}
});
const result = await statusCommand(connection, 'INBOX', { messages: true, unseen: true });
test.equal(execCalled, true);
test.ok(result);
test.equal(result.path, 'INBOX');
test.equal(result.messages, 100);
test.equal(result.unseen, 10);
test.done();
};
module.exports['Commands: status skips when not authenticated'] = async test => {
const connection = createMockConnection({ state: 1 }); // NOT_AUTHENTICATED
const result = await statusCommand(connection, 'INBOX', { messages: true });
test.equal(result, false);
test.done();
};
module.exports['Commands: status skips when no path'] = async test => {
const connection = createMockConnection({ state: 2 });
const result = await statusCommand(connection, '', { messages: true });
test.equal(result, false);
test.done();
};
module.exports['Commands: status skips when no query attributes'] = async test => {
const connection = createMockConnection({ state: 2 });
const result = await statusCommand(connection, 'INBOX', {});
test.equal(result, false);
test.done();
};
module.exports['Commands: status skips when all query values are false'] = async test => {
const connection = createMockConnection({ state: 2 });
const result = await statusCommand(connection, 'INBOX', { messages: false, unseen: false });
test.equal(result, false);
test.done();
};
module.exports['Commands: status with all standard query attributes'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 2,
exec: async (cmd, attrs, opts) => {
queryAttrs = attrs;
if (opts && opts.untagged && opts.untagged.STATUS) {
await opts.untagged.STATUS({
attributes: [
{ value: 'INBOX' },
[
{ value: 'MESSAGES' },
{ value: '100' },
{ value: 'RECENT' },
{ value: '5' },
{ value: 'UIDNEXT' },
{ value: '1000' },
{ value: 'UIDVALIDITY' },
{ value: '12345' },
{ value: 'UNSEEN' },
{ value: '10' }
]
]
});
}
return { next: () => {} };
}
});
const result = await statusCommand(connection, 'INBOX', {
messages: true,
recent: true,
uidNext: true,
uidValidity: true,
unseen: true
});
test.ok(queryAttrs);
const queryStr = JSON.stringify(queryAttrs);
test.ok(queryStr.includes('MESSAGES'));
test.ok(queryStr.includes('RECENT'));
test.ok(queryStr.includes('UIDNEXT'));
test.ok(queryStr.includes('UIDVALIDITY'));
test.ok(queryStr.includes('UNSEEN'));
test.equal(result.messages, 100);
test.equal(result.recent, 5);
test.equal(result.uidNext, 1000);
test.equal(result.uidValidity, BigInt(12345));
test.equal(result.unseen, 10);
test.done();
};
module.exports['Commands: status with HIGHESTMODSEQ and CONDSTORE'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 2,
capabilities: new Map([['CONDSTORE', true]]),
exec: async (cmd, attrs, opts) => {
queryAttrs = attrs;
if (opts && opts.untagged && opts.untagged.STATUS) {
await opts.untagged.STATUS({
attributes: [{ value: 'INBOX' }, [{ value: 'HIGHESTMODSEQ' }, { value: '9876543210' }]]
});
}
return { next: () => {} };
}
});
const result = await statusCommand(connection, 'INBOX', { highestModseq: true });
test.ok(queryAttrs);
const queryStr = JSON.stringify(queryAttrs);
test.ok(queryStr.includes('HIGHESTMODSEQ'));
test.equal(result.highestModseq, BigInt('9876543210'));
test.done();
};
module.exports['Commands: status ignores HIGHESTMODSEQ without CONDSTORE'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map(), // No CONDSTORE
exec: async () => ({ next: () => {} })
});
const result = await statusCommand(connection, 'INBOX', { highestModseq: true });
// Should return false since no valid query attributes
test.equal(result, false);
test.done();
};
module.exports['Commands: status updates current mailbox when SELECTED'] = async test => {
let existsEmitted = false;
const connection = createMockConnection({
state: 3, // SELECTED
mailbox: { path: 'INBOX', exists: 50, uidNext: 500 },
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.STATUS) {
await opts.untagged.STATUS({
attributes: [{ value: 'INBOX' }, [{ value: 'MESSAGES' }, { value: '100' }, { value: 'UIDNEXT' }, { value: '1000' }]]
});
}
return { next: () => {} };
},
emit: event => {
if (event === 'exists') existsEmitted = true;
}
});
await statusCommand(connection, 'INBOX', { messages: true, uidNext: true });
// Mailbox should be updated
test.equal(connection.mailbox.exists, 100);
test.equal(connection.mailbox.uidNext, 1000);
// exists event should be emitted since count changed
test.equal(existsEmitted, true);
test.done();
};
module.exports['Commands: status does not emit exists when count unchanged'] = async test => {
let existsEmitted = false;
const connection = createMockConnection({
state: 3,
mailbox: { path: 'INBOX', exists: 100 }, // Same as response
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.STATUS) {
await opts.untagged.STATUS({
attributes: [{ value: 'INBOX' }, [{ value: 'MESSAGES' }, { value: '100' }]]
});
}
return { next: () => {} };
},
emit: event => {
if (event === 'exists') existsEmitted = true;
}
});
await statusCommand(connection, 'INBOX', { messages: true });
test.equal(existsEmitted, false);
test.done();
};
module.exports['Commands: status handles error with NO response'] = async test => {
const connection = createMockConnection({
state: 2,
run: async () => [], // LIST returns empty - folder doesn't exist
exec: async () => {
const err = new Error('Mailbox not found');
err.responseStatus = 'NO';
throw err;
}
});
try {
await statusCommand(connection, 'NonExistent', { messages: true });
test.ok(false, 'Should have thrown');
} catch (err) {
test.equal(err.code, 'NotFound');
}
test.done();
};
module.exports['Commands: status returns false on other errors'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async () => {
const err = new Error('Some error');
err.responseStatus = 'BAD';
throw err;
}
});
const result = await statusCommand(connection, 'INBOX', { messages: true });
test.equal(result, false);
test.done();
};
module.exports['Commands: status handles empty STATUS response'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.STATUS) {
// Empty list - should be ignored
await opts.untagged.STATUS({
attributes: [{ value: 'INBOX' }, false]
});
}
return { next: () => {} };
}
});
const result = await statusCommand(connection, 'INBOX', { messages: true });
test.ok(result);
test.equal(result.path, 'INBOX');
// No messages property since response was empty
test.equal(result.messages, undefined);
test.done();
};
module.exports['Commands: status handles invalid entry values'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.STATUS) {
await opts.untagged.STATUS({
attributes: [
{ value: 'INBOX' },
[
{ value: 'MESSAGES' },
{ value: 'not-a-number' },
{ value: 'UNSEEN' },
{ value: '10' },
null,
{ value: '5' }, // Invalid key
{ value: 'RECENT' },
null // Invalid value
]
]
});
}
return { next: () => {} };
}
});
const result = await statusCommand(connection, 'INBOX', { messages: true, unseen: true, recent: true });
test.ok(result);
// MESSAGES with invalid value should be skipped (isNaN check fails)
test.equal(result.messages, undefined);
// UNSEEN should work
test.equal(result.unseen, 10);
// RECENT with null value should be skipped
test.equal(result.recent, undefined);
test.done();
};
module.exports['Commands: status encodes path with special characters'] = async test => {
let execAttrs = null;
const connection = createMockConnection({
state: 2,
exec: async (cmd, attrs) => {
execAttrs = attrs;
return { next: () => {} };
}
});
await statusCommand(connection, 'Test&Folder', { messages: true });
// Path with & should use STRING type instead of ATOM
test.ok(execAttrs);
test.equal(execAttrs[0].type, 'STRING');
test.done();
};
module.exports['Commands: status works from SELECTED state'] = async test => {
let execCalled = false;
const connection = createMockConnection({
state: 3, // SELECTED
mailbox: { path: 'OtherFolder' }, // Different folder
exec: async (cmd, attrs, opts) => {
execCalled = true;
if (opts && opts.untagged && opts.untagged.STATUS) {
await opts.untagged.STATUS({
attributes: [{ value: 'INBOX' }, [{ value: 'MESSAGES' }, { value: '50' }]]
});
}
return { next: () => {} };
}
});
const result = await statusCommand(connection, 'INBOX', { messages: true });
test.equal(execCalled, true);
test.equal(result.messages, 50);
test.done();
};
module.exports['Commands: status updates HIGHESTMODSEQ for current mailbox'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['CONDSTORE', true]]),
mailbox: { path: 'INBOX', highestModseq: BigInt(100) },
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.STATUS) {
await opts.untagged.STATUS({
attributes: [{ value: 'INBOX' }, [{ value: 'HIGHESTMODSEQ' }, { value: '200' }]]
});
}
return { next: () => {} };
}
});
await statusCommand(connection, 'INBOX', { highestModseq: true });
test.equal(connection.mailbox.highestModseq, BigInt(200));
test.done();
};
module.exports['Commands: status handles NaN values in response'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.STATUS) {
await opts.untagged.STATUS({
attributes: [
{ value: 'TestFolder' },
[
{ value: 'MESSAGES' },
{ value: 'invalid' }, // NaN
{ value: 'RECENT' },
{ value: 'notanumber' }, // NaN
{ value: 'UIDNEXT' },
{ value: 'abc' }, // NaN
{ value: 'UIDVALIDITY' },
{ value: 'xyz' }, // NaN
{ value: 'UNSEEN' },
{ value: 'bad' } // NaN
]
]
});
}
return { next: () => {} };
}
});
const result = await statusCommand(connection, 'TestFolder', {
messages: true,
recent: true,
uidNext: true,
uidValidity: true,
unseen: true
});
test.ok(result);
test.equal(result.path, 'TestFolder');
// NaN values should not be set
test.equal(result.messages, undefined);
test.equal(result.recent, undefined);
test.equal(result.uidNext, undefined);
test.equal(result.uidValidity, undefined);
test.equal(result.unseen, undefined);
test.done();
};
module.exports['Commands: status handles NaN HIGHESTMODSEQ'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([['CONDSTORE', true]]),
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.STATUS) {
await opts.untagged.STATUS({
attributes: [{ value: 'TestFolder' }, [{ value: 'HIGHESTMODSEQ' }, { value: 'notvalid' }]]
});
}
return { next: () => {} };
}
});
const result = await statusCommand(connection, 'TestFolder', { highestModseq: true });
test.ok(result);
test.equal(result.highestModseq, undefined);
test.done();
};
module.exports['Commands: status filters falsy query values'] = async test => {
let queryAttrs = null;
const connection = createMockConnection({
state: 2,
exec: async (cmd, attrs) => {
queryAttrs = attrs;
return { next: () => {} };
}
});
// Mix of truthy and falsy values
const result = await statusCommand(connection, 'TestFolder', {
messages: true,
recent: false, // Should be filtered
uidNext: 0, // Falsy, should be filtered
uidValidity: true,
unseen: null // Falsy, should be filtered
});
test.ok(result);
// Query should only include messages and uidValidity
test.ok(queryAttrs);
const queryList = queryAttrs[1];
test.equal(queryList.length, 2);
test.done();
};
module.exports['Commands: status handles missing entry value'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.STATUS) {
await opts.untagged.STATUS({
attributes: [
{ value: 'TestFolder' },
[
{ value: 'MESSAGES' },
null, // Missing value
{ value: 'RECENT' },
{ value: '5' }
]
]
});
}
return { next: () => {} };
}
});
const result = await statusCommand(connection, 'TestFolder', {
messages: true,
recent: true
});
test.ok(result);
test.equal(result.messages, undefined); // Skipped due to null value
test.equal(result.recent, 5);
test.done();
};
module.exports['Commands: status handles missing key in response'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.STATUS) {
await opts.untagged.STATUS({
attributes: [
{ value: 'TestFolder' },
[
null, // Missing key
{ value: '10' },
{ value: 'MESSAGES' },
{ value: '20' }
]
]
});
}
return { next: () => {} };
}
});
const result = await statusCommand(connection, 'TestFolder', { messages: true });
test.ok(result);
test.equal(result.messages, 20);
test.done();
};
module.exports['Commands: status handles unknown key in response'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.STATUS) {
await opts.untagged.STATUS({
attributes: [{ value: 'TestFolder' }, [{ value: 'UNKNOWNKEY' }, { value: '999' }, { value: 'MESSAGES' }, { value: '10' }]]
});
}
return { next: () => {} };
}
});
const result = await statusCommand(connection, 'TestFolder', { messages: true });
test.ok(result);
test.equal(result.messages, 10);
test.equal(result.UNKNOWNKEY, undefined); // Unknown keys ignored
test.done();
};
// ============================================
// APPEND Command Tests
// ============================================
const appendCommand = require('../lib/commands/append');
module.exports['Commands: append basic'] = async test => {
let appendCalled = false;
const connection = createMockConnection({
state: 2, // AUTHENTICATED
mailbox: { path: 'OtherFolder' }, // Different folder to avoid EXISTS handling
exec: async cmd => {
if (cmd === 'APPEND') {
appendCalled = true;
}
return {
next: () => {},
response: { attributes: [] }
};
}
});
const result = await appendCommand(connection, 'INBOX', 'Test message content');
test.equal(appendCalled, true);
test.ok(result);
test.equal(result.destination, 'INBOX');
test.done();
};
module.exports['Commands: append with Buffer content'] = async test => {
let contentAttr = null;
const connection = createMockConnection({
state: 2,
mailbox: { path: 'OtherFolder' },
exec: async (cmd, attrs) => {
if (cmd === 'APPEND' && Array.isArray(attrs)) {
contentAttr = attrs.find(a => a && a.type === 'LITERAL');
}
return {
next: () => {},
response: { attributes: [] }
};
}
});
const buffer = Buffer.from('Test message');
await appendCommand(connection, 'INBOX', buffer);
test.ok(contentAttr);
test.ok(Buffer.isBuffer(contentAttr.value));
test.equal(contentAttr.value.toString(), 'Test message');
test.done();
};
module.exports['Commands: append skips when not authenticated'] = async test => {
const connection = createMockConnection({ state: 1 }); // NOT_AUTHENTICATED
const result = await appendCommand(connection, 'INBOX', 'content');
test.equal(result, undefined);
test.done();
};
module.exports['Commands: append skips when no destination'] = async test => {
const connection = createMockConnection({ state: 2 });
const result = await appendCommand(connection, '', 'content');
test.equal(result, undefined);
test.done();
};
module.exports['Commands: append with flags'] = async test => {
let execAttrs = null;
const connection = createMockConnection({
state: 2,
mailbox: { path: 'OtherFolder', permanentFlags: new Set(['\\*']) },
exec: async (cmd, attrs) => {
if (cmd === 'APPEND' && Array.isArray(attrs)) {
execAttrs = attrs;
}
return {
next: () => {},
response: { attributes: [] }
};
}
});
await appendCommand(connection, 'INBOX', 'content', ['\\Seen', '\\Flagged']);
test.ok(execAttrs);
// Should have flags array between path and content
const flagsAttr = execAttrs.find(a => Array.isArray(a));
test.ok(flagsAttr);
test.ok(flagsAttr.some(f => f.value === '\\Seen'));
test.ok(flagsAttr.some(f => f.value === '\\Flagged'));
test.done();
};
module.exports['Commands: append with internal date'] = async test => {
let execAttrs = null;
const connection = createMockConnection({
state: 2,
mailbox: { path: 'OtherFolder' },
exec: async (cmd, attrs) => {
if (cmd === 'APPEND' && Array.isArray(attrs)) {
execAttrs = attrs;
}
return {
next: () => {},
response: { attributes: [] }
};
}
});
const date = new Date('2024-01-15T10:30:00Z');
await appendCommand(connection, 'INBOX', 'content', [], date);
test.ok(execAttrs);
// Should have date string
const dateAttr = execAttrs.find(a => a && a.type === 'STRING');
test.ok(dateAttr);
test.ok(dateAttr.value.includes('2024'));
test.done();
};
module.exports['Commands: append checks APPENDLIMIT'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([['APPENDLIMIT', 100]]), // 100 byte limit
mailbox: { path: 'INBOX' }
});
const largeContent = Buffer.alloc(200, 'x'); // 200 bytes, exceeds limit
try {
await appendCommand(connection, 'INBOX', largeContent);
test.ok(false, 'Should have thrown');
} catch (err) {
test.equal(err.serverResponseCode, 'APPENDLIMIT');
test.ok(err.message.includes('APPENDLIMIT'));
}
test.done();
};
module.exports['Commands: append allows content within APPENDLIMIT'] = async test => {
let execCalled = false;
const connection = createMockConnection({
state: 2,
capabilities: new Map([['APPENDLIMIT', 1000]]),
mailbox: { path: 'INBOX' },
exec: async () => {
execCalled = true;
return {
next: () => {},
response: { attributes: [] }
};
}
});
const content = Buffer.alloc(500, 'x'); // Within limit
await appendCommand(connection, 'INBOX', content);
test.equal(execCalled, true);
test.done();
};
module.exports['Commands: append with APPENDUID response'] = async test => {
const connection = createMockConnection({
state: 2,
mailbox: { path: 'INBOX' },
exec: async () => ({
next: () => {},
response: {
attributes: [
{
section: [
{ value: 'APPENDUID' },
{ value: '12345' }, // uidValidity
{ value: '100' } // uid
]
}
]
}
})
});
const result = await appendCommand(connection, 'INBOX', 'content');
test.equal(result.uidValidity, BigInt(12345));
test.equal(result.uid, 100);
test.done();
};
module.exports['Commands: append to current mailbox triggers EXISTS'] = async test => {
let existsEmitted = false;
const connection = createMockConnection({
state: 3, // SELECTED
mailbox: { path: 'INBOX', exists: 10 },
exec: async (cmd, attrs, opts) => {
// Simulate EXISTS untagged response
if (cmd === 'APPEND' && opts && opts.untagged && opts.untagged.EXISTS) {
await opts.untagged.EXISTS({ command: '11' });
}
return {
next: () => {},
response: { attributes: [] }
};
},
emit: event => {
if (event === 'exists') existsEmitted = true;
},
search: async () => [100] // Return UID
});
await appendCommand(connection, 'INBOX', 'content');
test.equal(existsEmitted, true);
test.equal(connection.mailbox.exists, 11);
test.done();
};
module.exports['Commands: append runs NOOP to get sequence if not in EXISTS'] = async test => {
let noopCalled = false;
const connection = createMockConnection({
state: 3,
mailbox: { path: 'INBOX', exists: 10 },
exec: async (cmd, attrs, opts) => {
if (cmd === 'NOOP') {
noopCalled = true;
if (opts && opts.untagged && opts.untagged.EXISTS) {
await opts.untagged.EXISTS({ command: '11' });
}
}
return {
next: () => {},
response: { attributes: [] }
};
},
emit: () => {},
search: async () => [100] // Return UID
});
const result = await appendCommand(connection, 'INBOX', 'content');
test.equal(noopCalled, true);
test.equal(result.seq, 11);
test.done();
};
module.exports['Commands: append searches for UID if seq but no uid'] = async test => {
let searchCalled = false;
const connection = createMockConnection({
state: 3,
mailbox: { path: 'INBOX', exists: 10 },
exec: async (cmd, attrs, opts) => {
if (opts && opts.untagged && opts.untagged.EXISTS) {
await opts.untagged.EXISTS({ command: '11' });
}
return {
next: () => {},
response: { attributes: [] }
};
},
emit: () => {},
search: async () => {
searchCalled = true;
return [100];
}
});
const result = await appendCommand(connection, 'INBOX', 'content');
test.equal(searchCalled, true);
test.equal(result.uid, 100);
test.done();
};
module.exports['Commands: append with BINARY and NULL bytes'] = async test => {
let literalAttr = null;
const connection = createMockConnection({
state: 2,
capabilities: new Map([['BINARY', true]]),
mailbox: { path: 'INBOX' },
exec: async (cmd, attrs) => {
literalAttr = attrs.find(a => a.type === 'LITERAL');
return {
next: () => {},
response: { attributes: [] }
};
}
});
// Content with NULL byte
const content = Buffer.concat([Buffer.from('test'), Buffer.from([0]), Buffer.from('data')]);
await appendCommand(connection, 'INBOX', content);
test.ok(literalAttr);
test.equal(literalAttr.isLiteral8, true);
test.done();
};
module.exports['Commands: append without BINARY uses regular literal'] = async test => {
let literalAttr = null;
const connection = createMockConnection({
state: 2,
capabilities: new Map(), // No BINARY
mailbox: { path: 'INBOX' },
exec: async (cmd, attrs) => {
literalAttr = attrs.find(a => a.type === 'LITERAL');
return {
next: () => {},
response: { attributes: [] }
};
}
});
const content = Buffer.concat([Buffer.from('test'), Buffer.from([0]), Buffer.from('data')]);
await appendCommand(connection, 'INBOX', content);
test.ok(literalAttr);
test.equal(literalAttr.isLiteral8, false);
test.done();
};
module.exports['Commands: append handles error'] = async test => {
const connection = createMockConnection({
state: 2,
mailbox: { path: 'OtherFolder' },
exec: async () => {
const err = new Error('Append failed');
err.response = { attributes: [] };
throw err;
}
});
try {
await appendCommand(connection, 'INBOX', 'content');
test.ok(false, 'Should have thrown');
} catch (err) {
test.equal(err.message, 'Append failed');
}
test.done();
};
module.exports['Commands: append filters invalid flags'] = async test => {
let execAttrs = null;
const connection = createMockConnection({
state: 2,
mailbox: {
path: 'OtherFolder',
permanentFlags: new Set(['\\Seen', '\\Flagged']) // Only allow these
},
exec: async (cmd, attrs) => {
if (cmd === 'APPEND' && Array.isArray(attrs)) {
execAttrs = attrs;
}
return {
next: () => {},
response: { attributes: [] }
};
}
});
// Mix of valid and invalid flags
await appendCommand(connection, 'INBOX', 'content', ['\\Seen', '\\CustomFlag', null, '\\Flagged']);
test.ok(execAttrs);
const flagsAttr = execAttrs.find(a => Array.isArray(a));
test.ok(flagsAttr);
// Should only contain allowed flags
test.equal(flagsAttr.length, 2);
test.done();
};
module.exports['Commands: append works from SELECTED state'] = async test => {
let execCalled = false;
const connection = createMockConnection({
state: 3, // SELECTED
mailbox: { path: 'OtherFolder', exists: 10 },
exec: async () => {
execCalled = true;
return {
next: () => {},
response: { attributes: [] }
};
}
});
// Append to different folder than current
const result = await appendCommand(connection, 'INBOX', 'content');
test.equal(execCalled, true);
test.equal(result.destination, 'INBOX');
test.done();
};
module.exports['Commands: append error with serverResponseCode'] = async test => {
const connection = createMockConnection({
state: 2,
mailbox: { path: 'OtherFolder' },
exec: async () => {
const err = new Error('Append failed');
err.response = {
tag: '*',
command: 'NO',
attributes: [
{
type: 'SECTION',
section: [{ type: 'ATOM', value: 'TRYCREATE' }]
},
{ type: 'TEXT', value: 'Mailbox does not exist' }
]
};
throw err;
}
});
try {
await appendCommand(connection, 'NonExistent', 'content');
test.ok(false, 'Should have thrown');
} catch (err) {
test.equal(err.serverResponseCode, 'TRYCREATE');
}
test.done();
};
module.exports['Commands: append with invalid APPENDUID values'] = async test => {
const connection = createMockConnection({
state: 2,
mailbox: { path: 'OtherFolder' },
exec: async () => ({
next: () => {},
response: {
attributes: [
{
type: 'ATOM',
section: [
{ type: 'ATOM', value: 'APPENDUID' },
{ type: 'ATOM', value: 'invalid' }, // Invalid uidValidity
{ type: 'ATOM', value: 'notanumber' } // Invalid uid
]
}
]
}
})
});
const result = await appendCommand(connection, 'INBOX', 'content');
test.ok(result);
test.equal(result.uidValidity, undefined);
test.equal(result.uid, undefined);
test.done();
};
module.exports['Commands: append NOOP error is caught'] = async test => {
let noopCalled = false;
const connection = createMockConnection({
state: 3,
mailbox: { path: 'INBOX', exists: 10 },
exec: async cmd => {
if (cmd === 'APPEND') {
return {
next: () => {},
response: { attributes: [] }
};
}
if (cmd === 'NOOP') {
noopCalled = true;
const err = new Error('NOOP failed');
err.response = { attributes: [] };
throw err;
}
}
});
// Append to current mailbox, expectExists = true
const result = await appendCommand(connection, 'INBOX', 'content');
test.ok(result);
test.equal(noopCalled, true);
// Should not throw, NOOP error is caught
test.done();
};
module.exports['Commands: append EXISTS updates mailbox count'] = async test => {
let emittedEvent = null;
const connection = createMockConnection({
state: 3,
mailbox: { path: 'INBOX', exists: 10 },
exec: async (cmd, attrs, opts) => {
if (cmd === 'APPEND' && opts && opts.untagged && opts.untagged.EXISTS) {
await opts.untagged.EXISTS({ command: '11' }); // New count
}
return {
next: () => {},
response: {
attributes: [
{
type: 'ATOM',
section: [
{ type: 'ATOM', value: 'APPENDUID' },
{ type: 'ATOM', value: '12345' },
{ type: 'ATOM', value: '100' }
]
}
]
}
};
},
emit: (event, data) => {
if (event === 'exists') {
emittedEvent = data;
}
}
});
const result = await appendCommand(connection, 'INBOX', 'content');
test.ok(result);
test.equal(result.seq, 11);
test.equal(connection.mailbox.exists, 11);
test.ok(emittedEvent);
test.equal(emittedEvent.count, 11);
test.equal(emittedEvent.prevCount, 10);
test.done();
};
module.exports['Commands: append does not emit exists when count unchanged'] = async test => {
let emittedEvent = null;
const connection = createMockConnection({
state: 3,
mailbox: { path: 'INBOX', exists: 10 },
exec: async (cmd, attrs, opts) => {
if (cmd === 'APPEND' && opts && opts.untagged && opts.untagged.EXISTS) {
await opts.untagged.EXISTS({ command: '10' }); // Same count
}
return {
next: () => {},
response: {
attributes: [
{
type: 'ATOM',
section: [
{ type: 'ATOM', value: 'APPENDUID' },
{ type: 'ATOM', value: '12345' },
{ type: 'ATOM', value: '100' }
]
}
]
}
};
},
emit: (event, data) => {
if (event === 'exists') {
emittedEvent = data;
}
}
});
await appendCommand(connection, 'INBOX', 'content');
test.equal(emittedEvent, null); // No event emitted
test.done();
};
module.exports['Commands: append with both flags and date'] = async test => {
let execAttrs = null;
const connection = createMockConnection({
state: 2,
mailbox: { path: 'OtherFolder' },
exec: async (cmd, attrs) => {
execAttrs = attrs;
return {
next: () => {},
response: { attributes: [] }
};
}
});
const testDate = new Date('2024-01-15T10:30:00Z');
await appendCommand(connection, 'INBOX', 'content', ['\\Seen'], testDate);
test.ok(execAttrs);
// Should have: path, flags array, date string, literal
test.equal(execAttrs.length, 4);
// Flags array
test.ok(Array.isArray(execAttrs[1]));
// Date string
test.equal(execAttrs[2].type, 'STRING');
test.done();
};
module.exports['Commands: append with disableBinary does not use literal8'] = async test => {
let execAttrs = null;
const connection = createMockConnection({
state: 2,
mailbox: { path: 'OtherFolder' },
capabilities: new Map([['BINARY', true]]),
disableBinary: true,
exec: async (cmd, attrs) => {
execAttrs = attrs;
return {
next: () => {},
response: { attributes: [] }
};
}
});
// Content with NULL byte
const content = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x57, 0x6f, 0x72, 0x6c, 0x64]);
await appendCommand(connection, 'INBOX', content);
test.ok(execAttrs);
const literalAttr = execAttrs.find(a => a && a.type === 'LITERAL');
test.ok(literalAttr);
test.equal(literalAttr.isLiteral8, false); // Not literal8 due to disableBinary
test.done();
};
// ============================================
// IDLE Command Tests
// ============================================
const idleCommand = require('../lib/commands/idle');
module.exports['Commands: idle with IDLE capability'] = async test => {
let execCommand = '';
let idlingSet = false;
const connection = createMockConnection({
state: 3, // SELECTED
capabilities: new Map([['IDLE', true]]),
exec: async (cmd, attrs, opts) => {
execCommand = cmd;
idlingSet = connection.idling;
// Simulate continuation response
if (opts && opts.onPlusTag) {
await opts.onPlusTag();
}
return { next: () => {} };
},
write: () => {}
});
await idleCommand(connection);
test.equal(execCommand, 'IDLE');
test.equal(idlingSet, true);
test.done();
};
module.exports['Commands: idle skips when not selected'] = async test => {
const connection = createMockConnection({ state: 2 }); // AUTHENTICATED
const result = await idleCommand(connection);
test.equal(result, undefined);
test.done();
};
module.exports['Commands: idle falls back to NOOP without IDLE capability'] = async test => {
let noopCalled = false;
const connection = createMockConnection({
state: 3,
capabilities: new Map(), // No IDLE
currentSelectCommand: { command: 'SELECT', arguments: [{ value: 'INBOX' }] },
exec: async cmd => {
if (cmd === 'NOOP') {
noopCalled = true;
// Break out of the loop by calling preCheck
if (connection.preCheck) {
await connection.preCheck();
}
}
return { next: () => {} };
}
});
// Start idle - it will loop with NOOP
const idlePromise = idleCommand(connection);
// Give it a moment to start, then break the loop
await new Promise(resolve => setTimeout(resolve, 10));
if (connection.preCheck) {
await connection.preCheck();
}
await idlePromise;
test.equal(noopCalled, true);
test.done();
};
module.exports['Commands: idle preCheck breaks IDLE'] = async test => {
let doneSent = false;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['IDLE', true]]),
exec: async (cmd, attrs, opts) => {
if (opts && opts.onPlusTag) {
await opts.onPlusTag();
}
// After IDLE is initiated, trigger preCheck
if (connection.preCheck) {
await connection.preCheck();
}
return { next: () => {} };
},
write: data => {
if (data === 'DONE') {
doneSent = true;
}
}
});
await idleCommand(connection);
test.equal(doneSent, true);
test.equal(connection.idling, false);
test.done();
};
module.exports['Commands: idle handles error'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['IDLE', true]]),
exec: async () => {
throw new Error('IDLE failed');
}
});
const result = await idleCommand(connection);
test.equal(result, false);
test.equal(connection.idling, false);
test.done();
};
module.exports['Commands: idle with maxIdleTime restarts loop'] = async test => {
let idleCount = 0;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['IDLE', true]]),
exec: async (cmd, attrs, opts) => {
idleCount++;
if (opts && opts.onPlusTag) {
await opts.onPlusTag();
}
// Break after second iteration
if (idleCount >= 2 && connection.preCheck) {
await connection.preCheck();
}
return { next: () => {} };
},
write: () => {}
});
// Very short maxIdleTime to trigger restart
await idleCommand(connection, 5);
test.ok(idleCount >= 1);
test.done();
};
module.exports['Commands: idle without currentSelectCommand returns immediately'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map(), // No IDLE
currentSelectCommand: false // No select command
});
// Should resolve immediately
await idleCommand(connection);
test.ok(true);
test.done();
};
module.exports['Commands: idle NOOP fallback uses STATUS when configured'] = async test => {
let statusCalled = false;
const connection = createMockConnection({
state: 3,
capabilities: new Map(),
currentSelectCommand: { command: 'SELECT', arguments: [{ value: 'INBOX' }] },
missingIdleCommand: 'STATUS',
exec: async cmd => {
if (cmd === 'STATUS') {
statusCalled = true;
if (connection.preCheck) {
await connection.preCheck();
}
}
return { next: () => {} };
}
});
await idleCommand(connection);
test.equal(statusCalled, true);
test.done();
};
module.exports['Commands: idle NOOP fallback uses SELECT when configured'] = async test => {
let selectCalled = false;
const connection = createMockConnection({
state: 3,
capabilities: new Map(),
currentSelectCommand: { command: 'SELECT', arguments: [{ value: 'INBOX' }] },
missingIdleCommand: 'SELECT',
exec: async cmd => {
if (cmd === 'SELECT') {
selectCalled = true;
if (connection.preCheck) {
await connection.preCheck();
}
}
return { next: () => {} };
}
});
await idleCommand(connection);
test.equal(selectCalled, true);
test.done();
};
module.exports['Commands: idle sets preCheck function'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['IDLE', true]]),
exec: async (cmd, attrs, opts) => {
if (opts && opts.onPlusTag) {
await opts.onPlusTag();
}
// Check that preCheck is set
test.equal(typeof connection.preCheck, 'function');
// Break the IDLE
if (connection.preCheck) {
await connection.preCheck();
}
return { next: () => {} };
},
write: () => {}
});
await idleCommand(connection);
test.done();
};
module.exports['Commands: idle clears preCheck on completion'] = async test => {
const connection = createMockConnection({
state: 3,
capabilities: new Map([['IDLE', true]]),
exec: async (cmd, attrs, opts) => {
if (opts && opts.onPlusTag) {
await opts.onPlusTag();
}
if (connection.preCheck) {
await connection.preCheck();
}
return { next: () => {} };
},
write: () => {}
});
await idleCommand(connection);
test.equal(connection.preCheck, false);
test.done();
};
module.exports['Commands: idle NOOP fallback handles error'] = async test => {
let errorLogged = false;
const connection = createMockConnection({
state: 3,
capabilities: new Map(),
currentSelectCommand: { command: 'SELECT', arguments: [{ value: 'INBOX' }] },
exec: async () => {
throw new Error('NOOP failed');
},
log: {
warn: () => {
errorLogged = true;
},
debug: () => {},
trace: () => {}
}
});
// Should resolve even on error
await idleCommand(connection);
test.equal(errorLogged, true);
test.done();
};
module.exports['Commands: idle clears wait queue on normal completion'] = async test => {
let preCheckResolved = false;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['IDLE', true]]),
exec: async (cmd, attrs, opts) => {
if (opts && opts.onPlusTag) {
await opts.onPlusTag();
}
// Simulate waiting preCheck request before completion
if (connection.preCheck) {
// Queue a preCheck request
connection.preCheck().then(() => {
preCheckResolved = true;
});
}
return { next: () => {} };
},
write: () => {}
});
await idleCommand(connection);
// Wait a tick for the promise to resolve
await new Promise(resolve => setImmediate(resolve));
test.equal(preCheckResolved, true);
test.done();
};
module.exports['Commands: idle rejects wait queue on error'] = async test => {
let preCheckRejected = false;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['IDLE', true]]),
exec: async (cmd, attrs, opts) => {
if (opts && opts.onPlusTag) {
await opts.onPlusTag();
}
// Queue a preCheck request then throw
if (connection.preCheck) {
connection.preCheck().catch(() => {
preCheckRejected = true;
});
}
throw new Error('IDLE failed');
}
});
const result = await idleCommand(connection);
test.equal(result, false);
// Wait a tick for the promise to reject
await new Promise(resolve => setImmediate(resolve));
test.equal(preCheckRejected, true);
test.done();
};
module.exports['Commands: idle onPlusTag calls preCheck if doneRequested'] = async test => {
let doneSent = false;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['IDLE', true]]),
exec: async (cmd, attrs, opts) => {
// Request done before onPlusTag is called
if (connection.preCheck) {
connection.preCheck().catch(() => {});
}
// Then call onPlusTag which should send DONE
if (opts && opts.onPlusTag) {
await opts.onPlusTag();
}
return { next: () => {} };
},
write: data => {
if (data === 'DONE') {
doneSent = true;
}
}
});
await idleCommand(connection);
test.equal(doneSent, true);
test.done();
};
module.exports['Commands: idle calls onSend callback'] = async test => {
let onSendCalled = false;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['IDLE', true]]),
exec: async (cmd, attrs, opts) => {
// Call onSend callback
if (opts && opts.onSend) {
opts.onSend();
onSendCalled = true;
}
// Then call onPlusTag to enable IDLE
if (opts && opts.onPlusTag) {
await opts.onPlusTag();
}
// Break IDLE via preCheck
if (connection.preCheck) {
await connection.preCheck();
}
return { next: () => {} };
},
write: () => {}
});
await idleCommand(connection);
test.equal(onSendCalled, true);
test.done();
};
module.exports['Commands: idle clears preCheck and queue on normal completion'] = async test => {
let waitQueueResolved = false;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['IDLE', true]]),
exec: async (cmd, attrs, opts) => {
if (opts && opts.onPlusTag) {
await opts.onPlusTag();
}
// After onPlusTag, queue a preCheck but don't resolve yet
if (connection.preCheck) {
connection.preCheck().then(() => {
waitQueueResolved = true;
});
}
// Return to complete IDLE - this should clear the queue
return { next: () => {} };
},
write: () => {}
});
await idleCommand(connection);
await new Promise(resolve => setImmediate(resolve));
// preCheck should be cleared after completion
test.equal(connection.preCheck, false);
test.equal(waitQueueResolved, true);
test.done();
};
module.exports['Commands: idle with maxIdleTime triggers preCheck after timeout'] = async test => {
let preCheckCalled = false;
let loopCount = 0;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['IDLE', true]]),
idling: false,
exec: async (cmd, attrs, opts) => {
loopCount++;
if (opts && opts.onPlusTag) {
await opts.onPlusTag();
}
// Simulate time passing - on first loop, wait for timer
if (loopCount === 1) {
// Wait a bit for the timer to fire
await new Promise(resolve => setTimeout(resolve, 25));
}
// Check if preCheck was called by the timer
if (connection.preCheck && loopCount === 1) {
preCheckCalled = true;
await connection.preCheck();
}
return { next: () => {} };
},
write: () => {}
});
// Use very short maxIdleTime
await idleCommand(connection, 10);
test.ok(preCheckCalled || loopCount > 1, 'preCheck should be called by timer or loop should restart');
test.done();
};
module.exports['Commands: idle stillIdling triggers loop restart'] = async test => {
let loopCount = 0;
const connection = createMockConnection({
state: 3,
capabilities: new Map([['IDLE', true]]),
idling: false,
exec: async (cmd, attrs, opts) => {
loopCount++;
if (opts && opts.onPlusTag) {
await opts.onPlusTag();
}
// First iteration: let the timer set stillIdling and trigger preCheck
if (loopCount === 1) {
await new Promise(resolve => setTimeout(resolve, 20));
// Timer should have called preCheck which sets stillIdling
}
// Second iteration: just complete
if (connection.preCheck) {
await connection.preCheck();
}
return { next: () => {} };
},
write: () => {}
});
await idleCommand(connection, 5);
// Loop should have run at least once (could run twice if timer works)
test.ok(loopCount >= 1, 'IDLE loop should have run');
test.done();
};
// ============================================
// ID Command Tests
// ============================================
const idCommand = require('../lib/commands/id');
module.exports['Commands: id skips when no ID capability'] = async test => {
const connection = createMockConnection({
capabilities: new Map() // No ID capability
});
const result = await idCommand(connection, { name: 'TestClient' });
test.equal(result, undefined);
test.done();
};
module.exports['Commands: id sends client info'] = async test => {
let execArgs = null;
const connection = createMockConnection({
capabilities: new Map([['ID', true]]),
exec: async (cmd, args) => {
execArgs = { cmd, args };
return { next: () => {} };
}
});
await idCommand(connection, { name: 'TestClient', version: '1.0' });
test.equal(execArgs.cmd, 'ID');
test.ok(Array.isArray(execArgs.args));
test.ok(execArgs.args[0].includes('name'));
test.ok(execArgs.args[0].includes('TestClient'));
test.done();
};
module.exports['Commands: id sends null when no clientInfo'] = async test => {
let execArgs = null;
const connection = createMockConnection({
capabilities: new Map([['ID', true]]),
exec: async (cmd, args) => {
execArgs = { cmd, args };
return { next: () => {} };
}
});
await idCommand(connection, null);
test.equal(execArgs.cmd, 'ID');
test.equal(execArgs.args[0], null);
test.done();
};
module.exports['Commands: id sends null for empty clientInfo'] = async test => {
let execArgs = null;
const connection = createMockConnection({
capabilities: new Map([['ID', true]]),
exec: async (cmd, args) => {
execArgs = { cmd, args };
return { next: () => {} };
}
});
await idCommand(connection, {});
test.equal(execArgs.cmd, 'ID');
test.equal(execArgs.args[0], null);
test.done();
};
module.exports['Commands: id parses server response'] = async test => {
const connection = createMockConnection({
capabilities: new Map([['ID', true]]),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.ID) {
await opts.untagged.ID({
attributes: [[{ value: 'name' }, { value: 'TestServer' }, { value: 'version' }, { value: '2.0' }, { value: 'vendor' }, { value: 'ACME' }]]
});
}
return { next: () => {} };
}
});
const result = await idCommand(connection, { name: 'TestClient' });
test.equal(result.name, 'TestServer');
test.equal(result.version, '2.0');
test.equal(result.vendor, 'ACME');
test.done();
};
module.exports['Commands: id updates serverInfo'] = async test => {
const connection = createMockConnection({
capabilities: new Map([['ID', true]]),
serverInfo: {},
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.ID) {
await opts.untagged.ID({
attributes: [[{ value: 'name' }, { value: 'ImapServer' }, { value: 'support-url' }, { value: 'https://example.com' }]]
});
}
return { next: () => {} };
}
});
await idCommand(connection, { name: 'TestClient' });
test.equal(connection.serverInfo.name, 'ImapServer');
test.equal(connection.serverInfo['support-url'], 'https://example.com');
test.done();
};
module.exports['Commands: id handles non-array server response'] = async test => {
const connection = createMockConnection({
capabilities: new Map([['ID', true]]),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.ID) {
// Some servers might send NIL or a single value
await opts.untagged.ID({
attributes: [null]
});
}
return { next: () => {} };
}
});
const result = await idCommand(connection, { name: 'TestClient' });
test.ok(result);
test.deepEqual(result, {});
test.done();
};
module.exports['Commands: id formats date value'] = async test => {
let execArgs = null;
const connection = createMockConnection({
capabilities: new Map([['ID', true]]),
exec: async (cmd, args) => {
execArgs = { cmd, args };
return { next: () => {} };
}
});
const testDate = new Date('2024-06-15T10:30:00Z');
await idCommand(connection, { date: testDate });
test.equal(execArgs.cmd, 'ID');
// Date should be formatted, not passed as Date object
test.ok(execArgs.args[0].includes('date'));
test.done();
};
module.exports['Commands: id normalizes key names to lowercase'] = async test => {
const connection = createMockConnection({
capabilities: new Map([['ID', true]]),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.ID) {
await opts.untagged.ID({
attributes: [[{ value: 'NAME' }, { value: 'TestServer' }, { value: 'VERSION' }, { value: '1.0' }]]
});
}
return { next: () => {} };
}
});
const result = await idCommand(connection, { name: 'TestClient' });
test.equal(result.name, 'TestServer');
test.equal(result.version, '1.0');
test.done();
};
module.exports['Commands: id trims key names'] = async test => {
const connection = createMockConnection({
capabilities: new Map([['ID', true]]),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.ID) {
await opts.untagged.ID({
attributes: [[{ value: ' name ' }, { value: 'TestServer' }]]
});
}
return { next: () => {} };
}
});
const result = await idCommand(connection, { name: 'TestClient' });
test.equal(result.name, 'TestServer');
test.done();
};
module.exports['Commands: id handles error'] = async test => {
let warnLogged = false;
const connection = createMockConnection({
capabilities: new Map([['ID', true]]),
exec: async () => {
throw new Error('ID command failed');
},
log: {
warn: () => {
warnLogged = true;
},
debug: () => {},
trace: () => {}
}
});
const result = await idCommand(connection, { name: 'TestClient' });
test.equal(result, false);
test.ok(warnLogged);
test.done();
};
module.exports['Commands: id filters empty values'] = async test => {
let execArgs = null;
const connection = createMockConnection({
capabilities: new Map([['ID', true]]),
exec: async (cmd, args) => {
execArgs = { cmd, args };
return { next: () => {} };
}
});
await idCommand(connection, { name: 'TestClient', empty: '', valid: 'value' });
test.equal(execArgs.cmd, 'ID');
// Empty values should be filtered out
test.ok(execArgs.args[0].includes('name'));
test.ok(execArgs.args[0].includes('valid'));
test.done();
};
module.exports['Commands: id replaces whitespace in values'] = async test => {
let execArgs = null;
const connection = createMockConnection({
capabilities: new Map([['ID', true]]),
exec: async (cmd, args) => {
execArgs = { cmd, args };
return { next: () => {} };
}
});
await idCommand(connection, { name: 'Test\nClient\tApp' });
test.equal(execArgs.cmd, 'ID');
// Whitespace should be normalized to single spaces
const nameIndex = execArgs.args[0].indexOf('name');
test.ok(nameIndex >= 0);
const nameValue = execArgs.args[0][nameIndex + 1];
test.ok(!nameValue.includes('\n'));
test.ok(!nameValue.includes('\t'));
test.done();
};
// ============================================
// NAMESPACE Command Tests
// ============================================
const namespaceCommand = require('../lib/commands/namespace');
module.exports['Commands: namespace skips when not authenticated'] = async test => {
const connection = createMockConnection({ state: 1 }); // NOT_AUTHENTICATED
const result = await namespaceCommand(connection);
test.equal(result, undefined);
test.done();
};
module.exports['Commands: namespace with NAMESPACE capability'] = async test => {
const connection = createMockConnection({
state: 2, // AUTHENTICATED
capabilities: new Map([['NAMESPACE', true]]),
exec: async (cmd, args, opts) => {
test.equal(cmd, 'NAMESPACE');
if (opts && opts.untagged && opts.untagged.NAMESPACE) {
await opts.untagged.NAMESPACE({
attributes: [
// personal namespaces
[[{ value: 'INBOX.' }, { value: '.' }]],
// other users
[[{ value: 'Users.' }, { value: '.' }]],
// shared
[[{ value: 'Shared.' }, { value: '.' }]]
]
});
}
return { next: () => {} };
}
});
const result = await namespaceCommand(connection);
test.ok(result);
test.equal(result.prefix, 'INBOX.');
test.equal(result.delimiter, '.');
test.equal(connection.namespaces.personal[0].prefix, 'INBOX.');
test.equal(connection.namespaces.other[0].prefix, 'Users.');
test.equal(connection.namespaces.shared[0].prefix, 'Shared.');
test.done();
};
module.exports['Commands: namespace fallback without capability'] = async test => {
const connection = createMockConnection({
state: 2, // AUTHENTICATED
capabilities: new Map(), // No NAMESPACE capability
exec: async (cmd, args, opts) => {
test.equal(cmd, 'LIST');
if (opts && opts.untagged && opts.untagged.LIST) {
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '/' }, { value: 'INBOX' }]
});
}
return { next: () => {} };
}
});
const result = await namespaceCommand(connection);
test.ok(result);
test.equal(result.delimiter, '/');
test.equal(connection.namespaces.other, false);
test.equal(connection.namespaces.shared, false);
test.done();
};
module.exports['Commands: namespace fallback adds delimiter to prefix'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map(),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.LIST) {
await opts.untagged.LIST({
attributes: [[{ value: '\\HasNoChildren' }], { value: '.' }, { value: 'INBOX' }]
});
}
return { next: () => {} };
}
});
const result = await namespaceCommand(connection);
test.ok(result);
test.equal(result.delimiter, '.');
test.done();
};
module.exports['Commands: namespace handles empty response'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([['NAMESPACE', true]]),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.NAMESPACE) {
// Provide minimal valid namespace even in "empty" case
await opts.untagged.NAMESPACE({
attributes: [
[[{ value: '' }, { value: '.' }]], // minimal personal namespace
null,
null
]
});
}
return { next: () => {} };
}
});
const result = await namespaceCommand(connection);
test.ok(result);
test.equal(result.prefix, '');
test.equal(result.delimiter, '.');
test.done();
};
module.exports['Commands: namespace handles NIL namespaces'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([['NAMESPACE', true]]),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.NAMESPACE) {
await opts.untagged.NAMESPACE({
attributes: [
[[{ value: '' }, { value: '/' }]], // personal
null, // other (NIL)
null // shared (NIL)
]
});
}
return { next: () => {} };
}
});
const result = await namespaceCommand(connection);
test.ok(result);
test.equal(result.delimiter, '/');
test.equal(connection.namespaces.other, false);
test.equal(connection.namespaces.shared, false);
test.done();
};
module.exports['Commands: namespace handles multiple personal namespaces'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([['NAMESPACE', true]]),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.NAMESPACE) {
await opts.untagged.NAMESPACE({
attributes: [
[
[{ value: 'INBOX' }, { value: '.' }],
[{ value: 'Mail' }, { value: '/' }]
],
null,
null
]
});
}
return { next: () => {} };
}
});
const result = await namespaceCommand(connection);
test.ok(result);
test.equal(connection.namespaces.personal.length, 2);
test.equal(connection.namespaces.personal[0].prefix, 'INBOX.');
test.equal(connection.namespaces.personal[1].prefix, 'Mail/');
test.done();
};
module.exports['Commands: namespace works in SELECTED state'] = async test => {
const connection = createMockConnection({
state: 3, // SELECTED
capabilities: new Map([['NAMESPACE', true]]),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.NAMESPACE) {
await opts.untagged.NAMESPACE({
attributes: [[[{ value: '' }, { value: '/' }]], null, null]
});
}
return { next: () => {} };
}
});
const result = await namespaceCommand(connection);
test.ok(result);
test.equal(result.delimiter, '/');
test.done();
};
module.exports['Commands: namespace handles error'] = async test => {
let warnLogged = false;
const connection = createMockConnection({
state: 2,
capabilities: new Map([['NAMESPACE', true]]),
exec: async () => {
const err = new Error('Namespace failed');
err.responseStatus = 'NO';
err.responseText = 'Command not supported';
throw err;
},
log: {
warn: () => {
warnLogged = true;
},
debug: () => {},
trace: () => {}
}
});
const result = await namespaceCommand(connection);
test.ok(result.error);
test.equal(result.status, 'NO');
test.equal(result.text, 'Command not supported');
test.ok(warnLogged);
test.done();
};
module.exports['Commands: namespace fallback handles LIST error'] = async test => {
let warnLogged = false;
const connection = createMockConnection({
state: 2,
capabilities: new Map(), // No NAMESPACE capability
exec: async () => {
throw new Error('LIST failed');
},
log: {
warn: () => {
warnLogged = true;
},
debug: () => {},
trace: () => {}
}
});
const result = await namespaceCommand(connection);
test.ok(result);
// Should return default namespace even on error
test.equal(result.prefix, '');
test.ok(warnLogged);
test.done();
};
module.exports['Commands: namespace appends delimiter to prefix if missing'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([['NAMESPACE', true]]),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.NAMESPACE) {
await opts.untagged.NAMESPACE({
attributes: [
// prefix without trailing delimiter
[[{ value: 'INBOX' }, { value: '.' }]],
null,
null
]
});
}
return { next: () => {} };
}
});
const result = await namespaceCommand(connection);
test.equal(result.prefix, 'INBOX.');
test.done();
};
module.exports['Commands: namespace fallback strips leading delimiter from prefix'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map(),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.LIST) {
await opts.untagged.LIST({
attributes: [
[{ value: '\\HasNoChildren' }],
{ value: '/' },
{ value: '/INBOX' } // Leading delimiter
]
});
}
return { next: () => {} };
}
});
const result = await namespaceCommand(connection);
test.equal(result.prefix, 'INBOX/');
test.done();
};
module.exports['Commands: namespace ignores empty NAMESPACE response attributes'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([['NAMESPACE', true]]),
exec: async (cmd, args, opts) => {
if (cmd === 'NAMESPACE' && opts && opts.untagged && opts.untagged.NAMESPACE) {
// Empty attributes - the callback should return early
await opts.untagged.NAMESPACE({
attributes: []
});
// Also provide a valid NAMESPACE to avoid error
await opts.untagged.NAMESPACE({
attributes: [[[{ value: 'INBOX.' }, { value: '.' }]], null, null]
});
}
return { next: () => {} };
}
});
const result = await namespaceCommand(connection);
// Should return namespace from the second call
test.equal(result.prefix, 'INBOX.');
test.equal(result.delimiter, '.');
test.done();
};
module.exports['Commands: namespace sets default when personal namespace is empty array'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([['NAMESPACE', true]]),
exec: async (cmd, args, opts) => {
if (cmd === 'NAMESPACE' && opts && opts.untagged && opts.untagged.NAMESPACE) {
// Provide an array where entries don't pass the filter
// (entry.length < 2), so getNamsepaceInfo returns []
await opts.untagged.NAMESPACE({
attributes: [
[[]], // array with one empty entry - filter removes it, returns []
null, // other
null // shared
]
});
}
return { next: () => {} };
}
});
const result = await namespaceCommand(connection);
// Should set default personal namespace when personal[0] is falsy
test.equal(result.prefix, '');
test.equal(result.delimiter, '.');
test.done();
};
module.exports['Commands: namespace fallback ignores empty LIST attributes'] = async test => {
let listCallCount = 0;
const connection = createMockConnection({
state: 2,
capabilities: new Map(), // No NAMESPACE capability
exec: async (cmd, args, opts) => {
if (cmd === 'LIST' && opts && opts.untagged && opts.untagged.LIST) {
listCallCount++;
// Empty attributes - the callback should return early
await opts.untagged.LIST({
attributes: []
});
}
return { next: () => {} };
}
});
const result = await namespaceCommand(connection);
test.ok(result);
test.equal(listCallCount, 1);
// With empty LIST, prefix and delimiter are undefined
test.equal(result.prefix, '');
test.done();
};
// ============================================
// QUOTA Command Tests
// ============================================
const quotaCommand = require('../lib/commands/quota');
module.exports['Commands: quota skips when not authenticated'] = async test => {
const connection = createMockConnection({ state: 1 }); // NOT_AUTHENTICATED
const result = await quotaCommand(connection, 'INBOX');
test.equal(result, undefined);
test.done();
};
module.exports['Commands: quota skips when no path'] = async test => {
const connection = createMockConnection({ state: 2 });
const result = await quotaCommand(connection, null);
test.equal(result, undefined);
test.done();
};
module.exports['Commands: quota returns false without capability'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map() // No QUOTA capability
});
const result = await quotaCommand(connection, 'INBOX');
test.equal(result, false);
test.done();
};
module.exports['Commands: quota with storage quota'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([['QUOTA', true]]),
exec: async (cmd, args, opts) => {
test.equal(cmd, 'GETQUOTAROOT');
if (opts && opts.untagged) {
if (opts.untagged.QUOTAROOT) {
await opts.untagged.QUOTAROOT({
attributes: [
{ value: 'INBOX' },
{ value: 'user.root' } // quota root
]
});
}
if (opts.untagged.QUOTA) {
await opts.untagged.QUOTA({
attributes: [
{ value: 'user.root' },
[
{ value: 'STORAGE' },
{ value: '500' }, // 500 KB used
{ value: '1000' } // 1000 KB limit
]
]
});
}
}
return { next: () => {} };
}
});
const result = await quotaCommand(connection, 'INBOX');
test.ok(result);
test.equal(result.path, 'INBOX');
test.equal(result.quotaRoot, 'user.root');
test.equal(result.storage.usage, 500 * 1024); // Converted to bytes
test.equal(result.storage.limit, 1000 * 1024);
test.equal(result.storage.status, '50%');
test.done();
};
module.exports['Commands: quota with message quota'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([['QUOTA', true]]),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.QUOTA) {
await opts.untagged.QUOTA({
attributes: [{ value: 'root' }, [{ value: 'MESSAGE' }, { value: '100' }, { value: '1000' }]]
});
}
return { next: () => {} };
}
});
const result = await quotaCommand(connection, 'INBOX');
test.ok(result);
// MESSAGE quota is not multiplied by 1024
test.equal(result.message.usage, 100);
test.equal(result.message.limit, 1000);
test.equal(result.message.status, '10%');
test.done();
};
module.exports['Commands: quota with multiple quota types'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([['QUOTA', true]]),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.QUOTA) {
await opts.untagged.QUOTA({
attributes: [
{ value: '' },
[{ value: 'STORAGE' }, { value: '250' }, { value: '500' }, { value: 'MESSAGE' }, { value: '50' }, { value: '100' }]
]
});
}
return { next: () => {} };
}
});
const result = await quotaCommand(connection, 'INBOX');
test.ok(result.storage);
test.ok(result.message);
test.equal(result.storage.usage, 250 * 1024);
test.equal(result.message.usage, 50);
test.done();
};
module.exports['Commands: quota fetches GETQUOTA when quotaRoot but no QUOTA response'] = async test => {
let getQuotaCalled = false;
const connection = createMockConnection({
state: 2,
capabilities: new Map([['QUOTA', true]]),
exec: async (cmd, args, opts) => {
if (cmd === 'GETQUOTAROOT') {
if (opts && opts.untagged && opts.untagged.QUOTAROOT) {
await opts.untagged.QUOTAROOT({
attributes: [{ value: 'INBOX' }, { value: 'user.root' }]
});
}
// No QUOTA response
} else if (cmd === 'GETQUOTA') {
getQuotaCalled = true;
test.deepEqual(args, [{ type: 'ATOM', value: 'user.root' }]);
if (opts && opts.untagged && opts.untagged.QUOTA) {
await opts.untagged.QUOTA({
attributes: [{ value: 'user.root' }, [{ value: 'STORAGE' }, { value: '100' }, { value: '200' }]]
});
}
}
return { next: () => {} };
}
});
const result = await quotaCommand(connection, 'INBOX');
test.ok(getQuotaCalled);
test.equal(result.quotaRoot, 'user.root');
test.equal(result.storage.usage, 100 * 1024);
test.done();
};
module.exports['Commands: quota handles zero limit'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([['QUOTA', true]]),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.QUOTA) {
await opts.untagged.QUOTA({
attributes: [
{ value: '' },
[
{ value: 'STORAGE' },
{ value: '0' },
{ value: '0' } // Zero limit
]
]
});
}
return { next: () => {} };
}
});
const result = await quotaCommand(connection, 'INBOX');
test.ok(result.storage);
test.equal(result.storage.usage, 0);
test.equal(result.storage.limit, 0);
// No status when limit is 0
test.equal(result.storage.status, undefined);
test.done();
};
module.exports['Commands: quota handles empty attributes'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([['QUOTA', true]]),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.QUOTA) {
await opts.untagged.QUOTA({
attributes: [
{ value: '' },
[] // Empty quota list
]
});
}
return { next: () => {} };
}
});
const result = await quotaCommand(connection, 'INBOX');
test.ok(result);
test.equal(result.path, 'INBOX');
test.equal(result.storage, undefined);
test.done();
};
module.exports['Commands: quota works in SELECTED state'] = async test => {
const connection = createMockConnection({
state: 3, // SELECTED
capabilities: new Map([['QUOTA', true]]),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.QUOTA) {
await opts.untagged.QUOTA({
attributes: [{ value: '' }, [{ value: 'STORAGE' }, { value: '10' }, { value: '100' }]]
});
}
return { next: () => {} };
}
});
const result = await quotaCommand(connection, 'INBOX');
test.ok(result);
test.equal(result.storage.status, '10%');
test.done();
};
module.exports['Commands: quota handles error'] = async test => {
let warnLogged = false;
const connection = createMockConnection({
state: 2,
capabilities: new Map([['QUOTA', true]]),
exec: async () => {
const err = new Error('Quota failed');
err.response = { attributes: [] };
throw err;
},
log: {
warn: () => {
warnLogged = true;
},
debug: () => {},
trace: () => {}
}
});
const result = await quotaCommand(connection, 'INBOX');
test.equal(result, false);
test.ok(warnLogged);
test.done();
};
module.exports['Commands: quota handles error with status code'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([['QUOTA', true]]),
exec: async () => {
const err = new Error('Quota failed');
err.response = {
tag: 'A1',
command: 'NO',
attributes: [
{
type: 'ATOM',
value: '',
section: [{ type: 'ATOM', value: 'NOQUOTA' }]
}
]
};
throw err;
},
log: {
warn: () => {},
debug: () => {},
trace: () => {}
}
});
const result = await quotaCommand(connection, 'INBOX');
test.equal(result, false);
test.done();
};
module.exports['Commands: quota normalizes path'] = async test => {
let capturedArgs = null;
const connection = createMockConnection({
state: 2,
capabilities: new Map([['QUOTA', true]]),
namespace: { delimiter: '/', prefix: 'INBOX/' },
exec: async (cmd, args) => {
capturedArgs = args;
return { next: () => {} };
}
});
await quotaCommand(connection, 'Subfolder');
test.ok(capturedArgs);
test.done();
};
module.exports['Commands: quota handles non-numeric values'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([['QUOTA', true]]),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.QUOTA) {
await opts.untagged.QUOTA({
attributes: [
{ value: '' },
[
{ value: 'STORAGE' },
{ value: 'invalid' }, // Non-numeric usage
{ value: 'also-invalid' } // Non-numeric limit
]
]
});
}
return { next: () => {} };
}
});
const result = await quotaCommand(connection, 'INBOX');
test.ok(result);
// Non-numeric values should be skipped - no storage data set
test.equal(result.storage, undefined);
test.done();
};
module.exports['Commands: quota calculates percentage correctly'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([['QUOTA', true]]),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.QUOTA) {
await opts.untagged.QUOTA({
attributes: [{ value: '' }, [{ value: 'MESSAGE' }, { value: '333' }, { value: '1000' }]]
});
}
return { next: () => {} };
}
});
const result = await quotaCommand(connection, 'INBOX');
test.equal(result.message.status, '33%'); // Rounded
test.done();
};
module.exports['Commands: quota handles falsy key in attributes'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([['QUOTA', true]]),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.QUOTA) {
// First attribute (i=0) has invalid key (null value), so key becomes false
// Then i=1 and i=2 should be skipped due to !key check
await opts.untagged.QUOTA({
attributes: [
{ value: '' },
[
{ value: null }, // Invalid key at i=0 -> key = false
{ value: '100' }, // i=1, skipped because !key
{ value: '1000' } // i=2, skipped because !key
]
]
});
}
return { next: () => {} };
}
});
const result = await quotaCommand(connection, 'INBOX');
test.ok(result);
// No quota data should be set since key was falsy
test.equal(Object.keys(result).filter(k => k !== 'path').length, 0);
test.done();
};
module.exports['Commands: quota sets limit without prior usage'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([['QUOTA', true]]),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.QUOTA) {
// Provide only the limit (i=2) without usage (i=1) being valid
await opts.untagged.QUOTA({
attributes: [
{ value: '' },
[
{ value: 'STORAGE' }, // i=0, key = 'storage'
{ value: 'invalid' }, // i=1, usage - invalid number, skipped
{ value: '1000' } // i=2, limit - should create map[key] first
]
]
});
}
return { next: () => {} };
}
});
const result = await quotaCommand(connection, 'INBOX');
test.ok(result);
test.ok(result.storage);
test.equal(result.storage.limit, 1024000); // 1000 * 1024 for storage
test.equal(result.storage.usage, undefined);
test.done();
};
// ============================================
// AUTHENTICATE Command Tests
// ============================================
const authenticateCommand = require('../lib/commands/authenticate');
module.exports['Commands: authenticate skips when already authenticated'] = async test => {
const connection = createMockConnection({
state: 2 // AUTHENTICATED
});
const result = await authenticateCommand(connection, 'user', { password: 'pass' });
test.equal(result, undefined);
test.done();
};
module.exports['Commands: authenticate with OAUTHBEARER'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 1, // NOT_AUTHENTICATED
capabilities: new Map([['AUTH=OAUTHBEARER', true]]),
servername: 'imap.example.com',
authCapabilities: new Map(),
exec: async (cmd, args) => {
execArgs = { cmd, args };
return { next: () => {} };
},
write: () => {}
});
const result = await authenticateCommand(connection, 'user@example.com', { accessToken: 'token123' });
test.equal(result, 'user@example.com');
test.equal(execArgs.cmd, 'AUTHENTICATE');
test.equal(execArgs.args[0].value, 'OAUTHBEARER');
test.ok(connection.authCapabilities.has('AUTH=OAUTHBEARER'));
test.done();
};
module.exports['Commands: authenticate with XOAUTH2'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 1,
capabilities: new Map([['AUTH=XOAUTH2', true]]),
servername: 'imap.example.com',
authCapabilities: new Map(),
exec: async (cmd, args) => {
execArgs = { cmd, args };
return { next: () => {} };
},
write: () => {}
});
const result = await authenticateCommand(connection, 'user@example.com', { accessToken: 'token123' });
test.equal(result, 'user@example.com');
test.equal(execArgs.args[0].value, 'XOAUTH2');
test.ok(connection.authCapabilities.has('AUTH=XOAUTH2'));
test.done();
};
module.exports['Commands: authenticate with XOAUTH (legacy)'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 1,
capabilities: new Map([['AUTH=XOAUTH', true]]),
servername: 'imap.example.com',
authCapabilities: new Map(),
exec: async (cmd, args) => {
execArgs = { cmd, args };
return { next: () => {} };
},
write: () => {}
});
const result = await authenticateCommand(connection, 'user@example.com', { accessToken: 'token123' });
test.equal(result, 'user@example.com');
test.equal(execArgs.args[0].value, 'XOAUTH2');
test.done();
};
module.exports['Commands: authenticate OAuth handles error response'] = async test => {
const connection = createMockConnection({
state: 1,
capabilities: new Map([['AUTH=OAUTHBEARER', true]]),
servername: 'imap.example.com',
authCapabilities: new Map(),
exec: async (cmd, args, opts) => {
// Simulate server sending error in plus tag
if (opts && opts.onPlusTag) {
const errorJson = Buffer.from(JSON.stringify({ status: '401', error: 'invalid_token' })).toString('base64');
await opts.onPlusTag({
attributes: [{ type: 'TEXT', value: errorJson }]
});
}
const err = new Error('Authentication failed');
err.response = { attributes: [] };
throw err;
},
write: () => {},
log: {
debug: () => {},
warn: () => {},
trace: () => {}
}
});
try {
await authenticateCommand(connection, 'user@example.com', { accessToken: 'bad_token' });
test.ok(false, 'Should have thrown');
} catch (err) {
test.ok(err.authenticationFailed);
test.ok(err.oauthError);
test.equal(err.oauthError.status, '401');
}
test.done();
};
module.exports['Commands: authenticate OAuth handles malformed error response'] = async test => {
let debugLogged = false;
const connection = createMockConnection({
state: 1,
capabilities: new Map([['AUTH=OAUTHBEARER', true]]),
servername: 'imap.example.com',
authCapabilities: new Map(),
exec: async (cmd, args, opts) => {
if (opts && opts.onPlusTag) {
// Malformed base64/JSON
await opts.onPlusTag({
attributes: [{ type: 'TEXT', value: 'not-valid-base64!' }]
});
}
const err = new Error('Authentication failed');
err.response = { attributes: [] };
throw err;
},
write: () => {},
log: {
debug: () => {
debugLogged = true;
},
warn: () => {},
trace: () => {}
}
});
try {
await authenticateCommand(connection, 'user@example.com', { accessToken: 'token' });
test.ok(false, 'Should have thrown');
} catch (err) {
test.ok(err.authenticationFailed);
test.ok(debugLogged); // Should log the parse error
test.equal(err.oauthError, undefined); // No oauthError since parse failed
}
test.done();
};
module.exports['Commands: authenticate OAuth error with serverResponseCode'] = async test => {
const connection = createMockConnection({
state: 1,
capabilities: new Map([['AUTH=OAUTHBEARER', true]]),
servername: 'imap.example.com',
authCapabilities: new Map(),
exec: async (cmd, args, opts) => {
if (opts && opts.onPlusTag) {
await opts.onPlusTag({});
}
const err = new Error('Authentication failed');
err.response = {
tag: 'A1',
command: 'NO',
attributes: [
{
type: 'SECTION',
section: [{ type: 'ATOM', value: 'AUTHORIZATIONFAILED' }]
},
{ type: 'TEXT', value: 'OAuth token expired' }
]
};
throw err;
},
write: () => {},
log: {
debug: () => {},
warn: () => {},
trace: () => {}
}
});
try {
await authenticateCommand(connection, 'user@example.com', { accessToken: 'expired_token' });
test.ok(false, 'Should have thrown');
} catch (err) {
test.ok(err.authenticationFailed);
test.equal(err.serverResponseCode, 'AUTHORIZATIONFAILED');
}
test.done();
};
module.exports['Commands: authenticate with PLAIN'] = async test => {
let execArgs = null;
let writtenData = null;
const connection = createMockConnection({
state: 1,
capabilities: new Map([['AUTH=PLAIN', true]]),
authCapabilities: new Map(),
exec: async (cmd, args, opts) => {
execArgs = { cmd, args };
if (opts && opts.onPlusTag) {
await opts.onPlusTag({});
}
return { next: () => {} };
},
write: data => {
writtenData = data;
},
log: {
debug: () => {},
warn: () => {},
trace: () => {}
}
});
const result = await authenticateCommand(connection, 'testuser', { password: 'testpass' });
test.equal(result, 'testuser');
test.equal(execArgs.cmd, 'AUTHENTICATE');
test.equal(execArgs.args[0].value, 'PLAIN');
// Verify PLAIN format: \x00username\x00password
const decoded = Buffer.from(writtenData, 'base64').toString();
test.equal(decoded, '\x00testuser\x00testpass');
test.ok(connection.authCapabilities.has('AUTH=PLAIN'));
test.done();
};
module.exports['Commands: authenticate with PLAIN and authzid'] = async test => {
let writtenData = null;
const connection = createMockConnection({
state: 1,
capabilities: new Map([['AUTH=PLAIN', true]]),
authCapabilities: new Map(),
exec: async (cmd, args, opts) => {
if (opts && opts.onPlusTag) {
await opts.onPlusTag({});
}
return { next: () => {} };
},
write: data => {
writtenData = data;
},
log: {
debug: () => {},
warn: () => {},
trace: () => {}
}
});
const result = await authenticateCommand(connection, 'admin', {
password: 'adminpass',
authzid: 'impersonated_user'
});
test.equal(result, 'impersonated_user'); // Returns authzid when provided
// Verify PLAIN format with authzid: authzid\x00username\x00password
const decoded = Buffer.from(writtenData, 'base64').toString();
test.equal(decoded, 'impersonated_user\x00admin\x00adminpass');
test.done();
};
module.exports['Commands: authenticate with PLAIN forced via loginMethod'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 1,
capabilities: new Map([
['AUTH=LOGIN', true],
['AUTH=PLAIN', true]
]),
authCapabilities: new Map(),
exec: async (cmd, args, opts) => {
execArgs = { cmd, args };
if (opts && opts.onPlusTag) {
await opts.onPlusTag({});
}
return { next: () => {} };
},
write: () => {},
log: {
debug: () => {},
warn: () => {},
trace: () => {}
}
});
await authenticateCommand(connection, 'user', { password: 'pass', loginMethod: 'AUTH=PLAIN' });
test.equal(execArgs.args[0].value, 'PLAIN');
test.done();
};
module.exports['Commands: authenticate with LOGIN'] = async test => {
let execArgs = null;
let writeCount = 0;
let writtenValues = [];
const connection = createMockConnection({
state: 1,
capabilities: new Map([['AUTH=LOGIN', true]]),
authCapabilities: new Map(),
exec: async (cmd, args, opts) => {
execArgs = { cmd, args };
if (opts && opts.onPlusTag) {
// Simulate server prompts
await opts.onPlusTag({
attributes: [{ type: 'TEXT', value: Buffer.from('Username:').toString('base64') }]
});
await opts.onPlusTag({
attributes: [{ type: 'TEXT', value: Buffer.from('Password:').toString('base64') }]
});
}
return { next: () => {} };
},
write: data => {
writeCount++;
writtenValues.push(Buffer.from(data, 'base64').toString());
},
log: {
debug: () => {},
warn: () => {},
trace: () => {}
}
});
const result = await authenticateCommand(connection, 'loginuser', { password: 'loginpass' });
test.equal(result, 'loginuser');
test.equal(execArgs.args[0].value, 'LOGIN');
test.equal(writeCount, 2);
test.equal(writtenValues[0], 'loginuser');
test.equal(writtenValues[1], 'loginpass');
test.ok(connection.authCapabilities.has('AUTH=LOGIN'));
test.done();
};
module.exports['Commands: authenticate with LOGIN handles user name prompt'] = async test => {
let writtenValues = [];
const connection = createMockConnection({
state: 1,
capabilities: new Map([['AUTH=LOGIN', true]]),
authCapabilities: new Map(),
exec: async (cmd, args, opts) => {
if (opts && opts.onPlusTag) {
// Some servers use "User Name" instead of "Username"
await opts.onPlusTag({
attributes: [{ type: 'TEXT', value: Buffer.from('User Name:').toString('base64') }]
});
await opts.onPlusTag({
attributes: [{ type: 'TEXT', value: Buffer.from('Password').toString('base64') }]
});
}
return { next: () => {} };
},
write: data => {
writtenValues.push(Buffer.from(data, 'base64').toString());
},
log: {
debug: () => {},
warn: () => {},
trace: () => {}
}
});
await authenticateCommand(connection, 'testuser', { password: 'testpass' });
test.equal(writtenValues[0], 'testuser');
test.equal(writtenValues[1], 'testpass');
test.done();
};
module.exports['Commands: authenticate with LOGIN throws on unknown question'] = async test => {
const connection = createMockConnection({
state: 1,
capabilities: new Map([['AUTH=LOGIN', true]]),
authCapabilities: new Map(),
exec: async (cmd, args, opts) => {
if (opts && opts.onPlusTag) {
await opts.onPlusTag({
attributes: [{ type: 'TEXT', value: Buffer.from('Unknown Question:').toString('base64') }]
});
}
return { next: () => {} };
},
write: () => {},
log: {
debug: () => {},
warn: () => {},
trace: () => {}
}
});
try {
await authenticateCommand(connection, 'user', { password: 'pass' });
test.ok(false, 'Should have thrown');
} catch (err) {
test.ok(err.message.includes('Unknown LOGIN question'));
}
test.done();
};
module.exports['Commands: authenticate with LOGIN forced via loginMethod'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 1,
capabilities: new Map([
['AUTH=PLAIN', true],
['AUTH=LOGIN', true]
]),
authCapabilities: new Map(),
exec: async (cmd, args, opts) => {
execArgs = { cmd, args };
if (opts && opts.onPlusTag) {
await opts.onPlusTag({
attributes: [{ type: 'TEXT', value: Buffer.from('Username:').toString('base64') }]
});
await opts.onPlusTag({
attributes: [{ type: 'TEXT', value: Buffer.from('Password:').toString('base64') }]
});
}
return { next: () => {} };
},
write: () => {},
log: {
debug: () => {},
warn: () => {},
trace: () => {}
}
});
await authenticateCommand(connection, 'user', { password: 'pass', loginMethod: 'AUTH=LOGIN' });
test.equal(execArgs.args[0].value, 'LOGIN');
test.done();
};
module.exports['Commands: authenticate PLAIN handles error'] = async test => {
const connection = createMockConnection({
state: 1,
capabilities: new Map([['AUTH=PLAIN', true]]),
authCapabilities: new Map(),
exec: async (cmd, args, opts) => {
if (opts && opts.onPlusTag) {
await opts.onPlusTag({});
}
const err = new Error('Authentication failed');
err.response = {
tag: 'A1',
command: 'NO',
attributes: [
{
type: 'ATOM',
value: '',
section: [{ type: 'ATOM', value: 'AUTHENTICATIONFAILED' }]
}
]
};
throw err;
},
write: () => {},
log: {
debug: () => {},
warn: () => {},
trace: () => {}
}
});
try {
await authenticateCommand(connection, 'user', { password: 'wrongpass' });
test.ok(false, 'Should have thrown');
} catch (err) {
test.ok(err.authenticationFailed);
test.equal(err.serverResponseCode, 'AUTHENTICATIONFAILED');
}
test.done();
};
module.exports['Commands: authenticate LOGIN handles error'] = async test => {
const connection = createMockConnection({
state: 1,
capabilities: new Map([['AUTH=LOGIN', true]]),
authCapabilities: new Map(),
exec: async () => {
const err = new Error('Login failed');
err.response = {
tag: 'A1',
command: 'NO',
attributes: [
{
type: 'SECTION',
section: [{ type: 'ATOM', value: 'AUTHENTICATIONFAILED' }]
},
{ type: 'TEXT', value: 'Invalid credentials' }
]
};
throw err;
},
write: () => {},
log: {
debug: () => {},
warn: () => {},
trace: () => {}
}
});
try {
await authenticateCommand(connection, 'user', { password: 'pass' });
test.ok(false, 'Should have thrown');
} catch (err) {
test.ok(err.authenticationFailed);
test.equal(err.serverResponseCode, 'AUTHENTICATIONFAILED');
}
test.done();
};
module.exports['Commands: authenticate throws unsupported mechanism'] = async test => {
const connection = createMockConnection({
state: 1,
capabilities: new Map() // No auth capabilities
});
try {
await authenticateCommand(connection, 'user', { password: 'pass' });
test.ok(false, 'Should have thrown');
} catch (err) {
test.ok(err.message.includes('Unsupported authentication mechanism'));
}
test.done();
};
module.exports['Commands: authenticate throws unsupported for accessToken without OAuth capability'] = async test => {
const connection = createMockConnection({
state: 1,
capabilities: new Map([['AUTH=PLAIN', true]]) // No OAuth capability
});
try {
await authenticateCommand(connection, 'user', { accessToken: 'token123' });
test.ok(false, 'Should have thrown');
} catch (err) {
test.ok(err.message.includes('Unsupported authentication mechanism'));
}
test.done();
};
module.exports['Commands: authenticate prefers PLAIN over LOGIN by default'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 1,
capabilities: new Map([
['AUTH=LOGIN', true],
['AUTH=PLAIN', true]
]),
authCapabilities: new Map(),
exec: async (cmd, args, opts) => {
execArgs = { cmd, args };
if (opts && opts.onPlusTag) {
await opts.onPlusTag({});
}
return { next: () => {} };
},
write: () => {},
log: {
debug: () => {},
warn: () => {},
trace: () => {}
}
});
await authenticateCommand(connection, 'user', { password: 'pass' });
test.equal(execArgs.args[0].value, 'PLAIN'); // PLAIN should be preferred
test.done();
};
module.exports['Commands: authenticate prefers OAuth when accessToken provided'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 1,
capabilities: new Map([
['AUTH=PLAIN', true],
['AUTH=OAUTHBEARER', true]
]),
servername: 'imap.example.com',
authCapabilities: new Map(),
exec: async (cmd, args) => {
execArgs = { cmd, args };
return { next: () => {} };
},
write: () => {},
log: {
debug: () => {},
warn: () => {},
trace: () => {}
}
});
await authenticateCommand(connection, 'user', { accessToken: 'token', password: 'pass' });
test.equal(execArgs.args[0].value, 'OAUTHBEARER'); // OAuth preferred when token provided
test.done();
};
// ============================================
// CREATE Command Tests
// ============================================
module.exports['Commands: create skips when not authenticated'] = async test => {
const connection = createMockConnection({ state: 1 }); // NOT_AUTHENTICATED
const result = await createCommand(connection, 'NewFolder');
test.equal(result, undefined);
test.done();
};
module.exports['Commands: create mailbox success'] = async test => {
let execArgs = null;
let subscribeCalled = false;
const connection = createMockConnection({
state: 2, // AUTHENTICATED
exec: async (cmd, args) => {
execArgs = { cmd, args };
return {
next: () => {},
response: { attributes: [] }
};
},
run: async (cmd, path) => {
if (cmd === 'SUBSCRIBE') {
subscribeCalled = true;
test.equal(path, 'NewFolder');
}
}
});
const result = await createCommand(connection, 'NewFolder');
test.ok(result);
test.equal(result.path, 'NewFolder');
test.equal(result.created, true);
test.equal(execArgs.cmd, 'CREATE');
test.ok(subscribeCalled);
test.done();
};
module.exports['Commands: create works in SELECTED state'] = async test => {
const connection = createMockConnection({
state: 3, // SELECTED
exec: async () => ({
next: () => {},
response: { attributes: [] }
}),
run: async () => {}
});
const result = await createCommand(connection, 'NewFolder');
test.ok(result);
test.equal(result.created, true);
test.done();
};
module.exports['Commands: create with MAILBOXID response'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async () => ({
next: () => {},
response: {
attributes: [
{
section: [{ value: 'MAILBOXID' }, [{ value: 'F12345' }]]
}
]
}
}),
run: async () => {}
});
const result = await createCommand(connection, 'NewFolder');
test.ok(result);
test.equal(result.mailboxId, 'F12345');
test.equal(result.created, true);
test.done();
};
module.exports['Commands: create normalizes path'] = async test => {
let capturedArgs = null;
const connection = createMockConnection({
state: 2,
namespace: { delimiter: '/', prefix: 'INBOX/' },
exec: async (cmd, args) => {
capturedArgs = args;
return {
next: () => {},
response: { attributes: [] }
};
},
run: async () => {}
});
await createCommand(connection, 'Subfolder');
test.ok(capturedArgs);
test.done();
};
module.exports['Commands: create handles ALREADYEXISTS'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async () => {
const err = new Error('Mailbox already exists');
err.response = {
tag: 'A1',
command: 'NO',
attributes: [
{
type: 'ATOM',
value: '',
section: [{ type: 'ATOM', value: 'ALREADYEXISTS' }]
}
]
};
throw err;
},
run: async () => {},
log: {
warn: () => {},
debug: () => {},
trace: () => {}
}
});
const result = await createCommand(connection, 'ExistingFolder');
test.ok(result);
test.equal(result.path, 'ExistingFolder');
test.equal(result.created, false);
test.done();
};
module.exports['Commands: create throws on other errors'] = async test => {
let warnLogged = false;
const connection = createMockConnection({
state: 2,
exec: async () => {
const err = new Error('Permission denied');
err.response = {
tag: 'A1',
command: 'NO',
attributes: [
{
type: 'ATOM',
value: '',
section: [{ type: 'ATOM', value: 'NOPERM' }]
}
]
};
throw err;
},
run: async () => {},
log: {
warn: () => {
warnLogged = true;
},
debug: () => {},
trace: () => {}
}
});
try {
await createCommand(connection, 'RestrictedFolder');
test.ok(false, 'Should have thrown');
} catch (err) {
test.equal(err.serverResponseCode, 'NOPERM');
test.ok(warnLogged);
}
test.done();
};
module.exports['Commands: create handles empty section'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async () => ({
next: () => {},
response: {
attributes: [
{
section: [] // Empty section
}
]
}
}),
run: async () => {}
});
const result = await createCommand(connection, 'NewFolder');
test.ok(result);
test.equal(result.created, true);
test.equal(result.mailboxId, undefined);
test.done();
};
module.exports['Commands: create handles invalid MAILBOXID format'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async () => ({
next: () => {},
response: {
attributes: [
{
section: [
{ value: 'MAILBOXID' },
{ value: 'not-an-array' } // Should be array
]
}
]
}
}),
run: async () => {}
});
const result = await createCommand(connection, 'NewFolder');
test.ok(result);
test.equal(result.created, true);
// mailboxId should not be set due to invalid format
test.equal(result.mailboxId, undefined);
test.done();
};
module.exports['Commands: create handles null key in section'] = async test => {
const connection = createMockConnection({
state: 2,
exec: async () => ({
next: () => {},
response: {
attributes: [
{
section: [
null, // null key
[{ value: 'F12345' }]
]
}
]
}
}),
run: async () => {}
});
const result = await createCommand(connection, 'NewFolder');
test.ok(result);
test.equal(result.created, true);
test.done();
};
// ============================================
// ENABLE Command Tests
// ============================================
module.exports['Commands: enable skips without ENABLE capability'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map() // No ENABLE capability
});
const result = await enableCommand(connection, ['CONDSTORE']);
test.equal(result, undefined);
test.done();
};
module.exports['Commands: enable skips when not authenticated'] = async test => {
const connection = createMockConnection({
state: 3, // SELECTED - not AUTHENTICATED
capabilities: new Map([['ENABLE', true]])
});
const result = await enableCommand(connection, ['CONDSTORE']);
test.equal(result, undefined);
test.done();
};
module.exports['Commands: enable skips when no supported extensions'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([['ENABLE', true]]) // Has ENABLE but not CONDSTORE
});
const result = await enableCommand(connection, ['CONDSTORE']);
test.equal(result, undefined);
test.done();
};
module.exports['Commands: enable single extension'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 2,
capabilities: new Map([
['ENABLE', true],
['CONDSTORE', true]
]),
enabled: new Set(),
exec: async (cmd, args, opts) => {
execArgs = { cmd, args };
if (opts && opts.untagged && opts.untagged.ENABLED) {
await opts.untagged.ENABLED({
attributes: [{ value: 'CONDSTORE' }]
});
}
return { next: () => {} };
}
});
const result = await enableCommand(connection, ['CONDSTORE']);
test.ok(result instanceof Set);
test.ok(result.has('CONDSTORE'));
test.equal(execArgs.cmd, 'ENABLE');
test.equal(execArgs.args[0].value, 'CONDSTORE');
test.ok(connection.enabled.has('CONDSTORE'));
test.done();
};
module.exports['Commands: enable multiple extensions'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 2,
capabilities: new Map([
['ENABLE', true],
['CONDSTORE', true],
['QRESYNC', true]
]),
enabled: new Set(),
exec: async (cmd, args, opts) => {
execArgs = { cmd, args };
if (opts && opts.untagged && opts.untagged.ENABLED) {
await opts.untagged.ENABLED({
attributes: [{ value: 'CONDSTORE' }, { value: 'QRESYNC' }]
});
}
return { next: () => {} };
}
});
const result = await enableCommand(connection, ['CONDSTORE', 'QRESYNC']);
test.ok(result instanceof Set);
test.ok(result.has('CONDSTORE'));
test.ok(result.has('QRESYNC'));
test.equal(execArgs.args.length, 2);
test.done();
};
module.exports['Commands: enable filters unsupported extensions'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 2,
capabilities: new Map([
['ENABLE', true],
['CONDSTORE', true]
// QRESYNC not supported
]),
enabled: new Set(),
exec: async (cmd, args, opts) => {
execArgs = { cmd, args };
if (opts && opts.untagged && opts.untagged.ENABLED) {
await opts.untagged.ENABLED({
attributes: [{ value: 'CONDSTORE' }]
});
}
return { next: () => {} };
}
});
const result = await enableCommand(connection, ['CONDSTORE', 'QRESYNC']);
test.ok(result instanceof Set);
test.ok(result.has('CONDSTORE'));
test.ok(!result.has('QRESYNC'));
// Only CONDSTORE should be in the request
test.equal(execArgs.args.length, 1);
test.equal(execArgs.args[0].value, 'CONDSTORE');
test.done();
};
module.exports['Commands: enable converts to uppercase'] = async test => {
let execArgs = null;
const connection = createMockConnection({
state: 2,
capabilities: new Map([
['ENABLE', true],
['CONDSTORE', true]
]),
enabled: new Set(),
exec: async (cmd, args, opts) => {
execArgs = { cmd, args };
if (opts && opts.untagged && opts.untagged.ENABLED) {
await opts.untagged.ENABLED({
attributes: [{ value: 'condstore' }]
});
}
return { next: () => {} };
}
});
const result = await enableCommand(connection, ['condstore']); // lowercase
test.ok(result instanceof Set);
test.ok(result.has('CONDSTORE')); // Stored as uppercase
test.equal(execArgs.args[0].value, 'CONDSTORE'); // Sent as uppercase
test.done();
};
module.exports['Commands: enable handles empty ENABLED response'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([
['ENABLE', true],
['CONDSTORE', true]
]),
enabled: new Set(),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.ENABLED) {
await opts.untagged.ENABLED({
attributes: [] // Empty
});
}
return { next: () => {} };
}
});
const result = await enableCommand(connection, ['CONDSTORE']);
test.ok(result instanceof Set);
test.equal(result.size, 0);
test.done();
};
module.exports['Commands: enable handles null attributes'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([
['ENABLE', true],
['CONDSTORE', true]
]),
enabled: new Set(),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.ENABLED) {
await opts.untagged.ENABLED({
attributes: null
});
}
return { next: () => {} };
}
});
const result = await enableCommand(connection, ['CONDSTORE']);
test.ok(result instanceof Set);
test.equal(result.size, 0);
test.done();
};
module.exports['Commands: enable trims response values'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([
['ENABLE', true],
['CONDSTORE', true]
]),
enabled: new Set(),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.ENABLED) {
await opts.untagged.ENABLED({
attributes: [{ value: ' CONDSTORE ' }] // With whitespace
});
}
return { next: () => {} };
}
});
const result = await enableCommand(connection, ['CONDSTORE']);
test.ok(result.has('CONDSTORE'));
test.done();
};
module.exports['Commands: enable handles error'] = async test => {
let warnLogged = false;
const connection = createMockConnection({
state: 2,
capabilities: new Map([
['ENABLE', true],
['CONDSTORE', true]
]),
enabled: new Set(),
exec: async () => {
throw new Error('Enable failed');
},
log: {
warn: () => {
warnLogged = true;
},
debug: () => {},
trace: () => {}
}
});
const result = await enableCommand(connection, ['CONDSTORE']);
test.equal(result, false);
test.ok(warnLogged);
test.done();
};
module.exports['Commands: enable skips non-string attribute values'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([
['ENABLE', true],
['CONDSTORE', true]
]),
enabled: new Set(),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.ENABLED) {
await opts.untagged.ENABLED({
attributes: [
{ value: 'CONDSTORE' },
{ value: null }, // null value
{ value: 123 }, // number value
{ notValue: 'test' } // missing value property
]
});
}
return { next: () => {} };
}
});
const result = await enableCommand(connection, ['CONDSTORE']);
test.ok(result instanceof Set);
test.equal(result.size, 1);
test.ok(result.has('CONDSTORE'));
test.done();
};
module.exports['Commands: enable updates connection.enabled'] = async test => {
const connection = createMockConnection({
state: 2,
capabilities: new Map([
['ENABLE', true],
['CONDSTORE', true],
['UTF8=ACCEPT', true]
]),
enabled: new Set(['EXISTING']),
exec: async (cmd, args, opts) => {
if (opts && opts.untagged && opts.untagged.ENABLED) {
await opts.untagged.ENABLED({
attributes: [{ value: 'CONDSTORE' }, { value: 'UTF8=ACCEPT' }]
});
}
return { next: () => {} };
}
});
await enableCommand(connection, ['CONDSTORE', 'UTF8=ACCEPT']);
// connection.enabled should be replaced with new set
test.ok(connection.enabled.has('CONDSTORE'));
test.ok(connection.enabled.has('UTF8=ACCEPT'));
test.ok(!connection.enabled.has('EXISTING')); // Old value should be gone
test.done();
};