UNPKG

imapflow

Version:

IMAP Client for Node

1,619 lines (1,428 loc) 272 kB
'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