imapflow
Version:
IMAP Client for Node
1,619 lines (1,428 loc) • 272 kB
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.d