imapflow 1.7.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +8 -2
- package/dist/cjs/charsets.d.ts +1 -0
- package/dist/cjs/charsets.js +294 -0
- package/dist/cjs/commands/append.d.ts +22 -0
- package/dist/cjs/commands/append.js +151 -0
- package/dist/cjs/commands/authenticate.d.ts +24 -0
- package/dist/cjs/commands/authenticate.js +223 -0
- package/dist/cjs/commands/capability.d.ts +8 -0
- package/dist/cjs/commands/capability.js +32 -0
- package/dist/cjs/commands/close.d.ts +8 -0
- package/dist/cjs/commands/close.js +39 -0
- package/dist/cjs/commands/compress.d.ts +8 -0
- package/dist/cjs/commands/compress.js +56 -0
- package/dist/cjs/commands/copy.d.ts +13 -0
- package/dist/cjs/commands/copy.js +44 -0
- package/dist/cjs/commands/copyuid-parser.d.ts +11 -0
- package/dist/cjs/commands/copyuid-parser.js +32 -0
- package/dist/cjs/commands/create.d.ts +11 -0
- package/dist/cjs/commands/create.js +80 -0
- package/dist/cjs/commands/delete.d.ts +11 -0
- package/dist/cjs/commands/delete.js +40 -0
- package/dist/cjs/commands/enable.d.ts +9 -0
- package/dist/cjs/commands/enable.js +61 -0
- package/dist/cjs/commands/esearch-parser.d.ts +17 -0
- package/dist/cjs/commands/esearch-parser.js +91 -0
- package/dist/cjs/commands/expunge.d.ts +12 -0
- package/dist/cjs/commands/expunge.js +60 -0
- package/dist/cjs/commands/fetch.d.ts +30 -0
- package/dist/cjs/commands/fetch.js +241 -0
- package/dist/cjs/commands/id.d.ts +10 -0
- package/dist/cjs/commands/id.js +80 -0
- package/dist/cjs/commands/idle.d.ts +9 -0
- package/dist/cjs/commands/idle.js +347 -0
- package/dist/cjs/commands/list.d.ts +16 -0
- package/dist/cjs/commands/list.js +518 -0
- package/dist/cjs/commands/login.d.ts +11 -0
- package/dist/cjs/commands/login.js +42 -0
- package/dist/cjs/commands/logout.d.ts +8 -0
- package/dist/cjs/commands/logout.js +47 -0
- package/dist/cjs/commands/move.d.ts +13 -0
- package/dist/cjs/commands/move.js +57 -0
- package/dist/cjs/commands/namespace.d.ts +25 -0
- package/dist/cjs/commands/namespace.js +139 -0
- package/dist/cjs/commands/noop.d.ts +8 -0
- package/dist/cjs/commands/noop.js +22 -0
- package/dist/cjs/commands/quota.d.ts +10 -0
- package/dist/cjs/commands/quota.js +119 -0
- package/dist/cjs/commands/rename.d.ts +12 -0
- package/dist/cjs/commands/rename.js +48 -0
- package/dist/cjs/commands/search.d.ts +15 -0
- package/dist/cjs/commands/search.js +228 -0
- package/dist/cjs/commands/select.d.ts +25 -0
- package/dist/cjs/commands/select.js +250 -0
- package/dist/cjs/commands/starttls.d.ts +8 -0
- package/dist/cjs/commands/starttls.js +30 -0
- package/dist/cjs/commands/status-fields.d.ts +14 -0
- package/dist/cjs/commands/status-fields.js +61 -0
- package/dist/cjs/commands/status.d.ts +12 -0
- package/dist/cjs/commands/status.js +108 -0
- package/dist/cjs/commands/store.d.ts +19 -0
- package/dist/cjs/commands/store.js +93 -0
- package/dist/cjs/commands/subscribe.d.ts +9 -0
- package/dist/cjs/commands/subscribe.js +31 -0
- package/dist/cjs/commands/unsubscribe.d.ts +9 -0
- package/dist/cjs/commands/unsubscribe.js +31 -0
- package/dist/cjs/connection-deadline.d.ts +49 -0
- package/dist/cjs/connection-deadline.js +91 -0
- package/dist/cjs/errors.d.ts +83 -0
- package/dist/cjs/errors.js +13 -0
- package/dist/cjs/handler/imap-compiler.d.ts +24 -0
- package/dist/cjs/handler/imap-compiler.js +285 -0
- package/dist/cjs/handler/imap-formal-syntax.d.ts +28 -0
- package/dist/cjs/handler/imap-formal-syntax.js +121 -0
- package/dist/cjs/handler/imap-handler.d.ts +9 -0
- package/dist/cjs/handler/imap-handler.js +10 -0
- package/dist/cjs/handler/imap-parser.d.ts +16 -0
- package/dist/cjs/handler/imap-parser.js +90 -0
- package/dist/cjs/handler/imap-stream.d.ts +181 -0
- package/dist/cjs/handler/imap-stream.js +446 -0
- package/dist/cjs/handler/limits.d.ts +25 -0
- package/dist/cjs/handler/limits.js +51 -0
- package/dist/cjs/handler/parser-instance.d.ts +68 -0
- package/dist/cjs/handler/parser-instance.js +223 -0
- package/dist/cjs/handler/token-parser.d.ts +91 -0
- package/dist/cjs/handler/token-parser.js +673 -0
- package/dist/cjs/handler/types.d.ts +91 -0
- package/dist/cjs/handler/types.js +4 -0
- package/dist/cjs/imap-commands.d.ts +16 -0
- package/dist/cjs/imap-commands.js +74 -0
- package/dist/cjs/imap-flow.d.ts +676 -0
- package/dist/cjs/imap-flow.js +3949 -0
- package/dist/cjs/jp-decoder.d.ts +12 -0
- package/dist/cjs/jp-decoder.js +79 -0
- package/dist/cjs/limited-passthrough.d.ts +25 -0
- package/dist/cjs/limited-passthrough.js +54 -0
- package/dist/cjs/logger.d.ts +3 -0
- package/dist/cjs/logger.js +11 -0
- package/dist/cjs/package-info.d.ts +3 -0
- package/dist/cjs/package-info.js +7 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/proxy-connection.d.ts +33 -0
- package/dist/cjs/proxy-connection.js +392 -0
- package/dist/cjs/search-compiler.d.ts +34 -0
- package/dist/cjs/search-compiler.js +476 -0
- package/dist/cjs/special-use.d.ts +22 -0
- package/dist/cjs/special-use.js +911 -0
- package/dist/cjs/tools.d.ts +427 -0
- package/dist/cjs/tools.js +1496 -0
- package/{lib/imap-flow.d.ts → dist/cjs/types.d.ts} +386 -516
- package/dist/cjs/types.js +5 -0
- package/dist/esm/charsets.d.ts +1 -0
- package/{lib → dist/esm}/charsets.js +1 -6
- package/dist/esm/commands/append.d.ts +22 -0
- package/{lib → dist/esm}/commands/append.js +22 -52
- package/dist/esm/commands/authenticate.d.ts +24 -0
- package/{lib → dist/esm}/commands/authenticate.js +62 -87
- package/dist/esm/commands/capability.d.ts +8 -0
- package/{lib → dist/esm}/commands/capability.js +6 -9
- package/dist/esm/commands/close.d.ts +8 -0
- package/{lib → dist/esm}/commands/close.js +6 -10
- package/dist/esm/commands/compress.d.ts +8 -0
- package/{lib → dist/esm}/commands/compress.js +7 -11
- package/dist/esm/commands/copy.d.ts +13 -0
- package/{lib → dist/esm}/commands/copy.js +12 -20
- package/dist/esm/commands/copyuid-parser.d.ts +11 -0
- package/{lib → dist/esm}/commands/copyuid-parser.js +9 -15
- package/dist/esm/commands/create.d.ts +11 -0
- package/{lib → dist/esm}/commands/create.js +13 -27
- package/dist/esm/commands/delete.d.ts +11 -0
- package/{lib → dist/esm}/commands/delete.js +9 -14
- package/dist/esm/commands/enable.d.ts +9 -0
- package/{lib → dist/esm}/commands/enable.js +23 -30
- package/dist/esm/commands/esearch-parser.d.ts +17 -0
- package/dist/esm/commands/esearch-parser.js +88 -0
- package/dist/esm/commands/expunge.d.ts +12 -0
- package/{lib → dist/esm}/commands/expunge.js +17 -22
- package/dist/esm/commands/fetch.d.ts +30 -0
- package/{lib → dist/esm}/commands/fetch.js +32 -64
- package/dist/esm/commands/id.d.ts +10 -0
- package/{lib → dist/esm}/commands/id.js +17 -23
- package/dist/esm/commands/idle.d.ts +9 -0
- package/{lib → dist/esm}/commands/idle.js +47 -81
- package/dist/esm/commands/list.d.ts +16 -0
- package/{lib → dist/esm}/commands/list.js +56 -121
- package/dist/esm/commands/login.d.ts +11 -0
- package/{lib → dist/esm}/commands/login.js +10 -15
- package/dist/esm/commands/logout.d.ts +8 -0
- package/{lib → dist/esm}/commands/logout.js +9 -11
- package/dist/esm/commands/move.d.ts +13 -0
- package/{lib → dist/esm}/commands/move.js +13 -21
- package/dist/esm/commands/namespace.d.ts +25 -0
- package/{lib → dist/esm}/commands/namespace.js +34 -44
- package/dist/esm/commands/noop.d.ts +8 -0
- package/{lib → dist/esm}/commands/noop.js +6 -7
- package/dist/esm/commands/quota.d.ts +10 -0
- package/{lib → dist/esm}/commands/quota.js +18 -36
- package/dist/esm/commands/rename.d.ts +12 -0
- package/{lib → dist/esm}/commands/rename.js +10 -15
- package/dist/esm/commands/search.d.ts +15 -0
- package/{lib → dist/esm}/commands/search.js +36 -135
- package/dist/esm/commands/select.d.ts +25 -0
- package/{lib → dist/esm}/commands/select.js +33 -64
- package/dist/esm/commands/starttls.d.ts +8 -0
- package/{lib → dist/esm}/commands/starttls.js +6 -8
- package/dist/esm/commands/status-fields.d.ts +14 -0
- package/{lib → dist/esm}/commands/status-fields.js +5 -16
- package/dist/esm/commands/status.d.ts +12 -0
- package/{lib → dist/esm}/commands/status.js +18 -29
- package/dist/esm/commands/store.d.ts +19 -0
- package/{lib → dist/esm}/commands/store.js +24 -37
- package/dist/esm/commands/subscribe.d.ts +9 -0
- package/{lib → dist/esm}/commands/subscribe.js +8 -12
- package/dist/esm/commands/unsubscribe.d.ts +9 -0
- package/{lib → dist/esm}/commands/unsubscribe.js +8 -12
- package/dist/esm/connection-deadline.d.ts +49 -0
- package/{lib → dist/esm}/connection-deadline.js +14 -25
- package/dist/esm/errors.d.ts +83 -0
- package/dist/esm/errors.js +9 -0
- package/dist/esm/handler/imap-compiler.d.ts +24 -0
- package/{lib → dist/esm}/handler/imap-compiler.js +22 -80
- package/dist/esm/handler/imap-formal-syntax.d.ts +28 -0
- package/dist/esm/handler/imap-formal-syntax.js +117 -0
- package/dist/esm/handler/imap-handler.d.ts +9 -0
- package/dist/esm/handler/imap-handler.js +9 -0
- package/dist/esm/handler/imap-parser.d.ts +16 -0
- package/{lib → dist/esm}/handler/imap-parser.js +31 -44
- package/dist/esm/handler/imap-stream.d.ts +181 -0
- package/{lib → dist/esm}/handler/imap-stream.js +29 -121
- package/dist/esm/handler/limits.d.ts +25 -0
- package/{lib → dist/esm}/handler/limits.js +13 -22
- package/dist/esm/handler/parser-instance.d.ts +68 -0
- package/{lib → dist/esm}/handler/parser-instance.js +19 -47
- package/dist/esm/handler/token-parser.d.ts +91 -0
- package/{lib → dist/esm}/handler/token-parser.js +71 -155
- package/dist/esm/handler/types.d.ts +91 -0
- package/dist/esm/handler/types.js +3 -0
- package/dist/esm/imap-commands.d.ts +16 -0
- package/dist/esm/imap-commands.js +67 -0
- package/dist/esm/imap-flow.d.ts +676 -0
- package/{lib → dist/esm}/imap-flow.js +761 -1789
- package/dist/esm/jp-decoder.d.ts +12 -0
- package/{lib → dist/esm}/jp-decoder.js +6 -21
- package/dist/esm/limited-passthrough.d.ts +25 -0
- package/{lib → dist/esm}/limited-passthrough.js +7 -20
- package/dist/esm/logger.d.ts +3 -0
- package/dist/esm/logger.js +4 -0
- package/dist/esm/package-info.d.ts +3 -0
- package/dist/esm/package-info.js +4 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/proxy-connection.d.ts +33 -0
- package/{lib → dist/esm}/proxy-connection.js +56 -127
- package/dist/esm/search-compiler.d.ts +34 -0
- package/{lib → dist/esm}/search-compiler.js +54 -110
- package/dist/esm/special-use.d.ts +22 -0
- package/dist/esm/special-use.js +907 -0
- package/dist/esm/tools.d.ts +427 -0
- package/dist/esm/tools.js +1446 -0
- package/dist/esm/types.d.ts +828 -0
- package/dist/esm/types.js +4 -0
- package/package.json +60 -20
- package/.gitattributes +0 -1
- package/.github/CODE_OF_CONDUCT.md +0 -76
- package/.github/FUNDING.yml +0 -4
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- package/.github/contributing.md +0 -17
- package/.github/workflows/release.yaml +0 -36
- package/.github/workflows/stale.yml +0 -29
- package/.github/workflows/test.yml +0 -51
- package/.ncurc.js +0 -4
- package/.prettierignore +0 -4
- package/.prettierrc.js +0 -8
- package/.release-please-manifest.json +0 -3
- package/CLAUDE.md +0 -104
- package/Gruntfile.js +0 -23
- package/eslint.config.js +0 -45
- package/lib/handler/imap-formal-syntax.js +0 -189
- package/lib/handler/imap-handler.js +0 -17
- package/lib/imap-commands.js +0 -45
- package/lib/logger.js +0 -5
- package/lib/special-use.js +0 -923
- package/lib/tools.js +0 -1612
- package/release-please-config.json +0 -10
- package/test/authentication-test.js +0 -101
- package/test/auto-idle-test.js +0 -470
- package/test/bodystructure-test.js +0 -899
- package/test/charsets-test.js +0 -161
- package/test/commands-branches-test.js +0 -1095
- package/test/commands-integration-test.js +0 -11124
- package/test/commands-test.js +0 -73
- package/test/connection-edge-cases-test.js +0 -1828
- package/test/connection-test.js +0 -162
- package/test/copyuid-parser-test.js +0 -173
- package/test/fetch-generator-test.js +0 -218
- package/test/fixtures/fake-timers.js +0 -115
- package/test/fixtures/serialized-mimetorture.js +0 -2738
- package/test/fixtures/test-client.js +0 -101
- package/test/fixtures/test-tls.js +0 -8
- package/test/handler-branches-test.js +0 -310
- package/test/idle-polling-test.js +0 -518
- package/test/imap-compiler-test.js +0 -809
- package/test/imap-flow-compress-test.js +0 -166
- package/test/imap-flow-coverage-test.js +0 -612
- package/test/imap-flow-fetch-download-test.js +0 -909
- package/test/imap-flow-internals-test.js +0 -725
- package/test/imap-flow-methods-test.js +0 -889
- package/test/imap-flow-proxy-paths-test.js +0 -366
- package/test/imap-flow-secure-test.js +0 -573
- package/test/imap-flow-server-test.js +0 -1474
- package/test/imap-formal-syntax-test.js +0 -293
- package/test/imap-parser-test.js +0 -1474
- package/test/imap-stream-edge-cases-test.js +0 -666
- package/test/imap-stream-test.js +0 -177
- package/test/imapflow-test.js +0 -258
- package/test/integration/README.md +0 -52
- package/test/integration/dovecot-test.conf +0 -27
- package/test/integration/rev2-live-test.js +0 -431
- package/test/integration/run-rev2-tests.sh +0 -75
- package/test/integration-test.js +0 -83
- package/test/jp-decoder-test.js +0 -304
- package/test/limited-passthrough-test.js +0 -299
- package/test/memory-cleanup-test.js +0 -144
- package/test/memory-leak-test.js +0 -667
- package/test/parser-limits-test.js +0 -292
- package/test/proxy-connection-test.js +0 -738
- package/test/reliability-improvements-test.js +0 -548
- package/test/search-compiler-test.js +0 -1300
- package/test/search-test.js +0 -329
- package/test/special-use-test.js +0 -418
- package/test/starttls-injection-test.js +0 -181
- package/test/tag-correlation-test.js +0 -333
- package/test/timer-policy-test.js +0 -227
- package/test/token-parser-test.js +0 -456
- package/test/tools-test.js +0 -2013
- package/test/unhandled-rejection-test.js +0 -661
package/test/tools-test.js
DELETED
|
@@ -1,2013 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const tools = require('../lib/tools');
|
|
4
|
-
const { parser } = require('../lib/handler/imap-handler');
|
|
5
|
-
const crypto = require('crypto');
|
|
6
|
-
const iconv = require('iconv-lite');
|
|
7
|
-
|
|
8
|
-
// Mock connection for testing
|
|
9
|
-
let createMockConnection = (options = {}) => ({
|
|
10
|
-
enabled: new Set(options.enabled || (options.utf8 ? ['UTF8=ACCEPT'] : [])),
|
|
11
|
-
capabilities: new Map(options.capabilities || []),
|
|
12
|
-
namespace: options.namespace || null
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
// ============================================
|
|
16
|
-
// encodePath / decodePath tests
|
|
17
|
-
// ============================================
|
|
18
|
-
|
|
19
|
-
module.exports['Tools: encodePath with ASCII path'] = test => {
|
|
20
|
-
let connection = createMockConnection();
|
|
21
|
-
let result = tools.encodePath(connection, 'INBOX');
|
|
22
|
-
test.equal(result, 'INBOX');
|
|
23
|
-
test.done();
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
module.exports['Tools: encodePath with ASCII path (no UTF8)'] = test => {
|
|
27
|
-
let connection = createMockConnection({ utf8: false });
|
|
28
|
-
let result = tools.encodePath(connection, 'Sent/Gesendete');
|
|
29
|
-
// ASCII path should remain unchanged
|
|
30
|
-
test.equal(result, 'Sent/Gesendete');
|
|
31
|
-
test.done();
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
module.exports['Tools: encodePath encodes Unicode to modified UTF-7 on rev1 (no UTF8)'] = test => {
|
|
35
|
-
let connection = createMockConnection({ utf8: false });
|
|
36
|
-
// 'õ' (U+00F5) -> UTF-16BE 00F5 -> modified-base64 'APU'
|
|
37
|
-
test.equal(tools.encodePath(connection, 'Tõrva'), 'T&APU-rva');
|
|
38
|
-
// a literal '&' must be escaped as '&-'
|
|
39
|
-
test.equal(tools.encodePath(connection, 'Test&Folder'), 'Test&-Folder');
|
|
40
|
-
test.done();
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
module.exports['Tools: encodePath with Unicode when UTF8=ACCEPT enabled'] = test => {
|
|
44
|
-
let connection = createMockConnection({ utf8: true });
|
|
45
|
-
let result = tools.encodePath(connection, 'Posteingang/Ordner');
|
|
46
|
-
// With UTF8=ACCEPT, path should remain unchanged
|
|
47
|
-
test.equal(result, 'Posteingang/Ordner');
|
|
48
|
-
test.done();
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
module.exports['Tools: encodePath with null/undefined'] = test => {
|
|
52
|
-
let connection = createMockConnection();
|
|
53
|
-
test.equal(tools.encodePath(connection, null), '');
|
|
54
|
-
test.equal(tools.encodePath(connection, undefined), '');
|
|
55
|
-
test.done();
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
module.exports['Tools: decodePath with ASCII path'] = test => {
|
|
59
|
-
let connection = createMockConnection();
|
|
60
|
-
let result = tools.decodePath(connection, 'INBOX');
|
|
61
|
-
test.equal(result, 'INBOX');
|
|
62
|
-
test.done();
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
module.exports['Tools: decodePath with ampersand'] = test => {
|
|
66
|
-
let connection = createMockConnection({ utf8: false });
|
|
67
|
-
// modified UTF-7: '&-' is the escaped form of a literal ampersand
|
|
68
|
-
test.equal(tools.decodePath(connection, 'Test&-Folder'), 'Test&Folder');
|
|
69
|
-
// and an encoded sequence round-trips back to Unicode
|
|
70
|
-
test.equal(tools.decodePath(connection, 'T&APU-rva'), 'Tõrva');
|
|
71
|
-
test.done();
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
module.exports['Tools: decodePath with UTF8=ACCEPT enabled'] = test => {
|
|
75
|
-
let connection = createMockConnection({ utf8: true });
|
|
76
|
-
let result = tools.decodePath(connection, 'Test&Folder');
|
|
77
|
-
// With UTF8=ACCEPT, should not decode
|
|
78
|
-
test.equal(result, 'Test&Folder');
|
|
79
|
-
test.done();
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
module.exports['Tools: decodePath with null/undefined'] = test => {
|
|
83
|
-
let connection = createMockConnection();
|
|
84
|
-
test.equal(tools.decodePath(connection, null), '');
|
|
85
|
-
test.equal(tools.decodePath(connection, undefined), '');
|
|
86
|
-
test.done();
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
// ============================================
|
|
90
|
-
// normalizePath tests
|
|
91
|
-
// ============================================
|
|
92
|
-
|
|
93
|
-
module.exports['Tools: normalizePath with INBOX (case insensitive)'] = test => {
|
|
94
|
-
let connection = createMockConnection();
|
|
95
|
-
test.equal(tools.normalizePath(connection, 'inbox'), 'INBOX');
|
|
96
|
-
test.equal(tools.normalizePath(connection, 'INBOX'), 'INBOX');
|
|
97
|
-
test.equal(tools.normalizePath(connection, 'InBox'), 'INBOX');
|
|
98
|
-
test.done();
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
module.exports['Tools: normalizePath with array path'] = test => {
|
|
102
|
-
let connection = createMockConnection({
|
|
103
|
-
namespace: { delimiter: '/', prefix: '' }
|
|
104
|
-
});
|
|
105
|
-
let result = tools.normalizePath(connection, ['Folder', 'Subfolder']);
|
|
106
|
-
test.equal(result, 'Folder/Subfolder');
|
|
107
|
-
test.done();
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
module.exports['Tools: normalizePath with namespace prefix'] = test => {
|
|
111
|
-
let connection = createMockConnection({
|
|
112
|
-
namespace: { delimiter: '.', prefix: 'INBOX.' }
|
|
113
|
-
});
|
|
114
|
-
let result = tools.normalizePath(connection, 'Sent');
|
|
115
|
-
test.equal(result, 'INBOX.Sent');
|
|
116
|
-
test.done();
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
module.exports['Tools: normalizePath skip namespace'] = test => {
|
|
120
|
-
let connection = createMockConnection({
|
|
121
|
-
namespace: { delimiter: '.', prefix: 'INBOX.' }
|
|
122
|
-
});
|
|
123
|
-
let result = tools.normalizePath(connection, 'Sent', true);
|
|
124
|
-
test.equal(result, 'Sent');
|
|
125
|
-
test.done();
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
module.exports['Tools: normalizePath already has prefix'] = test => {
|
|
129
|
-
let connection = createMockConnection({
|
|
130
|
-
namespace: { delimiter: '.', prefix: 'INBOX.' }
|
|
131
|
-
});
|
|
132
|
-
let result = tools.normalizePath(connection, 'INBOX.Sent');
|
|
133
|
-
test.equal(result, 'INBOX.Sent');
|
|
134
|
-
test.done();
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
// ============================================
|
|
138
|
-
// comparePaths tests
|
|
139
|
-
// ============================================
|
|
140
|
-
|
|
141
|
-
module.exports['Tools: comparePaths equal paths'] = test => {
|
|
142
|
-
let connection = createMockConnection();
|
|
143
|
-
test.equal(tools.comparePaths(connection, 'INBOX', 'INBOX'), true);
|
|
144
|
-
test.equal(tools.comparePaths(connection, 'inbox', 'INBOX'), true);
|
|
145
|
-
test.done();
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
module.exports['Tools: comparePaths different paths'] = test => {
|
|
149
|
-
let connection = createMockConnection();
|
|
150
|
-
test.equal(tools.comparePaths(connection, 'INBOX', 'Sent'), false);
|
|
151
|
-
test.done();
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
module.exports['Tools: comparePaths with null/undefined'] = test => {
|
|
155
|
-
let connection = createMockConnection();
|
|
156
|
-
test.equal(tools.comparePaths(connection, null, 'INBOX'), false);
|
|
157
|
-
test.equal(tools.comparePaths(connection, 'INBOX', null), false);
|
|
158
|
-
test.equal(tools.comparePaths(connection, null, null), false);
|
|
159
|
-
test.done();
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
// ============================================
|
|
163
|
-
// updateCapabilities tests
|
|
164
|
-
// ============================================
|
|
165
|
-
|
|
166
|
-
module.exports['Tools: updateCapabilities with valid list'] = test => {
|
|
167
|
-
let list = [{ value: 'IMAP4rev1' }, { value: 'IDLE' }, { value: 'NAMESPACE' }];
|
|
168
|
-
let result = tools.updateCapabilities(list);
|
|
169
|
-
test.ok(result instanceof Map);
|
|
170
|
-
test.equal(result.get('IMAP4rev1'), true);
|
|
171
|
-
test.equal(result.get('IDLE'), true);
|
|
172
|
-
test.equal(result.get('NAMESPACE'), true);
|
|
173
|
-
test.done();
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
module.exports['Tools: updateCapabilities normalizes IMAP4rev2 casing'] = test => {
|
|
177
|
-
let list = [{ value: 'IMAP4rev2' }, { value: 'imap4rev1' }];
|
|
178
|
-
let result = tools.updateCapabilities(list);
|
|
179
|
-
// Wire tokens are uppercased, but the rev1/rev2 keys use the RFC spelling
|
|
180
|
-
test.equal(result.get('IMAP4rev2'), true);
|
|
181
|
-
test.equal(result.get('IMAP4rev1'), true);
|
|
182
|
-
test.equal(result.has('IMAP4REV2'), false);
|
|
183
|
-
test.done();
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
module.exports['Tools: isRev2Active for rev2-only server'] = test => {
|
|
187
|
-
// rev2 without rev1 means rev2 is the base protocol, no ENABLE needed
|
|
188
|
-
let connection = createMockConnection({ capabilities: [['IMAP4rev2', true]] });
|
|
189
|
-
test.equal(tools.isRev2Active(connection), true);
|
|
190
|
-
test.done();
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
module.exports['Tools: isRev2Active for dual server without ENABLE'] = test => {
|
|
194
|
-
// Advertising both keeps the session in rev1 mode until ENABLE IMAP4rev2
|
|
195
|
-
let connection = createMockConnection({
|
|
196
|
-
capabilities: [
|
|
197
|
-
['IMAP4rev1', true],
|
|
198
|
-
['IMAP4rev2', true]
|
|
199
|
-
]
|
|
200
|
-
});
|
|
201
|
-
test.equal(tools.isRev2Active(connection), false);
|
|
202
|
-
test.done();
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
module.exports['Tools: isRev2Active for dual server with ENABLE'] = test => {
|
|
206
|
-
let connection = createMockConnection({
|
|
207
|
-
capabilities: [
|
|
208
|
-
['IMAP4rev1', true],
|
|
209
|
-
['IMAP4rev2', true]
|
|
210
|
-
],
|
|
211
|
-
enabled: ['IMAP4REV2']
|
|
212
|
-
});
|
|
213
|
-
test.equal(tools.isRev2Active(connection), true);
|
|
214
|
-
test.done();
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
module.exports['Tools: isRev2Active for rev1-only server'] = test => {
|
|
218
|
-
let connection = createMockConnection({ capabilities: [['IMAP4rev1', true]] });
|
|
219
|
-
test.equal(tools.isRev2Active(connection), false);
|
|
220
|
-
test.done();
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
module.exports['Tools: hasCapability with advertised token'] = test => {
|
|
224
|
-
let connection = createMockConnection({
|
|
225
|
-
capabilities: [
|
|
226
|
-
['IMAP4rev1', true],
|
|
227
|
-
['UIDPLUS', true]
|
|
228
|
-
]
|
|
229
|
-
});
|
|
230
|
-
test.equal(tools.hasCapability(connection, 'UIDPLUS'), true);
|
|
231
|
-
test.equal(tools.hasCapability(connection, 'MOVE'), false);
|
|
232
|
-
test.done();
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
module.exports['Tools: hasCapability folds extensions into active rev2'] = test => {
|
|
236
|
-
let connection = createMockConnection({ capabilities: [['IMAP4rev2', true]] });
|
|
237
|
-
// RFC 9051 Appendix E folds these into base IMAP4rev2 - the complete set
|
|
238
|
-
for (let capability of [
|
|
239
|
-
'ENABLE',
|
|
240
|
-
'ESEARCH',
|
|
241
|
-
'IDLE',
|
|
242
|
-
'LIST-EXTENDED',
|
|
243
|
-
'LIST-STATUS',
|
|
244
|
-
'LITERAL-',
|
|
245
|
-
'MOVE',
|
|
246
|
-
'NAMESPACE',
|
|
247
|
-
'SASL-IR',
|
|
248
|
-
'SEARCHRES',
|
|
249
|
-
'SPECIAL-USE',
|
|
250
|
-
'STATUS=SIZE',
|
|
251
|
-
'UIDPLUS',
|
|
252
|
-
'UNSELECT'
|
|
253
|
-
]) {
|
|
254
|
-
test.equal(tools.hasCapability(connection, capability), true, `${capability} should be folded into rev2`);
|
|
255
|
-
}
|
|
256
|
-
// BINARY is intentionally not folded
|
|
257
|
-
test.equal(tools.hasCapability(connection, 'BINARY'), false);
|
|
258
|
-
test.done();
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
module.exports['Tools: hasCapability does not fold on unenabled dual server'] = test => {
|
|
262
|
-
let connection = createMockConnection({
|
|
263
|
-
capabilities: [
|
|
264
|
-
['IMAP4rev1', true],
|
|
265
|
-
['IMAP4rev2', true]
|
|
266
|
-
]
|
|
267
|
-
});
|
|
268
|
-
// Session is in rev1 mode - only explicitly advertised tokens count
|
|
269
|
-
test.equal(tools.hasCapability(connection, 'UIDPLUS'), false);
|
|
270
|
-
test.done();
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
module.exports['Tools: encodePath keeps UTF-8 when rev2 is active'] = test => {
|
|
274
|
-
let connection = createMockConnection({ capabilities: [['IMAP4rev2', true]] });
|
|
275
|
-
// rev2 mailbox names are native UTF-8, modified UTF-7 must not be applied
|
|
276
|
-
test.equal(tools.encodePath(connection, 'T\u00f5rva'), 'T\u00f5rva');
|
|
277
|
-
test.done();
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
module.exports['Tools: decodePath keeps ampersand sequences when rev2 is active'] = test => {
|
|
281
|
-
let connection = createMockConnection({ capabilities: [['IMAP4rev2', true]] });
|
|
282
|
-
// Under rev2 an "&"-sequence is a literal name, not modified UTF-7
|
|
283
|
-
test.equal(tools.decodePath(connection, 'A&AOQ-B'), 'A&AOQ-B');
|
|
284
|
-
test.done();
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
module.exports['Tools: updateCapabilities with APPENDLIMIT'] = test => {
|
|
288
|
-
let list = [{ value: 'APPENDLIMIT=52428800' }];
|
|
289
|
-
let result = tools.updateCapabilities(list);
|
|
290
|
-
test.equal(result.get('APPENDLIMIT'), 52428800);
|
|
291
|
-
test.done();
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
module.exports['Tools: updateCapabilities with empty/null list'] = test => {
|
|
295
|
-
test.ok(tools.updateCapabilities(null) instanceof Map);
|
|
296
|
-
test.ok(tools.updateCapabilities([]) instanceof Map);
|
|
297
|
-
test.ok(tools.updateCapabilities(undefined) instanceof Map);
|
|
298
|
-
test.done();
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
module.exports['Tools: updateCapabilities skips non-string values'] = test => {
|
|
302
|
-
let list = [{ value: 'IDLE' }, { value: 123 }, { value: null }];
|
|
303
|
-
let result = tools.updateCapabilities(list);
|
|
304
|
-
test.equal(result.get('IDLE'), true);
|
|
305
|
-
test.equal(result.size, 1);
|
|
306
|
-
test.done();
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
// ============================================
|
|
310
|
-
// getStatusCode tests
|
|
311
|
-
// ============================================
|
|
312
|
-
|
|
313
|
-
module.exports['Tools: getStatusCode with valid response'] = test => {
|
|
314
|
-
let response = {
|
|
315
|
-
attributes: [
|
|
316
|
-
{
|
|
317
|
-
section: [{ value: 'TRYCREATE' }]
|
|
318
|
-
}
|
|
319
|
-
]
|
|
320
|
-
};
|
|
321
|
-
test.equal(tools.getStatusCode(response), 'TRYCREATE');
|
|
322
|
-
test.done();
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
module.exports['Tools: getStatusCode with null/invalid response'] = test => {
|
|
326
|
-
test.equal(tools.getStatusCode(null), false);
|
|
327
|
-
test.equal(tools.getStatusCode({}), false);
|
|
328
|
-
test.equal(tools.getStatusCode({ attributes: [] }), false);
|
|
329
|
-
test.equal(tools.getStatusCode({ attributes: [{}] }), false);
|
|
330
|
-
test.done();
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
// ============================================
|
|
334
|
-
// getErrorText tests
|
|
335
|
-
// ============================================
|
|
336
|
-
|
|
337
|
-
module.exports['Tools: getErrorText with null response'] = async test => {
|
|
338
|
-
let result = await tools.getErrorText(null);
|
|
339
|
-
test.equal(result, false);
|
|
340
|
-
test.done();
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
module.exports['Tools: getErrorText with valid response'] = async test => {
|
|
344
|
-
let response = {
|
|
345
|
-
tag: '*',
|
|
346
|
-
command: 'OK',
|
|
347
|
-
attributes: [{ type: 'TEXT', value: 'Success' }]
|
|
348
|
-
};
|
|
349
|
-
let result = await tools.getErrorText(response);
|
|
350
|
-
test.ok(typeof result === 'string');
|
|
351
|
-
test.done();
|
|
352
|
-
};
|
|
353
|
-
|
|
354
|
-
module.exports['Tools: getErrorText survives a response that cannot be re-encoded'] = async test => {
|
|
355
|
-
// The parser tolerates stray bytes inside an OK/NO/BAD atom, and those have no
|
|
356
|
-
// valid IMAP string encoding. The error text is diagnostic, so it must still be
|
|
357
|
-
// produced rather than replacing the server's error with an encoding failure.
|
|
358
|
-
let response = await parser(Buffer.from('A1 NO [SERVERBUG\x00X] it failed', 'binary'));
|
|
359
|
-
let result = await tools.getErrorText(response);
|
|
360
|
-
test.ok(typeof result === 'string', 'error text should still be produced');
|
|
361
|
-
test.ok(result.includes('it failed'), 'the human-readable part must survive');
|
|
362
|
-
test.done();
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
// ============================================
|
|
366
|
-
// getFlagColor tests
|
|
367
|
-
// ============================================
|
|
368
|
-
|
|
369
|
-
module.exports['Tools: getFlagColor without Flagged'] = test => {
|
|
370
|
-
let flags = new Set(['\\Seen']);
|
|
371
|
-
test.equal(tools.getFlagColor(flags), null);
|
|
372
|
-
test.done();
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
module.exports['Tools: getFlagColor with Flagged only (red)'] = test => {
|
|
376
|
-
let flags = new Set(['\\Flagged']);
|
|
377
|
-
test.equal(tools.getFlagColor(flags), 'red');
|
|
378
|
-
test.done();
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
module.exports['Tools: getFlagColor with color bits'] = test => {
|
|
382
|
-
// bit0=1, bit1=0, bit2=0 => orange (index 1)
|
|
383
|
-
let flags = new Set(['\\Flagged', '$MailFlagBit0']);
|
|
384
|
-
test.equal(tools.getFlagColor(flags), 'orange');
|
|
385
|
-
|
|
386
|
-
// bit0=0, bit1=1, bit2=0 => yellow (index 2)
|
|
387
|
-
flags = new Set(['\\Flagged', '$MailFlagBit1']);
|
|
388
|
-
test.equal(tools.getFlagColor(flags), 'yellow');
|
|
389
|
-
|
|
390
|
-
// bit0=1, bit1=1, bit2=0 => green (index 3)
|
|
391
|
-
flags = new Set(['\\Flagged', '$MailFlagBit0', '$MailFlagBit1']);
|
|
392
|
-
test.equal(tools.getFlagColor(flags), 'green');
|
|
393
|
-
|
|
394
|
-
// bit0=0, bit1=0, bit2=1 => blue (index 4)
|
|
395
|
-
flags = new Set(['\\Flagged', '$MailFlagBit2']);
|
|
396
|
-
test.equal(tools.getFlagColor(flags), 'blue');
|
|
397
|
-
|
|
398
|
-
// bit0=1, bit1=0, bit2=1 => purple (index 5)
|
|
399
|
-
flags = new Set(['\\Flagged', '$MailFlagBit0', '$MailFlagBit2']);
|
|
400
|
-
test.equal(tools.getFlagColor(flags), 'purple');
|
|
401
|
-
|
|
402
|
-
// bit0=0, bit1=1, bit2=1 => grey (index 6)
|
|
403
|
-
flags = new Set(['\\Flagged', '$MailFlagBit1', '$MailFlagBit2']);
|
|
404
|
-
test.equal(tools.getFlagColor(flags), 'grey');
|
|
405
|
-
|
|
406
|
-
test.done();
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
module.exports['Tools: getFlagColor with all bits set (index 7) defaults to red'] = test => {
|
|
410
|
-
// bit0=1, bit1=2, bit2=4 => color=7, FLAG_COLORS[7] is undefined => defaults to 'red'
|
|
411
|
-
let flags = new Set(['\\Flagged', '$MailFlagBit0', '$MailFlagBit1', '$MailFlagBit2']);
|
|
412
|
-
test.equal(tools.getFlagColor(flags), 'red');
|
|
413
|
-
test.done();
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
// ============================================
|
|
417
|
-
// getColorFlags tests
|
|
418
|
-
// ============================================
|
|
419
|
-
|
|
420
|
-
module.exports['Tools: getColorFlags with valid color'] = test => {
|
|
421
|
-
// 'orange' is index 1, which is truthy
|
|
422
|
-
let result = tools.getColorFlags('orange');
|
|
423
|
-
test.ok(Array.isArray(result.add));
|
|
424
|
-
test.ok(Array.isArray(result.remove));
|
|
425
|
-
test.ok(result.add.includes('\\Flagged'));
|
|
426
|
-
test.done();
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
module.exports['Tools: getColorFlags with red (index 0)'] = test => {
|
|
430
|
-
// 'red' is index 0 — should add \\Flagged and remove all MailFlagBit flags
|
|
431
|
-
let result = tools.getColorFlags('red');
|
|
432
|
-
test.ok(Array.isArray(result.add));
|
|
433
|
-
test.ok(Array.isArray(result.remove));
|
|
434
|
-
test.ok(result.add.includes('\\Flagged'));
|
|
435
|
-
test.done();
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
module.exports['Tools: getColorFlags with null (remove flag)'] = test => {
|
|
439
|
-
let result = tools.getColorFlags(null);
|
|
440
|
-
test.ok(result.remove.includes('\\Flagged'));
|
|
441
|
-
test.done();
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
module.exports['Tools: getColorFlags with invalid color'] = test => {
|
|
445
|
-
let result = tools.getColorFlags('invalid-color');
|
|
446
|
-
test.equal(result, null);
|
|
447
|
-
test.done();
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
module.exports['Tools: getColorFlags sets correct bits'] = test => {
|
|
451
|
-
// orange = index 1 = bit0 set
|
|
452
|
-
let result = tools.getColorFlags('orange');
|
|
453
|
-
test.ok(result.add.includes('$MailFlagBit0'));
|
|
454
|
-
test.ok(result.remove.includes('$MailFlagBit1'));
|
|
455
|
-
test.ok(result.remove.includes('$MailFlagBit2'));
|
|
456
|
-
|
|
457
|
-
// green = index 3 = bit0 + bit1 set
|
|
458
|
-
result = tools.getColorFlags('green');
|
|
459
|
-
test.ok(result.add.includes('$MailFlagBit0'));
|
|
460
|
-
test.ok(result.add.includes('$MailFlagBit1'));
|
|
461
|
-
test.ok(result.remove.includes('$MailFlagBit2'));
|
|
462
|
-
|
|
463
|
-
test.done();
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
// ============================================
|
|
467
|
-
// isDate tests
|
|
468
|
-
// ============================================
|
|
469
|
-
|
|
470
|
-
module.exports['Tools: isDate with Date object'] = test => {
|
|
471
|
-
test.equal(tools.isDate(new Date()), true);
|
|
472
|
-
test.equal(tools.isDate(new Date('2023-01-01')), true);
|
|
473
|
-
test.done();
|
|
474
|
-
};
|
|
475
|
-
|
|
476
|
-
module.exports['Tools: isDate with non-Date'] = test => {
|
|
477
|
-
test.equal(tools.isDate('2023-01-01'), false);
|
|
478
|
-
test.equal(tools.isDate(12345), false);
|
|
479
|
-
test.equal(tools.isDate(null), false);
|
|
480
|
-
test.equal(tools.isDate({}), false);
|
|
481
|
-
test.done();
|
|
482
|
-
};
|
|
483
|
-
|
|
484
|
-
// ============================================
|
|
485
|
-
// formatDate tests
|
|
486
|
-
// ============================================
|
|
487
|
-
|
|
488
|
-
module.exports['Tools: formatDate with Date object'] = test => {
|
|
489
|
-
let date = new Date('2023-06-15T00:00:00.000Z');
|
|
490
|
-
let result = tools.formatDate(date);
|
|
491
|
-
test.equal(result, '15-Jun-2023');
|
|
492
|
-
test.done();
|
|
493
|
-
};
|
|
494
|
-
|
|
495
|
-
module.exports['Tools: formatDate with string'] = test => {
|
|
496
|
-
let result = tools.formatDate('2023-06-15');
|
|
497
|
-
test.equal(result, '15-Jun-2023');
|
|
498
|
-
test.done();
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
module.exports['Tools: formatDate with invalid date'] = test => {
|
|
502
|
-
let result = tools.formatDate('invalid');
|
|
503
|
-
test.equal(result, undefined);
|
|
504
|
-
test.done();
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
// ============================================
|
|
508
|
-
// formatDateTime tests
|
|
509
|
-
// ============================================
|
|
510
|
-
|
|
511
|
-
module.exports['Tools: formatDateTime with Date object'] = test => {
|
|
512
|
-
let date = new Date('2023-06-15T14:30:45.000Z');
|
|
513
|
-
let result = tools.formatDateTime(date);
|
|
514
|
-
test.ok(result.includes('Jun-2023'));
|
|
515
|
-
test.ok(result.includes('14:30:45'));
|
|
516
|
-
test.ok(result.includes('+0000'));
|
|
517
|
-
test.done();
|
|
518
|
-
};
|
|
519
|
-
|
|
520
|
-
module.exports['Tools: formatDateTime with null/undefined'] = test => {
|
|
521
|
-
test.equal(tools.formatDateTime(null), undefined);
|
|
522
|
-
test.equal(tools.formatDateTime(undefined), undefined);
|
|
523
|
-
test.done();
|
|
524
|
-
};
|
|
525
|
-
|
|
526
|
-
module.exports['Tools: formatDateTime with string'] = test => {
|
|
527
|
-
let result = tools.formatDateTime('2023-06-15T10:00:00Z');
|
|
528
|
-
test.ok(typeof result === 'string');
|
|
529
|
-
test.ok(result.includes('Jun-2023'));
|
|
530
|
-
test.done();
|
|
531
|
-
};
|
|
532
|
-
|
|
533
|
-
module.exports['Tools: formatDateTime with invalid date string'] = test => {
|
|
534
|
-
let result = tools.formatDateTime('invalid-date-string');
|
|
535
|
-
test.equal(result, undefined);
|
|
536
|
-
test.done();
|
|
537
|
-
};
|
|
538
|
-
|
|
539
|
-
// ============================================
|
|
540
|
-
// formatFlag tests
|
|
541
|
-
// ============================================
|
|
542
|
-
|
|
543
|
-
module.exports['Tools: formatFlag with standard flags'] = test => {
|
|
544
|
-
test.equal(tools.formatFlag('\\Seen'), '\\Seen');
|
|
545
|
-
test.equal(tools.formatFlag('\\SEEN'), '\\Seen');
|
|
546
|
-
test.equal(tools.formatFlag('\\answered'), '\\Answered');
|
|
547
|
-
test.equal(tools.formatFlag('\\flagged'), '\\Flagged');
|
|
548
|
-
test.equal(tools.formatFlag('\\deleted'), '\\Deleted');
|
|
549
|
-
test.equal(tools.formatFlag('\\draft'), '\\Draft');
|
|
550
|
-
test.done();
|
|
551
|
-
};
|
|
552
|
-
|
|
553
|
-
module.exports['Tools: formatFlag with Recent (cannot set)'] = test => {
|
|
554
|
-
test.equal(tools.formatFlag('\\Recent'), false);
|
|
555
|
-
test.equal(tools.formatFlag('\\recent'), false);
|
|
556
|
-
test.done();
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
module.exports['Tools: formatFlag with custom flags'] = test => {
|
|
560
|
-
test.equal(tools.formatFlag('$CustomFlag'), '$CustomFlag');
|
|
561
|
-
test.equal(tools.formatFlag('MyFlag'), 'MyFlag');
|
|
562
|
-
test.done();
|
|
563
|
-
};
|
|
564
|
-
|
|
565
|
-
// ============================================
|
|
566
|
-
// canUseFlag tests
|
|
567
|
-
// ============================================
|
|
568
|
-
|
|
569
|
-
module.exports['Tools: canUseFlag with no mailbox'] = test => {
|
|
570
|
-
test.equal(tools.canUseFlag(null, '\\Seen'), true);
|
|
571
|
-
test.done();
|
|
572
|
-
};
|
|
573
|
-
|
|
574
|
-
module.exports['Tools: canUseFlag with wildcard permanent flags'] = test => {
|
|
575
|
-
let mailbox = { permanentFlags: new Set(['\\*']) };
|
|
576
|
-
test.equal(tools.canUseFlag(mailbox, '\\Seen'), true);
|
|
577
|
-
test.equal(tools.canUseFlag(mailbox, '$CustomFlag'), true);
|
|
578
|
-
test.done();
|
|
579
|
-
};
|
|
580
|
-
|
|
581
|
-
module.exports['Tools: canUseFlag with specific permanent flags'] = test => {
|
|
582
|
-
let mailbox = { permanentFlags: new Set(['\\Seen', '\\Flagged']) };
|
|
583
|
-
test.equal(tools.canUseFlag(mailbox, '\\Seen'), true);
|
|
584
|
-
test.equal(tools.canUseFlag(mailbox, '\\Flagged'), true);
|
|
585
|
-
test.equal(tools.canUseFlag(mailbox, '\\Deleted'), false);
|
|
586
|
-
test.done();
|
|
587
|
-
};
|
|
588
|
-
|
|
589
|
-
module.exports['Tools: canUseFlag with no permanent flags'] = test => {
|
|
590
|
-
let mailbox = { permanentFlags: null };
|
|
591
|
-
test.equal(tools.canUseFlag(mailbox, '\\Seen'), true);
|
|
592
|
-
test.done();
|
|
593
|
-
};
|
|
594
|
-
|
|
595
|
-
// ============================================
|
|
596
|
-
// expandRange tests
|
|
597
|
-
// ============================================
|
|
598
|
-
|
|
599
|
-
module.exports['Tools: expandRange with single values'] = test => {
|
|
600
|
-
let result = tools.expandRange('1,2,3');
|
|
601
|
-
test.deepEqual(result, [1, 2, 3]);
|
|
602
|
-
test.done();
|
|
603
|
-
};
|
|
604
|
-
|
|
605
|
-
module.exports['Tools: expandRange with range'] = test => {
|
|
606
|
-
let result = tools.expandRange('1:5');
|
|
607
|
-
test.deepEqual(result, [1, 2, 3, 4, 5]);
|
|
608
|
-
test.done();
|
|
609
|
-
};
|
|
610
|
-
|
|
611
|
-
module.exports['Tools: expandRange with reverse range'] = test => {
|
|
612
|
-
let result = tools.expandRange('5:1');
|
|
613
|
-
test.deepEqual(result, [5, 4, 3, 2, 1]);
|
|
614
|
-
test.done();
|
|
615
|
-
};
|
|
616
|
-
|
|
617
|
-
module.exports['Tools: expandRange with mixed'] = test => {
|
|
618
|
-
let result = tools.expandRange('1,3:5,10');
|
|
619
|
-
test.deepEqual(result, [1, 3, 4, 5, 10]);
|
|
620
|
-
test.done();
|
|
621
|
-
};
|
|
622
|
-
|
|
623
|
-
module.exports['Tools: expandRange with same start/end'] = test => {
|
|
624
|
-
let result = tools.expandRange('5:5');
|
|
625
|
-
test.deepEqual(result, [5]);
|
|
626
|
-
test.done();
|
|
627
|
-
};
|
|
628
|
-
|
|
629
|
-
module.exports['Tools: expandRange skips entries that are not valid nz-numbers'] = test => {
|
|
630
|
-
// Server-supplied garbage must not produce bogus ids or endless loops
|
|
631
|
-
test.deepEqual(tools.expandRange('Infinity:5'), []);
|
|
632
|
-
test.deepEqual(tools.expandRange('0:3'), []);
|
|
633
|
-
test.deepEqual(tools.expandRange('abc,4,1:x'), [4]);
|
|
634
|
-
test.deepEqual(tools.expandRange('*'), []);
|
|
635
|
-
test.deepEqual(tools.expandRange('4294967296'), []);
|
|
636
|
-
test.done();
|
|
637
|
-
};
|
|
638
|
-
|
|
639
|
-
module.exports['Tools: expandRange caps hostile range spans'] = test => {
|
|
640
|
-
// A hostile range like 1:4294967295 is cut off at the expansion limit
|
|
641
|
-
// instead of exhausting memory
|
|
642
|
-
let result = tools.expandRange('1:4294967295');
|
|
643
|
-
test.equal(result.length, 0x1000000);
|
|
644
|
-
test.equal(result[0], 1);
|
|
645
|
-
test.equal(result[result.length - 1], 0x1000000);
|
|
646
|
-
|
|
647
|
-
let reverse = tools.expandRange('4294967295:4278190080');
|
|
648
|
-
test.equal(reverse.length, 0x1000000);
|
|
649
|
-
test.equal(reverse[0], 4294967295);
|
|
650
|
-
test.done();
|
|
651
|
-
};
|
|
652
|
-
|
|
653
|
-
// ============================================
|
|
654
|
-
// packMessageRange tests
|
|
655
|
-
// ============================================
|
|
656
|
-
|
|
657
|
-
module.exports['Tools: packMessageRange with sequential numbers'] = test => {
|
|
658
|
-
let result = tools.packMessageRange([1, 2, 3, 4, 5]);
|
|
659
|
-
test.equal(result, '1:5');
|
|
660
|
-
test.done();
|
|
661
|
-
};
|
|
662
|
-
|
|
663
|
-
module.exports['Tools: packMessageRange with gaps'] = test => {
|
|
664
|
-
let result = tools.packMessageRange([1, 2, 3, 7, 8, 9]);
|
|
665
|
-
test.equal(result, '1:3,7:9');
|
|
666
|
-
test.done();
|
|
667
|
-
};
|
|
668
|
-
|
|
669
|
-
module.exports['Tools: packMessageRange with single values'] = test => {
|
|
670
|
-
let result = tools.packMessageRange([1, 5, 10]);
|
|
671
|
-
test.equal(result, '1,5,10');
|
|
672
|
-
test.done();
|
|
673
|
-
};
|
|
674
|
-
|
|
675
|
-
module.exports['Tools: packMessageRange with unsorted input'] = test => {
|
|
676
|
-
let result = tools.packMessageRange([5, 1, 3, 2, 4]);
|
|
677
|
-
test.equal(result, '1:5');
|
|
678
|
-
test.done();
|
|
679
|
-
};
|
|
680
|
-
|
|
681
|
-
module.exports['Tools: packMessageRange with empty array'] = test => {
|
|
682
|
-
test.equal(tools.packMessageRange([]), '');
|
|
683
|
-
test.done();
|
|
684
|
-
};
|
|
685
|
-
|
|
686
|
-
module.exports['Tools: packMessageRange with non-array'] = test => {
|
|
687
|
-
test.equal(tools.packMessageRange(5), '5');
|
|
688
|
-
test.equal(tools.packMessageRange(null), '');
|
|
689
|
-
test.done();
|
|
690
|
-
};
|
|
691
|
-
|
|
692
|
-
// ============================================
|
|
693
|
-
// processName tests
|
|
694
|
-
// ============================================
|
|
695
|
-
|
|
696
|
-
module.exports['Tools: processName with quoted string'] = test => {
|
|
697
|
-
test.equal(tools.processName('"John Doe"'), 'John Doe');
|
|
698
|
-
test.done();
|
|
699
|
-
};
|
|
700
|
-
|
|
701
|
-
module.exports['Tools: processName with unquoted string'] = test => {
|
|
702
|
-
test.equal(tools.processName('John Doe'), 'John Doe');
|
|
703
|
-
test.done();
|
|
704
|
-
};
|
|
705
|
-
|
|
706
|
-
module.exports['Tools: processName with null/undefined'] = test => {
|
|
707
|
-
test.equal(tools.processName(null), '');
|
|
708
|
-
test.equal(tools.processName(undefined), '');
|
|
709
|
-
test.done();
|
|
710
|
-
};
|
|
711
|
-
|
|
712
|
-
module.exports['Tools: processName with short quoted'] = test => {
|
|
713
|
-
// String too short to have quotes removed (less than 3 chars)
|
|
714
|
-
test.equal(tools.processName('""'), '""');
|
|
715
|
-
test.equal(tools.processName('"a"'), 'a');
|
|
716
|
-
test.done();
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
// ============================================
|
|
720
|
-
// decodeText tests
|
|
721
|
-
// ============================================
|
|
722
|
-
|
|
723
|
-
module.exports['Tools: decodeText decodes encoded words and strips quotes'] = test => {
|
|
724
|
-
test.equal(tools.decodeText('=?utf-8?Q?T=C3=B5nu?='), 'Tõnu');
|
|
725
|
-
test.equal(tools.decodeText('"=?utf-8?Q?T=C3=B5nu?="'), 'Tõnu');
|
|
726
|
-
test.equal(tools.decodeText('Plain Name'), 'Plain Name');
|
|
727
|
-
test.done();
|
|
728
|
-
};
|
|
729
|
-
|
|
730
|
-
module.exports['Tools: decodeText tolerates missing values'] = test => {
|
|
731
|
-
// getStrValue returns false for a NIL envelope field
|
|
732
|
-
test.equal(tools.decodeText(false), '');
|
|
733
|
-
test.equal(tools.decodeText(null), '');
|
|
734
|
-
test.equal(tools.decodeText(undefined), '');
|
|
735
|
-
test.done();
|
|
736
|
-
};
|
|
737
|
-
|
|
738
|
-
// ============================================
|
|
739
|
-
// getFolderTree tests
|
|
740
|
-
// ============================================
|
|
741
|
-
|
|
742
|
-
module.exports['Tools: getFolderTree with flat folders'] = test => {
|
|
743
|
-
let folders = [
|
|
744
|
-
{ name: 'INBOX', path: 'INBOX', flags: new Set(), parent: [] },
|
|
745
|
-
{ name: 'Sent', path: 'Sent', flags: new Set(), parent: [] }
|
|
746
|
-
];
|
|
747
|
-
let tree = tools.getFolderTree(folders);
|
|
748
|
-
|
|
749
|
-
test.ok(tree.root);
|
|
750
|
-
test.ok(Array.isArray(tree.folders));
|
|
751
|
-
test.equal(tree.folders.length, 2);
|
|
752
|
-
test.done();
|
|
753
|
-
};
|
|
754
|
-
|
|
755
|
-
module.exports['Tools: getFolderTree with nested folders'] = test => {
|
|
756
|
-
let folders = [
|
|
757
|
-
{ name: 'INBOX', path: 'INBOX', flags: new Set(['\\HasChildren']), parent: [] },
|
|
758
|
-
{ name: 'Work', path: 'INBOX/Work', flags: new Set(), parent: ['INBOX'] }
|
|
759
|
-
];
|
|
760
|
-
let tree = tools.getFolderTree(folders);
|
|
761
|
-
|
|
762
|
-
test.ok(tree.root);
|
|
763
|
-
test.equal(tree.folders.length, 1);
|
|
764
|
-
test.equal(tree.folders[0].name, 'INBOX');
|
|
765
|
-
test.ok(Array.isArray(tree.folders[0].folders));
|
|
766
|
-
test.done();
|
|
767
|
-
};
|
|
768
|
-
|
|
769
|
-
module.exports['Tools: getFolderTree with Noselect flag'] = test => {
|
|
770
|
-
let folders = [{ name: 'Archive', path: 'Archive', flags: new Set(['\\Noselect']), parent: [] }];
|
|
771
|
-
let tree = tools.getFolderTree(folders);
|
|
772
|
-
|
|
773
|
-
test.equal(tree.folders[0].disabled, true);
|
|
774
|
-
test.done();
|
|
775
|
-
};
|
|
776
|
-
|
|
777
|
-
module.exports['Tools: getFolderTree with specialUse'] = test => {
|
|
778
|
-
let folders = [{ name: 'Sent', path: 'Sent', flags: new Set(), parent: [], specialUse: '\\Sent' }];
|
|
779
|
-
let tree = tools.getFolderTree(folders);
|
|
780
|
-
|
|
781
|
-
test.equal(tree.folders[0].specialUse, '\\Sent');
|
|
782
|
-
test.done();
|
|
783
|
-
};
|
|
784
|
-
|
|
785
|
-
module.exports['Tools: getFolderTree with delimiter'] = test => {
|
|
786
|
-
let folders = [{ name: 'Folder', path: 'Folder', flags: new Set(), parent: [], delimiter: '/' }];
|
|
787
|
-
let tree = tools.getFolderTree(folders);
|
|
788
|
-
|
|
789
|
-
test.equal(tree.folders[0].delimiter, '/');
|
|
790
|
-
test.done();
|
|
791
|
-
};
|
|
792
|
-
|
|
793
|
-
module.exports['Tools: getFolderTree updates existing entries'] = test => {
|
|
794
|
-
let folders = [
|
|
795
|
-
{ name: 'INBOX', path: 'INBOX', flags: new Set(['\\HasChildren']), parent: [], listed: true },
|
|
796
|
-
{ name: 'INBOX', path: 'INBOX', flags: new Set(['\\HasChildren']), parent: [], subscribed: true }
|
|
797
|
-
];
|
|
798
|
-
let tree = tools.getFolderTree(folders);
|
|
799
|
-
|
|
800
|
-
// Should update the existing entry, not create duplicate
|
|
801
|
-
test.equal(tree.folders.length, 1);
|
|
802
|
-
test.done();
|
|
803
|
-
};
|
|
804
|
-
|
|
805
|
-
// ============================================
|
|
806
|
-
// parseEnvelope tests
|
|
807
|
-
// ============================================
|
|
808
|
-
|
|
809
|
-
module.exports['Tools: parseEnvelope with complete envelope'] = test => {
|
|
810
|
-
let entry = [
|
|
811
|
-
{ value: 'Mon, 15 Jun 2023 10:00:00 +0000' }, // date
|
|
812
|
-
{ value: 'Test Subject' }, // subject
|
|
813
|
-
[[{ value: 'Sender Name' }, null, { value: 'sender' }, { value: 'example.com' }]], // from
|
|
814
|
-
[[{ value: 'Sender Name' }, null, { value: 'sender' }, { value: 'example.com' }]], // sender
|
|
815
|
-
[[{ value: 'Reply Name' }, null, { value: 'reply' }, { value: 'example.com' }]], // reply-to
|
|
816
|
-
[[{ value: 'To Name' }, null, { value: 'to' }, { value: 'example.com' }]], // to
|
|
817
|
-
[[{ value: 'CC Name' }, null, { value: 'cc' }, { value: 'example.com' }]], // cc
|
|
818
|
-
[[{ value: 'BCC Name' }, null, { value: 'bcc' }, { value: 'example.com' }]], // bcc
|
|
819
|
-
{ value: '<reply-id@example.com>' }, // in-reply-to
|
|
820
|
-
{ value: '<message-id@example.com>' } // message-id
|
|
821
|
-
];
|
|
822
|
-
|
|
823
|
-
let result = tools.parseEnvelope(entry);
|
|
824
|
-
|
|
825
|
-
test.ok(result.date instanceof Date);
|
|
826
|
-
test.equal(result.subject, 'Test Subject');
|
|
827
|
-
test.equal(result.from[0].address, 'sender@example.com');
|
|
828
|
-
test.equal(result.to[0].address, 'to@example.com');
|
|
829
|
-
test.equal(result.messageId, '<message-id@example.com>');
|
|
830
|
-
test.done();
|
|
831
|
-
};
|
|
832
|
-
|
|
833
|
-
module.exports['Tools: parseEnvelope with minimal envelope'] = test => {
|
|
834
|
-
let entry = [
|
|
835
|
-
null, // date
|
|
836
|
-
null, // subject
|
|
837
|
-
[], // from
|
|
838
|
-
[], // sender
|
|
839
|
-
[], // reply-to
|
|
840
|
-
[], // to
|
|
841
|
-
[], // cc
|
|
842
|
-
[], // bcc
|
|
843
|
-
null, // in-reply-to
|
|
844
|
-
null // message-id
|
|
845
|
-
];
|
|
846
|
-
|
|
847
|
-
let result = tools.parseEnvelope(entry);
|
|
848
|
-
test.ok(typeof result === 'object');
|
|
849
|
-
test.equal(result.subject, undefined);
|
|
850
|
-
test.done();
|
|
851
|
-
};
|
|
852
|
-
|
|
853
|
-
module.exports['Tools: parseEnvelope with invalid date'] = test => {
|
|
854
|
-
let entry = [{ value: 'invalid-date' }, null, null, null, null, null, null, null, null, null];
|
|
855
|
-
|
|
856
|
-
let result = tools.parseEnvelope(entry);
|
|
857
|
-
test.equal(result.date, 'invalid-date');
|
|
858
|
-
test.done();
|
|
859
|
-
};
|
|
860
|
-
|
|
861
|
-
module.exports['Tools: parseEnvelope with Buffer value'] = test => {
|
|
862
|
-
let entry = [
|
|
863
|
-
{ value: Buffer.from('Mon, 15 Jun 2023 10:00:00 +0000') }, // date as Buffer
|
|
864
|
-
{ value: Buffer.from('Buffer Subject') }, // subject as Buffer
|
|
865
|
-
[[{ value: Buffer.from('Sender Name') }, null, { value: Buffer.from('sender') }, { value: Buffer.from('example.com') }]], // from with Buffers
|
|
866
|
-
[], // sender
|
|
867
|
-
[], // reply-to
|
|
868
|
-
[], // to
|
|
869
|
-
[], // cc
|
|
870
|
-
[], // bcc
|
|
871
|
-
null, // in-reply-to
|
|
872
|
-
{ value: Buffer.from('<msg-id@example.com>') } // message-id as Buffer
|
|
873
|
-
];
|
|
874
|
-
|
|
875
|
-
let result = tools.parseEnvelope(entry);
|
|
876
|
-
test.equal(result.subject, 'Buffer Subject');
|
|
877
|
-
test.equal(result.from[0].address, 'sender@example.com');
|
|
878
|
-
test.equal(result.messageId, '<msg-id@example.com>');
|
|
879
|
-
test.done();
|
|
880
|
-
};
|
|
881
|
-
|
|
882
|
-
module.exports['Tools: parseEnvelope with empty address parts'] = test => {
|
|
883
|
-
// When both local part and domain are null/empty, address should be empty string
|
|
884
|
-
let entry = [
|
|
885
|
-
null, // date
|
|
886
|
-
null, // subject
|
|
887
|
-
[[{ value: 'Group Name' }, null, null, null]], // from with no email parts (group syntax)
|
|
888
|
-
[], // sender
|
|
889
|
-
[], // reply-to
|
|
890
|
-
[], // to
|
|
891
|
-
[], // cc
|
|
892
|
-
[], // bcc
|
|
893
|
-
null, // in-reply-to
|
|
894
|
-
null // message-id
|
|
895
|
-
];
|
|
896
|
-
|
|
897
|
-
let result = tools.parseEnvelope(entry);
|
|
898
|
-
// Address '@' should be converted to empty string and filtered out if no name
|
|
899
|
-
test.ok(result.from);
|
|
900
|
-
// The entry has a name but no valid address, should still be included with empty address
|
|
901
|
-
test.equal(result.from.length, 1);
|
|
902
|
-
test.equal(result.from[0].name, 'Group Name');
|
|
903
|
-
test.equal(result.from[0].address, '');
|
|
904
|
-
test.done();
|
|
905
|
-
};
|
|
906
|
-
|
|
907
|
-
module.exports['Tools: parseEnvelope keeps group syntax out of the address'] = test => {
|
|
908
|
-
// RFC 9051 7.5.2: a NIL host marks group syntax, so "undisclosed-recipients:;" must
|
|
909
|
-
// not turn into the invented address "undisclosed-recipients@"
|
|
910
|
-
let entry = [
|
|
911
|
-
null, // date
|
|
912
|
-
null, // subject
|
|
913
|
-
[], // from
|
|
914
|
-
[], // sender
|
|
915
|
-
[], // reply-to
|
|
916
|
-
[
|
|
917
|
-
[null, null, { value: 'undisclosed-recipients' }, null], // start of group
|
|
918
|
-
[null, null, null, null] // end of group
|
|
919
|
-
], // to
|
|
920
|
-
[], // cc
|
|
921
|
-
[], // bcc
|
|
922
|
-
null, // in-reply-to
|
|
923
|
-
null // message-id
|
|
924
|
-
];
|
|
925
|
-
|
|
926
|
-
let result = tools.parseEnvelope(entry);
|
|
927
|
-
// The end-of-group marker carries neither name nor address and is dropped
|
|
928
|
-
test.deepEqual(result.to, [{ name: 'undisclosed-recipients', address: '' }]);
|
|
929
|
-
test.done();
|
|
930
|
-
};
|
|
931
|
-
|
|
932
|
-
module.exports['Tools: parseEnvelope keeps group members alongside the markers'] = test => {
|
|
933
|
-
let entry = [
|
|
934
|
-
null, // date
|
|
935
|
-
null, // subject
|
|
936
|
-
[], // from
|
|
937
|
-
[], // sender
|
|
938
|
-
[], // reply-to
|
|
939
|
-
[
|
|
940
|
-
[null, null, { value: 'Team' }, null], // start of group
|
|
941
|
-
[{ value: 'Member One' }, null, { value: 'one' }, { value: 'example.com' }],
|
|
942
|
-
[{ value: 'Member Two' }, null, { value: 'two' }, { value: 'example.com' }],
|
|
943
|
-
[null, null, null, null] // end of group
|
|
944
|
-
], // to
|
|
945
|
-
[], // cc
|
|
946
|
-
[], // bcc
|
|
947
|
-
null, // in-reply-to
|
|
948
|
-
null // message-id
|
|
949
|
-
];
|
|
950
|
-
|
|
951
|
-
let result = tools.parseEnvelope(entry);
|
|
952
|
-
test.deepEqual(result.to, [
|
|
953
|
-
{ name: 'Team', address: '' },
|
|
954
|
-
{ name: 'Member One', address: 'one@example.com' },
|
|
955
|
-
{ name: 'Member Two', address: 'two@example.com' }
|
|
956
|
-
]);
|
|
957
|
-
test.done();
|
|
958
|
-
};
|
|
959
|
-
|
|
960
|
-
module.exports['Tools: parseEnvelope does not join a NIL host onto a mailbox'] = test => {
|
|
961
|
-
// Some servers parse a malformed header such as
|
|
962
|
-
// "To: user@example.com user@example.com" into a personal name plus a mailbox
|
|
963
|
-
// with a NIL host. Joining those produced the invalid address "example.com@".
|
|
964
|
-
let entry = [
|
|
965
|
-
null, // date
|
|
966
|
-
null, // subject
|
|
967
|
-
[], // from
|
|
968
|
-
[], // sender
|
|
969
|
-
[], // reply-to
|
|
970
|
-
[[{ value: 'user@example.com user@' }, null, { value: 'example.com' }, null]], // to
|
|
971
|
-
[], // cc
|
|
972
|
-
[], // bcc
|
|
973
|
-
null, // in-reply-to
|
|
974
|
-
null // message-id
|
|
975
|
-
];
|
|
976
|
-
|
|
977
|
-
let result = tools.parseEnvelope(entry);
|
|
978
|
-
test.deepEqual(result.to, [{ name: 'user@example.com user@', address: '' }]);
|
|
979
|
-
test.done();
|
|
980
|
-
};
|
|
981
|
-
|
|
982
|
-
module.exports['Tools: parseEnvelope decodes an encoded group name'] = test => {
|
|
983
|
-
let entry = [
|
|
984
|
-
null, // date
|
|
985
|
-
null, // subject
|
|
986
|
-
[], // from
|
|
987
|
-
[], // sender
|
|
988
|
-
[], // reply-to
|
|
989
|
-
[[null, null, { value: '=?utf-8?Q?T=C3=B5ny?=' }, null]], // to
|
|
990
|
-
[], // cc
|
|
991
|
-
[], // bcc
|
|
992
|
-
null, // in-reply-to
|
|
993
|
-
null // message-id
|
|
994
|
-
];
|
|
995
|
-
|
|
996
|
-
let result = tools.parseEnvelope(entry);
|
|
997
|
-
test.deepEqual(result.to, [{ name: 'Tõny', address: '' }]);
|
|
998
|
-
test.done();
|
|
999
|
-
};
|
|
1000
|
-
|
|
1001
|
-
// ============================================
|
|
1002
|
-
// getStructuredParams tests
|
|
1003
|
-
// ============================================
|
|
1004
|
-
|
|
1005
|
-
module.exports['Tools: getStructuredParams with simple params'] = test => {
|
|
1006
|
-
let arr = [{ value: 'charset' }, { value: 'utf-8' }, { value: 'name' }, { value: 'file.txt' }];
|
|
1007
|
-
|
|
1008
|
-
let result = tools.getStructuredParams(arr);
|
|
1009
|
-
test.equal(result.charset, 'utf-8');
|
|
1010
|
-
test.equal(result.name, 'file.txt');
|
|
1011
|
-
test.done();
|
|
1012
|
-
};
|
|
1013
|
-
|
|
1014
|
-
module.exports['Tools: getStructuredParams with null'] = test => {
|
|
1015
|
-
let result = tools.getStructuredParams(null);
|
|
1016
|
-
test.deepEqual(result, {});
|
|
1017
|
-
test.done();
|
|
1018
|
-
};
|
|
1019
|
-
|
|
1020
|
-
module.exports['Tools: getStructuredParams with continuation'] = test => {
|
|
1021
|
-
// RFC 2231 continuation
|
|
1022
|
-
let arr = [{ value: 'filename*0' }, { value: 'very' }, { value: 'filename*1' }, { value: 'long' }, { value: 'filename*2' }, { value: 'name.txt' }];
|
|
1023
|
-
|
|
1024
|
-
let result = tools.getStructuredParams(arr);
|
|
1025
|
-
test.equal(result.filename, 'verylongname.txt');
|
|
1026
|
-
test.done();
|
|
1027
|
-
};
|
|
1028
|
-
|
|
1029
|
-
// ============================================
|
|
1030
|
-
// parseBodystructure tests
|
|
1031
|
-
// ============================================
|
|
1032
|
-
|
|
1033
|
-
module.exports['Tools: parseBodystructure with simple text'] = test => {
|
|
1034
|
-
let entry = [
|
|
1035
|
-
{ value: 'TEXT' },
|
|
1036
|
-
{ value: 'PLAIN' },
|
|
1037
|
-
[{ value: 'CHARSET' }, { value: 'UTF-8' }],
|
|
1038
|
-
null, // id
|
|
1039
|
-
null, // description
|
|
1040
|
-
{ value: '7BIT' }, // encoding
|
|
1041
|
-
{ value: '1234' }, // size
|
|
1042
|
-
{ value: '50' } // lines
|
|
1043
|
-
];
|
|
1044
|
-
|
|
1045
|
-
let result = tools.parseBodystructure(entry);
|
|
1046
|
-
test.equal(result.type, 'text/plain');
|
|
1047
|
-
test.equal(result.encoding, '7bit');
|
|
1048
|
-
test.equal(result.size, 1234);
|
|
1049
|
-
test.equal(result.parameters.charset, 'UTF-8');
|
|
1050
|
-
test.done();
|
|
1051
|
-
};
|
|
1052
|
-
|
|
1053
|
-
module.exports['Tools: parseBodystructure with multipart'] = test => {
|
|
1054
|
-
let textPart = [{ value: 'TEXT' }, { value: 'PLAIN' }, null, null, null, { value: '7BIT' }, { value: '100' }, { value: '5' }];
|
|
1055
|
-
|
|
1056
|
-
let htmlPart = [{ value: 'TEXT' }, { value: 'HTML' }, null, null, null, { value: 'QUOTED-PRINTABLE' }, { value: '200' }, { value: '10' }];
|
|
1057
|
-
|
|
1058
|
-
let entry = [textPart, htmlPart, { value: 'ALTERNATIVE' }];
|
|
1059
|
-
|
|
1060
|
-
let result = tools.parseBodystructure(entry);
|
|
1061
|
-
test.equal(result.type, 'multipart/alternative');
|
|
1062
|
-
test.ok(Array.isArray(result.childNodes));
|
|
1063
|
-
test.equal(result.childNodes.length, 2);
|
|
1064
|
-
test.equal(result.childNodes[0].type, 'text/plain');
|
|
1065
|
-
test.equal(result.childNodes[1].type, 'text/html');
|
|
1066
|
-
test.done();
|
|
1067
|
-
};
|
|
1068
|
-
|
|
1069
|
-
module.exports['Tools: parseBodystructure with attachment'] = test => {
|
|
1070
|
-
let entry = [
|
|
1071
|
-
{ value: 'APPLICATION' },
|
|
1072
|
-
{ value: 'PDF' },
|
|
1073
|
-
[{ value: 'NAME' }, { value: 'document.pdf' }],
|
|
1074
|
-
null,
|
|
1075
|
-
null,
|
|
1076
|
-
{ value: 'BASE64' },
|
|
1077
|
-
{ value: '50000' }
|
|
1078
|
-
];
|
|
1079
|
-
|
|
1080
|
-
let result = tools.parseBodystructure(entry);
|
|
1081
|
-
test.equal(result.type, 'application/pdf');
|
|
1082
|
-
test.equal(result.parameters.name, 'document.pdf');
|
|
1083
|
-
test.done();
|
|
1084
|
-
};
|
|
1085
|
-
|
|
1086
|
-
module.exports['Tools: parseBodystructure with md5'] = test => {
|
|
1087
|
-
// Non-text type with extension data including md5
|
|
1088
|
-
let entry = [
|
|
1089
|
-
{ value: 'APPLICATION' },
|
|
1090
|
-
{ value: 'OCTET-STREAM' },
|
|
1091
|
-
null, // params
|
|
1092
|
-
null, // id
|
|
1093
|
-
null, // description
|
|
1094
|
-
{ value: 'BASE64' }, // encoding
|
|
1095
|
-
{ value: '1000' }, // size
|
|
1096
|
-
{ value: 'd41d8cd98f00b204e9800998ecf8427e' }, // md5
|
|
1097
|
-
null, // disposition
|
|
1098
|
-
null // language (to ensure we have enough elements)
|
|
1099
|
-
];
|
|
1100
|
-
|
|
1101
|
-
let result = tools.parseBodystructure(entry);
|
|
1102
|
-
test.equal(result.type, 'application/octet-stream');
|
|
1103
|
-
test.equal(result.md5, 'd41d8cd98f00b204e9800998ecf8427e');
|
|
1104
|
-
test.done();
|
|
1105
|
-
};
|
|
1106
|
-
|
|
1107
|
-
module.exports['Tools: parseBodystructure with language'] = test => {
|
|
1108
|
-
// Non-text type with language extension
|
|
1109
|
-
let entry = [
|
|
1110
|
-
{ value: 'APPLICATION' },
|
|
1111
|
-
{ value: 'PDF' },
|
|
1112
|
-
null, // params
|
|
1113
|
-
null, // id
|
|
1114
|
-
null, // description
|
|
1115
|
-
{ value: 'BASE64' }, // encoding
|
|
1116
|
-
{ value: '5000' }, // size
|
|
1117
|
-
null, // md5
|
|
1118
|
-
null, // disposition
|
|
1119
|
-
[{ value: 'EN' }, { value: 'DE' }], // language (array of values)
|
|
1120
|
-
null // location (to ensure enough elements)
|
|
1121
|
-
];
|
|
1122
|
-
|
|
1123
|
-
let result = tools.parseBodystructure(entry);
|
|
1124
|
-
test.equal(result.type, 'application/pdf');
|
|
1125
|
-
test.ok(Array.isArray(result.language));
|
|
1126
|
-
test.deepEqual(result.language, ['en', 'de']);
|
|
1127
|
-
test.done();
|
|
1128
|
-
};
|
|
1129
|
-
|
|
1130
|
-
module.exports['Tools: parseBodystructure with location'] = test => {
|
|
1131
|
-
// Non-text type with location extension
|
|
1132
|
-
let entry = [
|
|
1133
|
-
{ value: 'IMAGE' },
|
|
1134
|
-
{ value: 'PNG' },
|
|
1135
|
-
null, // params
|
|
1136
|
-
null, // id
|
|
1137
|
-
null, // description
|
|
1138
|
-
{ value: 'BASE64' }, // encoding
|
|
1139
|
-
{ value: '10000' }, // size
|
|
1140
|
-
null, // md5
|
|
1141
|
-
null, // disposition
|
|
1142
|
-
null, // language
|
|
1143
|
-
{ value: 'http://example.com/image.png' }, // location
|
|
1144
|
-
null // extra element to ensure we have enough
|
|
1145
|
-
];
|
|
1146
|
-
|
|
1147
|
-
let result = tools.parseBodystructure(entry);
|
|
1148
|
-
test.equal(result.type, 'image/png');
|
|
1149
|
-
test.equal(result.location, 'http://example.com/image.png');
|
|
1150
|
-
test.done();
|
|
1151
|
-
};
|
|
1152
|
-
|
|
1153
|
-
module.exports['Tools: parseBodystructure with all extension fields'] = test => {
|
|
1154
|
-
// Non-text type with all extension fields
|
|
1155
|
-
let entry = [
|
|
1156
|
-
{ value: 'APPLICATION' },
|
|
1157
|
-
{ value: 'ZIP' },
|
|
1158
|
-
[{ value: 'NAME' }, { value: 'archive.zip' }], // params
|
|
1159
|
-
{ value: '<id123@example.com>' }, // id
|
|
1160
|
-
{ value: 'A zip archive' }, // description
|
|
1161
|
-
{ value: 'BASE64' }, // encoding
|
|
1162
|
-
{ value: '50000' }, // size
|
|
1163
|
-
{ value: 'abc123def456' }, // md5
|
|
1164
|
-
[{ value: 'ATTACHMENT' }, [{ value: 'FILENAME' }, { value: 'archive.zip' }]], // disposition with params
|
|
1165
|
-
[{ value: 'EN' }], // language
|
|
1166
|
-
{ value: 'http://example.com/archive.zip' }, // location
|
|
1167
|
-
null // extra element
|
|
1168
|
-
];
|
|
1169
|
-
|
|
1170
|
-
let result = tools.parseBodystructure(entry);
|
|
1171
|
-
test.equal(result.type, 'application/zip');
|
|
1172
|
-
test.equal(result.parameters.name, 'archive.zip');
|
|
1173
|
-
test.equal(result.id, '<id123@example.com>');
|
|
1174
|
-
test.equal(result.description, 'A zip archive');
|
|
1175
|
-
test.equal(result.encoding, 'base64');
|
|
1176
|
-
test.equal(result.size, 50000);
|
|
1177
|
-
test.equal(result.md5, 'abc123def456');
|
|
1178
|
-
test.equal(result.disposition, 'attachment');
|
|
1179
|
-
test.equal(result.dispositionParameters.filename, 'archive.zip');
|
|
1180
|
-
test.deepEqual(result.language, ['en']);
|
|
1181
|
-
test.equal(result.location, 'http://example.com/archive.zip');
|
|
1182
|
-
test.done();
|
|
1183
|
-
};
|
|
1184
|
-
|
|
1185
|
-
module.exports['Tools: parseBodystructure with message/rfc822'] = test => {
|
|
1186
|
-
// message/rfc822 has special handling with envelope and nested bodystructure
|
|
1187
|
-
let nestedBody = [
|
|
1188
|
-
{ value: 'TEXT' },
|
|
1189
|
-
{ value: 'PLAIN' },
|
|
1190
|
-
[{ value: 'CHARSET' }, { value: 'UTF-8' }],
|
|
1191
|
-
null,
|
|
1192
|
-
null,
|
|
1193
|
-
{ value: '7BIT' },
|
|
1194
|
-
{ value: '500' },
|
|
1195
|
-
{ value: '20' } // line count for text
|
|
1196
|
-
];
|
|
1197
|
-
|
|
1198
|
-
let envelope = [
|
|
1199
|
-
{ value: 'Mon, 15 Jun 2023 10:00:00 +0000' }, // date
|
|
1200
|
-
{ value: 'Nested Subject' }, // subject
|
|
1201
|
-
[[null, null, { value: 'sender' }, { value: 'example.com' }]], // from
|
|
1202
|
-
[],
|
|
1203
|
-
[],
|
|
1204
|
-
[],
|
|
1205
|
-
[],
|
|
1206
|
-
[], // sender, reply-to, to, cc, bcc
|
|
1207
|
-
null, // in-reply-to
|
|
1208
|
-
{ value: '<nested@example.com>' } // message-id
|
|
1209
|
-
];
|
|
1210
|
-
|
|
1211
|
-
let entry = [
|
|
1212
|
-
{ value: 'MESSAGE' },
|
|
1213
|
-
{ value: 'RFC822' },
|
|
1214
|
-
null, // params
|
|
1215
|
-
null, // id
|
|
1216
|
-
null, // description
|
|
1217
|
-
{ value: '7BIT' }, // encoding
|
|
1218
|
-
{ value: '10000' }, // size
|
|
1219
|
-
envelope, // envelope
|
|
1220
|
-
nestedBody, // nested bodystructure
|
|
1221
|
-
{ value: '100' }, // line count
|
|
1222
|
-
null, // md5
|
|
1223
|
-
null // disposition
|
|
1224
|
-
];
|
|
1225
|
-
|
|
1226
|
-
let result = tools.parseBodystructure(entry);
|
|
1227
|
-
test.equal(result.type, 'message/rfc822');
|
|
1228
|
-
test.equal(result.size, 10000);
|
|
1229
|
-
test.equal(result.lineCount, 100);
|
|
1230
|
-
test.ok(result.envelope);
|
|
1231
|
-
test.equal(result.envelope.subject, 'Nested Subject');
|
|
1232
|
-
test.ok(result.childNodes);
|
|
1233
|
-
test.equal(result.childNodes.length, 1);
|
|
1234
|
-
test.equal(result.childNodes[0].type, 'text/plain');
|
|
1235
|
-
test.done();
|
|
1236
|
-
};
|
|
1237
|
-
|
|
1238
|
-
// ============================================
|
|
1239
|
-
// getDecoder tests
|
|
1240
|
-
// ============================================
|
|
1241
|
-
|
|
1242
|
-
module.exports['Tools: getDecoder with standard charset'] = test => {
|
|
1243
|
-
let decoder = tools.getDecoder('utf-8');
|
|
1244
|
-
test.ok(decoder);
|
|
1245
|
-
test.ok(typeof decoder.write === 'function');
|
|
1246
|
-
test.done();
|
|
1247
|
-
};
|
|
1248
|
-
|
|
1249
|
-
module.exports['Tools: getDecoder with Japanese charset'] = test => {
|
|
1250
|
-
let decoder = tools.getDecoder('iso-2022-jp');
|
|
1251
|
-
test.ok(decoder);
|
|
1252
|
-
test.equal(decoder.constructor.name, 'JPDecoder');
|
|
1253
|
-
test.done();
|
|
1254
|
-
};
|
|
1255
|
-
|
|
1256
|
-
module.exports['Tools: getDecoder with null/undefined'] = test => {
|
|
1257
|
-
let decoder = tools.getDecoder(null);
|
|
1258
|
-
test.ok(decoder);
|
|
1259
|
-
test.done();
|
|
1260
|
-
};
|
|
1261
|
-
|
|
1262
|
-
// ============================================
|
|
1263
|
-
// AuthenticationFailure tests
|
|
1264
|
-
// ============================================
|
|
1265
|
-
|
|
1266
|
-
module.exports['Tools: AuthenticationFailure error class'] = test => {
|
|
1267
|
-
let error = new tools.AuthenticationFailure('Auth failed');
|
|
1268
|
-
test.ok(error instanceof Error);
|
|
1269
|
-
test.equal(error.authenticationFailed, true);
|
|
1270
|
-
test.equal(error.message, 'Auth failed');
|
|
1271
|
-
test.done();
|
|
1272
|
-
};
|
|
1273
|
-
|
|
1274
|
-
// ============================================
|
|
1275
|
-
// enhanceCommandError tests
|
|
1276
|
-
// ============================================
|
|
1277
|
-
|
|
1278
|
-
module.exports['Tools: enhanceCommandError sets serverResponseCode'] = async test => {
|
|
1279
|
-
let err = new Error('Command failed');
|
|
1280
|
-
err.response = {
|
|
1281
|
-
tag: '*',
|
|
1282
|
-
command: 'NO',
|
|
1283
|
-
attributes: [
|
|
1284
|
-
{
|
|
1285
|
-
type: 'SECTION',
|
|
1286
|
-
section: [{ type: 'ATOM', value: 'NONEXISTENT' }]
|
|
1287
|
-
},
|
|
1288
|
-
{ type: 'ATOM', value: 'Mailbox' },
|
|
1289
|
-
{ type: 'ATOM', value: 'not' },
|
|
1290
|
-
{ type: 'ATOM', value: 'found' }
|
|
1291
|
-
]
|
|
1292
|
-
};
|
|
1293
|
-
let result = await tools.enhanceCommandError(err);
|
|
1294
|
-
test.equal(result.serverResponseCode, 'NONEXISTENT');
|
|
1295
|
-
test.equal(typeof result.response, 'string');
|
|
1296
|
-
test.done();
|
|
1297
|
-
};
|
|
1298
|
-
|
|
1299
|
-
module.exports['Tools: enhanceCommandError with no status code'] = async test => {
|
|
1300
|
-
let err = new Error('Command failed');
|
|
1301
|
-
err.response = { tag: '*', command: 'NO' };
|
|
1302
|
-
let result = await tools.enhanceCommandError(err);
|
|
1303
|
-
test.ok(!result.serverResponseCode);
|
|
1304
|
-
test.done();
|
|
1305
|
-
};
|
|
1306
|
-
|
|
1307
|
-
module.exports['Tools: enhanceCommandError with null response'] = async test => {
|
|
1308
|
-
let err = new Error('Command failed');
|
|
1309
|
-
err.response = null;
|
|
1310
|
-
let result = await tools.enhanceCommandError(err);
|
|
1311
|
-
test.equal(result.response, false);
|
|
1312
|
-
test.done();
|
|
1313
|
-
};
|
|
1314
|
-
|
|
1315
|
-
// ============================================
|
|
1316
|
-
// getDecoder additional tests
|
|
1317
|
-
// ============================================
|
|
1318
|
-
|
|
1319
|
-
module.exports['Tools: getDecoder with eucjp charset'] = test => {
|
|
1320
|
-
let decoder = tools.getDecoder('eucjp');
|
|
1321
|
-
test.ok(decoder);
|
|
1322
|
-
test.equal(decoder.constructor.name, 'JPDecoder');
|
|
1323
|
-
test.done();
|
|
1324
|
-
};
|
|
1325
|
-
|
|
1326
|
-
module.exports['Tools: getDecoder with euc-jp (hyphenated) returns JPDecoder'] = test => {
|
|
1327
|
-
let decoder = tools.getDecoder('euc-jp');
|
|
1328
|
-
test.ok(decoder);
|
|
1329
|
-
test.equal(decoder.constructor.name, 'JPDecoder');
|
|
1330
|
-
test.done();
|
|
1331
|
-
};
|
|
1332
|
-
|
|
1333
|
-
module.exports['Tools: getDecoder with jis charset'] = test => {
|
|
1334
|
-
let decoder = tools.getDecoder('jis');
|
|
1335
|
-
test.ok(decoder);
|
|
1336
|
-
test.equal(decoder.constructor.name, 'JPDecoder');
|
|
1337
|
-
test.done();
|
|
1338
|
-
};
|
|
1339
|
-
|
|
1340
|
-
module.exports['Tools: getDecoder with windows-1252 returns iconv stream'] = test => {
|
|
1341
|
-
let decoder = tools.getDecoder('windows-1252');
|
|
1342
|
-
test.ok(decoder);
|
|
1343
|
-
test.notEqual(decoder.constructor.name, 'JPDecoder');
|
|
1344
|
-
test.ok(typeof decoder.write === 'function');
|
|
1345
|
-
test.done();
|
|
1346
|
-
};
|
|
1347
|
-
|
|
1348
|
-
module.exports['Tools: getDecoder with no arg defaults to ascii'] = test => {
|
|
1349
|
-
let decoder = tools.getDecoder();
|
|
1350
|
-
test.ok(decoder);
|
|
1351
|
-
test.ok(typeof decoder.write === 'function');
|
|
1352
|
-
test.done();
|
|
1353
|
-
};
|
|
1354
|
-
|
|
1355
|
-
// ============================================
|
|
1356
|
-
// getColorFlags tests
|
|
1357
|
-
// ============================================
|
|
1358
|
-
|
|
1359
|
-
module.exports['Tools: getColorFlags returns non-null for all valid colors'] = test => {
|
|
1360
|
-
let colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'grey'];
|
|
1361
|
-
for (let color of colors) {
|
|
1362
|
-
let result = tools.getColorFlags(color);
|
|
1363
|
-
test.ok(result, `getColorFlags('${color}') should not be null`);
|
|
1364
|
-
test.ok(Array.isArray(result.add), `${color} should have add array`);
|
|
1365
|
-
test.ok(Array.isArray(result.remove), `${color} should have remove array`);
|
|
1366
|
-
}
|
|
1367
|
-
test.done();
|
|
1368
|
-
};
|
|
1369
|
-
|
|
1370
|
-
module.exports['Tools: getColorFlags red has Flagged but no MailFlagBit set'] = test => {
|
|
1371
|
-
let result = tools.getColorFlags('red');
|
|
1372
|
-
// red = index 0, all bits 0 — adds \\Flagged, removes all MailFlagBit flags
|
|
1373
|
-
test.ok(result.add.includes('\\Flagged'));
|
|
1374
|
-
test.ok(result.remove.includes('$MailFlagBit0'));
|
|
1375
|
-
test.ok(result.remove.includes('$MailFlagBit1'));
|
|
1376
|
-
test.ok(result.remove.includes('$MailFlagBit2'));
|
|
1377
|
-
test.done();
|
|
1378
|
-
};
|
|
1379
|
-
|
|
1380
|
-
module.exports['Tools: getColorFlags orange has MailFlagBit0 set'] = test => {
|
|
1381
|
-
let result = tools.getColorFlags('orange');
|
|
1382
|
-
// orange = index 1, bit 0 set
|
|
1383
|
-
test.ok(result.add.includes('\\Flagged'));
|
|
1384
|
-
test.ok(result.add.includes('$MailFlagBit0'));
|
|
1385
|
-
test.ok(result.remove.includes('$MailFlagBit1'));
|
|
1386
|
-
test.ok(result.remove.includes('$MailFlagBit2'));
|
|
1387
|
-
test.done();
|
|
1388
|
-
};
|
|
1389
|
-
|
|
1390
|
-
module.exports['Tools: getColorFlags yellow has MailFlagBit1 set'] = test => {
|
|
1391
|
-
let result = tools.getColorFlags('yellow');
|
|
1392
|
-
// yellow = index 2, bit 1 set
|
|
1393
|
-
test.ok(result.add.includes('\\Flagged'));
|
|
1394
|
-
test.ok(result.add.includes('$MailFlagBit1'));
|
|
1395
|
-
test.ok(result.remove.includes('$MailFlagBit0'));
|
|
1396
|
-
test.ok(result.remove.includes('$MailFlagBit2'));
|
|
1397
|
-
test.done();
|
|
1398
|
-
};
|
|
1399
|
-
|
|
1400
|
-
module.exports['Tools: getColorFlags returns null for invalid color'] = test => {
|
|
1401
|
-
test.equal(tools.getColorFlags('invalid'), null);
|
|
1402
|
-
test.equal(tools.getColorFlags('pink'), null);
|
|
1403
|
-
test.done();
|
|
1404
|
-
};
|
|
1405
|
-
|
|
1406
|
-
module.exports['Tools: getColorFlags with null returns result not null'] = test => {
|
|
1407
|
-
// null input: colorCode becomes null, which is not < 0, so it falls through
|
|
1408
|
-
let result = tools.getColorFlags(null);
|
|
1409
|
-
test.ok(result);
|
|
1410
|
-
test.ok(result.remove.includes('\\Flagged'));
|
|
1411
|
-
test.done();
|
|
1412
|
-
};
|
|
1413
|
-
|
|
1414
|
-
// ============================================
|
|
1415
|
-
// formatMessageResponse: OBJECTID NIL handling (RFC 8474)
|
|
1416
|
-
// ============================================
|
|
1417
|
-
|
|
1418
|
-
module.exports['Tools: formatMessageResponse handles THREADID NIL'] = async test => {
|
|
1419
|
-
// RFC 8474 allows `THREADID NIL` when the server has no thread relation to
|
|
1420
|
-
// report (e.g. Strato). The NIL token is parsed as null and must not crash.
|
|
1421
|
-
let untagged = await parser('* 1 FETCH (UID 1 EMAILID (E1) THREADID NIL FLAGS (\\Seen) MODSEQ (4312))');
|
|
1422
|
-
let result = await tools.formatMessageResponse(untagged, {});
|
|
1423
|
-
test.equal(result.seq, 1);
|
|
1424
|
-
test.equal(result.uid, 1);
|
|
1425
|
-
test.equal(result.emailId, 'E1');
|
|
1426
|
-
test.equal(result.threadId, undefined);
|
|
1427
|
-
test.ok(result.flags.has('\\Seen'));
|
|
1428
|
-
test.equal(result.modseq, 4312n);
|
|
1429
|
-
test.done();
|
|
1430
|
-
};
|
|
1431
|
-
|
|
1432
|
-
module.exports['Tools: formatMessageResponse handles normal THREADID'] = async test => {
|
|
1433
|
-
let untagged = await parser('* 2 FETCH (THREADID (T9999) EMAILID (E2))');
|
|
1434
|
-
let result = await tools.formatMessageResponse(untagged, {});
|
|
1435
|
-
test.equal(result.threadId, 'T9999');
|
|
1436
|
-
test.equal(result.emailId, 'E2');
|
|
1437
|
-
test.done();
|
|
1438
|
-
};
|
|
1439
|
-
|
|
1440
|
-
// ============================================
|
|
1441
|
-
// formatMessageResponse: BINARY vs BODY part tracking (RFC 3516 / RFC 9051)
|
|
1442
|
-
// ============================================
|
|
1443
|
-
|
|
1444
|
-
module.exports['Tools: formatMessageResponse records which parts arrived via BINARY'] = async test => {
|
|
1445
|
-
// Parts answered as BINARY[...] arrive with the content-transfer-encoding
|
|
1446
|
-
// already decoded by the server; consumers (download/downloadMany) use the
|
|
1447
|
-
// binaryParts set to skip their own decoder for exactly those parts
|
|
1448
|
-
let untagged = await parser('* 1 FETCH (UID 7 BINARY[1] {4}\r\n BODY[2] {4}\r\n)', {
|
|
1449
|
-
literals: [Buffer.from('AAAA'), Buffer.from('BBBB')]
|
|
1450
|
-
});
|
|
1451
|
-
let result = await tools.formatMessageResponse(untagged, {});
|
|
1452
|
-
test.equal(result.bodyParts.get('1').toString(), 'AAAA');
|
|
1453
|
-
test.equal(result.bodyParts.get('2').toString(), 'BBBB');
|
|
1454
|
-
test.ok(result.binaryParts, 'binaryParts set should exist when a BINARY part arrived');
|
|
1455
|
-
test.ok(result.binaryParts.has('1'), 'BINARY-answered part is recorded');
|
|
1456
|
-
test.ok(!result.binaryParts.has('2'), 'BODY-answered part is not recorded');
|
|
1457
|
-
test.done();
|
|
1458
|
-
};
|
|
1459
|
-
|
|
1460
|
-
module.exports['Tools: formatMessageResponse leaves binaryParts unset for plain BODY fetches'] = async test => {
|
|
1461
|
-
let untagged = await parser('* 1 FETCH (UID 8 BODY[1] {4}\r\n)', { literals: [Buffer.from('CCCC')] });
|
|
1462
|
-
let result = await tools.formatMessageResponse(untagged, {});
|
|
1463
|
-
test.equal(result.bodyParts.get('1').toString(), 'CCCC');
|
|
1464
|
-
test.equal(result.binaryParts, undefined);
|
|
1465
|
-
test.done();
|
|
1466
|
-
};
|
|
1467
|
-
|
|
1468
|
-
// ============================================
|
|
1469
|
-
// formatMessageResponse: non-ASCII mailbox path normalization for stable id
|
|
1470
|
-
// ============================================
|
|
1471
|
-
|
|
1472
|
-
module.exports['Tools: formatMessageResponse normalizes non-ASCII mailbox path for id'] = async test => {
|
|
1473
|
-
// No EMAILID, so the message id falls back to an md5 over [path, uidValidity, uid].
|
|
1474
|
-
// The mailbox path is non-ASCII and must be modified-UTF-7 normalized before hashing.
|
|
1475
|
-
// (A previous bogus regex /[0x80-0xff]/ never matched non-ASCII, skipping normalization.)
|
|
1476
|
-
let untagged = await parser('* 1 FETCH (UID 5 FLAGS (\\Seen))');
|
|
1477
|
-
let mailbox = { path: '日本語', uidValidity: 123n };
|
|
1478
|
-
let result = await tools.formatMessageResponse(untagged, mailbox);
|
|
1479
|
-
|
|
1480
|
-
let encodedPath = iconv.encode('日本語', 'utf-7-imap').toString();
|
|
1481
|
-
let expectedId = crypto.createHash('md5').update([encodedPath, '123', '5'].join(':')).digest('hex');
|
|
1482
|
-
let rawId = crypto.createHash('md5').update(['日本語', '123', '5'].join(':')).digest('hex');
|
|
1483
|
-
|
|
1484
|
-
test.equal(result.id, expectedId, 'id must hash the UTF-7 normalized path');
|
|
1485
|
-
test.notEqual(result.id, rawId, 'id must not hash the raw non-ASCII path');
|
|
1486
|
-
test.done();
|
|
1487
|
-
};
|
|
1488
|
-
|
|
1489
|
-
module.exports['Tools: formatMessageResponse parses the full attribute set'] = async test => {
|
|
1490
|
-
let untagged = await parser(
|
|
1491
|
-
'* 3 FETCH (UID 100 RFC822.SIZE 5000 FLAGS (\\Seen \\Flagged) MODSEQ (12345) ' +
|
|
1492
|
-
'X-GM-MSGID 999 X-GM-THRID 888 X-GM-LABELS (\\Important Work) ' +
|
|
1493
|
-
'INTERNALDATE "12-Jan-2020 10:00:00 +0000" BODY[1] {3}\r\n BODY[HEADER] {2}\r\n)',
|
|
1494
|
-
{ literals: [Buffer.from('abc'), Buffer.from('hi')] }
|
|
1495
|
-
);
|
|
1496
|
-
let mailbox = { path: 'INBOX', uidValidity: 1n }; // no uidNext/highestModseq -> exercise the bump branches
|
|
1497
|
-
let r = await tools.formatMessageResponse(untagged, mailbox);
|
|
1498
|
-
test.equal(r.uid, 100);
|
|
1499
|
-
test.equal(r.size, 5000);
|
|
1500
|
-
test.ok(r.flags.has('\\Seen'));
|
|
1501
|
-
test.equal(r.modseq, 12345n);
|
|
1502
|
-
test.equal(r.emailId, '999'); // X-GM-MSGID
|
|
1503
|
-
test.equal(r.threadId, '888'); // X-GM-THRID
|
|
1504
|
-
test.ok(r.labels.has('Work'));
|
|
1505
|
-
test.ok(r.internalDate instanceof Date);
|
|
1506
|
-
test.ok(Buffer.isBuffer(r.headers));
|
|
1507
|
-
test.ok(r.bodyParts.get('1'));
|
|
1508
|
-
// mailbox estimates bumped from the FETCH data
|
|
1509
|
-
test.equal(mailbox.uidNext, 101);
|
|
1510
|
-
test.equal(mailbox.highestModseq, 12345n);
|
|
1511
|
-
test.done();
|
|
1512
|
-
};
|
|
1513
|
-
|
|
1514
|
-
module.exports['Tools: formatMessageResponse parses envelope and bodystructure'] = async test => {
|
|
1515
|
-
let untagged = await parser(
|
|
1516
|
-
'* 1 FETCH (ENVELOPE ("Mon, 2 Sep 2013 05:30:13 -0700" "Subject" ((NIL NIL "a" "b.com")) NIL NIL NIL NIL NIL NIL "<id@x>") ' +
|
|
1517
|
-
'BODYSTRUCTURE ("TEXT" "PLAIN" ("CHARSET" "utf-8") NIL NIL "7BIT" 12 0 NIL NIL NIL))'
|
|
1518
|
-
);
|
|
1519
|
-
let r = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
|
|
1520
|
-
test.ok(r.envelope);
|
|
1521
|
-
test.equal(r.envelope.subject, 'Subject');
|
|
1522
|
-
test.ok(r.bodyStructure);
|
|
1523
|
-
test.equal(r.bodyStructure.type, 'text/plain');
|
|
1524
|
-
test.done();
|
|
1525
|
-
};
|
|
1526
|
-
|
|
1527
|
-
module.exports['Tools: formatMessageResponse source from BODY[] and BINARY[] literals'] = async test => {
|
|
1528
|
-
let untagged = await parser('* 1 FETCH (BODY[] {5}\r\n)', { literals: [Buffer.from('HELLO')] });
|
|
1529
|
-
let r = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
|
|
1530
|
-
test.ok(Buffer.isBuffer(r.source));
|
|
1531
|
-
test.equal(r.source.toString(), 'HELLO');
|
|
1532
|
-
test.done();
|
|
1533
|
-
};
|
|
1534
|
-
|
|
1535
|
-
module.exports['Tools: formatMessageResponse keeps invalid INTERNALDATE as raw string'] = async test => {
|
|
1536
|
-
let untagged = await parser('* 1 FETCH (INTERNALDATE "not a date")');
|
|
1537
|
-
let r = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
|
|
1538
|
-
test.equal(r.internalDate, 'not a date');
|
|
1539
|
-
test.done();
|
|
1540
|
-
};
|
|
1541
|
-
|
|
1542
|
-
module.exports['Tools: parseEnvelope parses every field'] = async test => {
|
|
1543
|
-
let untagged = await parser(
|
|
1544
|
-
'* 1 FETCH (ENVELOPE ("Mon, 2 Sep 2013 05:30:13 -0700" "Hello" ' +
|
|
1545
|
-
'((NIL NIL "from" "x.com")) ((NIL NIL "sender" "x.com")) ((NIL NIL "reply" "x.com")) ' +
|
|
1546
|
-
'((NIL NIL "to" "x.com")) ((NIL NIL "cc" "x.com")) ((NIL NIL "bcc" "x.com")) ' +
|
|
1547
|
-
'"<inreplyto@x>" "<msgid@x>"))'
|
|
1548
|
-
);
|
|
1549
|
-
let env = tools.parseEnvelope(untagged.attributes[1][1]);
|
|
1550
|
-
test.ok(env.date instanceof Date);
|
|
1551
|
-
test.equal(env.subject, 'Hello');
|
|
1552
|
-
test.equal(env.from[0].address, 'from@x.com');
|
|
1553
|
-
test.equal(env.sender[0].address, 'sender@x.com');
|
|
1554
|
-
test.equal(env.replyTo[0].address, 'reply@x.com');
|
|
1555
|
-
test.equal(env.to[0].address, 'to@x.com');
|
|
1556
|
-
test.equal(env.cc[0].address, 'cc@x.com');
|
|
1557
|
-
test.equal(env.bcc[0].address, 'bcc@x.com');
|
|
1558
|
-
test.equal(env.inReplyTo, '<inreplyto@x>');
|
|
1559
|
-
test.equal(env.messageId, '<msgid@x>');
|
|
1560
|
-
test.done();
|
|
1561
|
-
};
|
|
1562
|
-
|
|
1563
|
-
module.exports['Tools: parseEnvelope keeps invalid date as string'] = async test => {
|
|
1564
|
-
let untagged = await parser('* 1 FETCH (ENVELOPE ("not a date" "Subj" NIL NIL NIL NIL NIL NIL NIL NIL))');
|
|
1565
|
-
let env = tools.parseEnvelope(untagged.attributes[1][1]);
|
|
1566
|
-
test.equal(env.date, 'not a date');
|
|
1567
|
-
test.done();
|
|
1568
|
-
};
|
|
1569
|
-
|
|
1570
|
-
module.exports['Tools: parseBodystructure parses all single-part extension fields'] = async test => {
|
|
1571
|
-
let bs =
|
|
1572
|
-
'("TEXT" "PLAIN" ("CHARSET" "utf-8") "<cid@x>" "a description" "BASE64" 100 5 ' +
|
|
1573
|
-
'"d41d8cd9" ("attachment" ("filename" "f.txt")) ("en" "de") "http://loc/")';
|
|
1574
|
-
let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
|
|
1575
|
-
let node = tools.parseBodystructure(untagged.attributes[1][1]);
|
|
1576
|
-
test.equal(node.type, 'text/plain');
|
|
1577
|
-
test.equal(node.id, '<cid@x>');
|
|
1578
|
-
test.equal(node.description, 'a description');
|
|
1579
|
-
test.equal(node.encoding, 'base64');
|
|
1580
|
-
test.equal(node.size, 100);
|
|
1581
|
-
test.equal(node.lineCount, 5);
|
|
1582
|
-
test.equal(node.md5, 'd41d8cd9');
|
|
1583
|
-
test.equal(node.disposition, 'attachment');
|
|
1584
|
-
test.equal(node.dispositionParameters.filename, 'f.txt');
|
|
1585
|
-
test.deepEqual(node.language, ['en', 'de']);
|
|
1586
|
-
test.done();
|
|
1587
|
-
};
|
|
1588
|
-
|
|
1589
|
-
module.exports['Tools: parseBodystructure parses message/rfc822 with envelope and child'] = async test => {
|
|
1590
|
-
let bs =
|
|
1591
|
-
'("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 200 ' +
|
|
1592
|
-
'("date" "subj" NIL NIL NIL NIL NIL NIL "<reply>" "<msgid>") ' +
|
|
1593
|
-
'("TEXT" "PLAIN" NIL NIL NIL "7BIT" 10 1) 3)';
|
|
1594
|
-
let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
|
|
1595
|
-
let node = tools.parseBodystructure(untagged.attributes[1][1]);
|
|
1596
|
-
test.equal(node.type, 'message/rfc822');
|
|
1597
|
-
test.ok(node.envelope);
|
|
1598
|
-
test.equal(node.childNodes.length, 1);
|
|
1599
|
-
test.equal(node.lineCount, 3);
|
|
1600
|
-
test.done();
|
|
1601
|
-
};
|
|
1602
|
-
|
|
1603
|
-
module.exports['Tools: parseBodystructure parses multipart with params and disposition'] = async test => {
|
|
1604
|
-
let bs = '(("TEXT" "PLAIN" NIL NIL NIL "7BIT" 10 1)("TEXT" "HTML" NIL NIL NIL "7BIT" 20 2) "ALTERNATIVE" ("BOUNDARY" "xyz") ("inline" NIL) ("en"))';
|
|
1605
|
-
let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
|
|
1606
|
-
let node = tools.parseBodystructure(untagged.attributes[1][1]);
|
|
1607
|
-
test.equal(node.type, 'multipart/alternative');
|
|
1608
|
-
test.equal(node.childNodes.length, 2);
|
|
1609
|
-
test.equal(node.parameters.boundary, 'xyz');
|
|
1610
|
-
test.equal(node.disposition, 'inline');
|
|
1611
|
-
test.done();
|
|
1612
|
-
};
|
|
1613
|
-
|
|
1614
|
-
module.exports['Tools: formatMessageResponse handles NIL, Buffer and unknown keys'] = async test => {
|
|
1615
|
-
let untagged = await parser('* 1 FETCH (UID NIL RFC822.SIZE NIL X-GM-MSGID {3}\r\n BODY[] NIL FOOBAR 123)', {
|
|
1616
|
-
literals: [Buffer.from('999')]
|
|
1617
|
-
});
|
|
1618
|
-
let r = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
|
|
1619
|
-
test.equal(r.uid, undefined); // a NIL UID is left unset rather than reported as UID 0
|
|
1620
|
-
test.equal(r.size, 0); // size keeps its 0 default
|
|
1621
|
-
|
|
1622
|
-
test.equal(r.emailId, '999'); // Buffer literal value -> getString stringifies it
|
|
1623
|
-
test.equal(r.source, false); // BODY[] NIL -> getBuffer returns false
|
|
1624
|
-
test.done();
|
|
1625
|
-
};
|
|
1626
|
-
|
|
1627
|
-
module.exports['Tools: getFolderTree merges duplicate folder entries'] = test => {
|
|
1628
|
-
let tree = tools.getFolderTree([
|
|
1629
|
-
{ name: 'Parent', path: 'Parent', parent: [], flags: new Set(['\\HasChildren']), delimiter: '/' },
|
|
1630
|
-
{ name: 'Parent', path: 'Parent', parent: [], flags: new Set(['\\Noselect', '\\HasChildren']), specialUse: '\\Sent', delimiter: '/' }
|
|
1631
|
-
]);
|
|
1632
|
-
test.equal(tree.folders.length, 1);
|
|
1633
|
-
test.equal(tree.folders[0].disabled, true);
|
|
1634
|
-
test.equal(tree.folders[0].specialUse, '\\Sent');
|
|
1635
|
-
test.done();
|
|
1636
|
-
};
|
|
1637
|
-
|
|
1638
|
-
module.exports['Tools: parseBodystructure handles minimal NIL fields'] = async test => {
|
|
1639
|
-
let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ("APPLICATION" "OCTET-STREAM" NIL NIL NIL NIL NIL))');
|
|
1640
|
-
let node = tools.parseBodystructure(untagged.attributes[1][1]);
|
|
1641
|
-
test.equal(node.type, 'application/octet-stream');
|
|
1642
|
-
test.equal(node.id, undefined);
|
|
1643
|
-
test.equal(node.encoding, undefined);
|
|
1644
|
-
test.done();
|
|
1645
|
-
};
|
|
1646
|
-
|
|
1647
|
-
module.exports['Tools: parseBodystructure decodes RFC 2231 charset continuation params'] = async test => {
|
|
1648
|
-
let bs = '("TEXT" "PLAIN" ("name*0*" "utf-8\'\'%E2%82%AC abc" "name*1" "def") NIL NIL "7BIT" 10 1)';
|
|
1649
|
-
let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
|
|
1650
|
-
let node = tools.parseBodystructure(untagged.attributes[1][1]);
|
|
1651
|
-
test.equal(node.parameters.name, '€ abcdef');
|
|
1652
|
-
test.done();
|
|
1653
|
-
};
|
|
1654
|
-
|
|
1655
|
-
module.exports['Tools: parseBodystructure handles empty-string extension fields'] = async test => {
|
|
1656
|
-
// Empty-string values exercise the `|| ''` / `|| 0` fallbacks in each field.
|
|
1657
|
-
let bs = '("TEXT" "PLAIN" ("CHARSET" "") "" "" "" 0 0 "" ("" ("" "")) ("") "")';
|
|
1658
|
-
let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
|
|
1659
|
-
let node = tools.parseBodystructure(untagged.attributes[1][1]);
|
|
1660
|
-
test.equal(node.id, '');
|
|
1661
|
-
test.equal(node.description, '');
|
|
1662
|
-
test.equal(node.size, 0);
|
|
1663
|
-
test.equal(node.md5, '');
|
|
1664
|
-
test.deepEqual(node.language, ['']);
|
|
1665
|
-
test.done();
|
|
1666
|
-
};
|
|
1667
|
-
|
|
1668
|
-
module.exports['Tools: formatMessageResponse with no data list returns just seq'] = async test => {
|
|
1669
|
-
let untagged = await parser('* 5 FETCH');
|
|
1670
|
-
let r = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
|
|
1671
|
-
test.equal(r.seq, 5);
|
|
1672
|
-
test.done();
|
|
1673
|
-
};
|
|
1674
|
-
|
|
1675
|
-
module.exports['Tools: getFolderTree adds folders array when existing entry gains HasChildren'] = test => {
|
|
1676
|
-
let tree = tools.getFolderTree([
|
|
1677
|
-
{ name: 'P', path: 'P', parent: [], flags: new Set([]), delimiter: '/' },
|
|
1678
|
-
{ name: 'P', path: 'P', parent: [], flags: new Set(['\\HasChildren']), delimiter: '/' }
|
|
1679
|
-
]);
|
|
1680
|
-
test.ok(Array.isArray(tree.folders[0].folders), 'folders array created on merge');
|
|
1681
|
-
test.done();
|
|
1682
|
-
};
|
|
1683
|
-
|
|
1684
|
-
module.exports['Tools: normalizePath joins array with empty delimiter fallback'] = test => {
|
|
1685
|
-
// namespace present but without a delimiter -> the (... || '') fallback joins directly
|
|
1686
|
-
let connection = { namespace: { prefix: '' } };
|
|
1687
|
-
test.equal(tools.normalizePath(connection, ['a', 'b']), 'ab');
|
|
1688
|
-
test.done();
|
|
1689
|
-
};
|
|
1690
|
-
|
|
1691
|
-
module.exports['Tools: updateCapabilities defaults non-numeric APPENDLIMIT to 0'] = test => {
|
|
1692
|
-
let caps = tools.updateCapabilities([{ value: 'APPENDLIMIT=abc' }]);
|
|
1693
|
-
test.equal(caps.get('APPENDLIMIT'), 0);
|
|
1694
|
-
test.done();
|
|
1695
|
-
};
|
|
1696
|
-
|
|
1697
|
-
module.exports['Tools: formatMessageResponse skips non-string label entries'] = async test => {
|
|
1698
|
-
let untagged = await parser('* 1 FETCH (X-GM-LABELS (\\Important NIL))');
|
|
1699
|
-
let r = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
|
|
1700
|
-
test.deepEqual([...r.labels], ['\\Important']); // NIL entry filtered out
|
|
1701
|
-
test.done();
|
|
1702
|
-
};
|
|
1703
|
-
|
|
1704
|
-
module.exports['Tools: formatMessageResponse does not lower highestModseq'] = async test => {
|
|
1705
|
-
let mailbox = { path: 'INBOX', highestModseq: 99999n };
|
|
1706
|
-
let untagged = await parser('* 1 FETCH (MODSEQ (5) FLAGS (\\Seen))');
|
|
1707
|
-
await tools.formatMessageResponse(untagged, mailbox);
|
|
1708
|
-
test.equal(mailbox.highestModseq, 99999n); // unchanged: incoming modseq is lower
|
|
1709
|
-
test.done();
|
|
1710
|
-
};
|
|
1711
|
-
|
|
1712
|
-
module.exports['Tools: parseBodystructure multipart with NIL subtype'] = async test => {
|
|
1713
|
-
let untagged = await parser('* 1 FETCH (BODYSTRUCTURE (("TEXT" "PLAIN" NIL NIL NIL "7BIT" 1 1) NIL))');
|
|
1714
|
-
let node = tools.parseBodystructure(untagged.attributes[1][1]);
|
|
1715
|
-
test.equal(node.type, 'multipart/');
|
|
1716
|
-
test.done();
|
|
1717
|
-
};
|
|
1718
|
-
|
|
1719
|
-
module.exports['Tools: parseBodystructure content type with NIL type/subtype'] = async test => {
|
|
1720
|
-
let untagged = await parser('* 1 FETCH (BODYSTRUCTURE (NIL NIL NIL NIL NIL "7BIT" 1))');
|
|
1721
|
-
let node = tools.parseBodystructure(untagged.attributes[1][1]);
|
|
1722
|
-
test.equal(node.type, '/');
|
|
1723
|
-
test.done();
|
|
1724
|
-
};
|
|
1725
|
-
|
|
1726
|
-
module.exports['Tools: parseBodystructure message/rfc822 with NIL envelope/linecount'] = async test => {
|
|
1727
|
-
let bs = '("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 100 NIL ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 1 1) NIL NIL (NIL) NIL)';
|
|
1728
|
-
let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
|
|
1729
|
-
let node = tools.parseBodystructure(untagged.attributes[1][1]);
|
|
1730
|
-
test.equal(node.type, 'message/rfc822');
|
|
1731
|
-
test.done();
|
|
1732
|
-
};
|
|
1733
|
-
|
|
1734
|
-
module.exports['Tools: parseBodystructure text with NIL language/location entries'] = async test => {
|
|
1735
|
-
let bs = '("TEXT" "PLAIN" NIL NIL NIL "7BIT" 100 NIL "md5" ("inline" NIL) (NIL) NIL)';
|
|
1736
|
-
let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
|
|
1737
|
-
let node = tools.parseBodystructure(untagged.attributes[1][1]);
|
|
1738
|
-
test.deepEqual(node.language, ['']);
|
|
1739
|
-
test.done();
|
|
1740
|
-
};
|
|
1741
|
-
|
|
1742
|
-
module.exports['Tools: parseBodystructure empty-string size/linecount/language/location'] = async test => {
|
|
1743
|
-
let bs = '("TEXT" "PLAIN" NIL NIL NIL "7BIT" "" "" "" ("inline" NIL) ("") "")';
|
|
1744
|
-
let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
|
|
1745
|
-
let node = tools.parseBodystructure(untagged.attributes[1][1]);
|
|
1746
|
-
test.equal(node.size, 0);
|
|
1747
|
-
test.equal(node.lineCount, 0);
|
|
1748
|
-
test.deepEqual(node.language, ['']);
|
|
1749
|
-
test.done();
|
|
1750
|
-
};
|
|
1751
|
-
|
|
1752
|
-
module.exports['Tools: parseBodystructure message/rfc822 empty-string linecount'] = async test => {
|
|
1753
|
-
let bs = '("MESSAGE" "RFC822" NIL NIL NIL "7BIT" 100 ("d" "s" NIL NIL NIL NIL NIL NIL NIL NIL) ("TEXT" "PLAIN" NIL NIL NIL "7BIT" 1 1) "")';
|
|
1754
|
-
let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
|
|
1755
|
-
let node = tools.parseBodystructure(untagged.attributes[1][1]);
|
|
1756
|
-
test.equal(node.type, 'message/rfc822');
|
|
1757
|
-
test.equal(node.lineCount, 0);
|
|
1758
|
-
test.done();
|
|
1759
|
-
};
|
|
1760
|
-
|
|
1761
|
-
module.exports['Tools: parseBodystructure decodes RFC 2231 with empty charset and special chars'] = async test => {
|
|
1762
|
-
// Empty charset before the first quote defaults to utf-8; '=' / '?' in the value
|
|
1763
|
-
// exercise the 2-char hex escape branch; the empty *1* continuation segment too.
|
|
1764
|
-
// value includes '=' / '?' (2-char hex escapes), a space (-> '_') and a TAB
|
|
1765
|
-
// (a <0x10 control char -> single-hex-digit escape needing a '0' pad).
|
|
1766
|
-
let bs = '("TEXT" "PLAIN" ("name*0*" "\'\'a=b?c d\te" "name*1*" "") NIL NIL "7BIT" 1 1)';
|
|
1767
|
-
let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
|
|
1768
|
-
let node = tools.parseBodystructure(untagged.attributes[1][1]);
|
|
1769
|
-
test.ok(node.parameters.name.includes('a=b?c'));
|
|
1770
|
-
test.done();
|
|
1771
|
-
};
|
|
1772
|
-
|
|
1773
|
-
module.exports['Tools: parseBodystructure parses empty-string body location'] = async test => {
|
|
1774
|
-
// A trailing element after location makes the (i < length-1) guard pass so the
|
|
1775
|
-
// empty-string location value goes through the `|| ''` fallback.
|
|
1776
|
-
let bs = '("TEXT" "PLAIN" NIL NIL NIL "7BIT" 1 1 NIL NIL NIL "" NIL)';
|
|
1777
|
-
let untagged = await parser('* 1 FETCH (BODYSTRUCTURE ' + bs + ')');
|
|
1778
|
-
let node = tools.parseBodystructure(untagged.attributes[1][1]);
|
|
1779
|
-
test.equal(node.location, '');
|
|
1780
|
-
test.done();
|
|
1781
|
-
};
|
|
1782
|
-
|
|
1783
|
-
module.exports['Tools: expandRange skips descending range to non-numeric second bound'] = test => {
|
|
1784
|
-
// Endpoints that are not valid nz-numbers make the whole entry invalid -
|
|
1785
|
-
// the pre-hardening behavior expanded '5:abc' down to 0
|
|
1786
|
-
test.deepEqual(tools.expandRange('5:abc'), []);
|
|
1787
|
-
test.done();
|
|
1788
|
-
};
|
|
1789
|
-
|
|
1790
|
-
module.exports['Tools: expandRange skips non-numeric entries but keeps valid ones'] = test => {
|
|
1791
|
-
test.deepEqual(tools.expandRange('abc,:5,1:3'), [1, 2, 3]);
|
|
1792
|
-
test.done();
|
|
1793
|
-
};
|
|
1794
|
-
|
|
1795
|
-
module.exports['Tools: expandRange handles descending ranges'] = test => {
|
|
1796
|
-
test.deepEqual(tools.expandRange('5:3'), [5, 4, 3]);
|
|
1797
|
-
test.done();
|
|
1798
|
-
};
|
|
1799
|
-
|
|
1800
|
-
module.exports['Tools: encodePath keeps name as-is when iconv encode throws'] = test => {
|
|
1801
|
-
// The iconv module object is shared by reference; patch encode to throw so the
|
|
1802
|
-
// defensive catch is exercised, then restore.
|
|
1803
|
-
let original = iconv.encode;
|
|
1804
|
-
iconv.encode = () => {
|
|
1805
|
-
throw new Error('encode boom');
|
|
1806
|
-
};
|
|
1807
|
-
try {
|
|
1808
|
-
let connection = { enabled: new Set(), capabilities: new Map() };
|
|
1809
|
-
let result = tools.encodePath(connection, 'Tärä');
|
|
1810
|
-
test.equal(result, 'Tärä', 'falls back to the raw path');
|
|
1811
|
-
} finally {
|
|
1812
|
-
iconv.encode = original;
|
|
1813
|
-
}
|
|
1814
|
-
test.done();
|
|
1815
|
-
};
|
|
1816
|
-
|
|
1817
|
-
module.exports['Tools: decodePath keeps name as-is when iconv decode throws'] = test => {
|
|
1818
|
-
let original = iconv.decode;
|
|
1819
|
-
iconv.decode = () => {
|
|
1820
|
-
throw new Error('decode boom');
|
|
1821
|
-
};
|
|
1822
|
-
try {
|
|
1823
|
-
let connection = { enabled: new Set(), capabilities: new Map() };
|
|
1824
|
-
let result = tools.decodePath(connection, 'Inbox&AOk-');
|
|
1825
|
-
test.equal(result, 'Inbox&AOk-');
|
|
1826
|
-
} finally {
|
|
1827
|
-
iconv.decode = original;
|
|
1828
|
-
}
|
|
1829
|
-
test.done();
|
|
1830
|
-
};
|
|
1831
|
-
|
|
1832
|
-
module.exports['Tools: formatMessageResponse id falls back when iconv encode throws'] = async test => {
|
|
1833
|
-
let original = iconv.encode;
|
|
1834
|
-
iconv.encode = () => {
|
|
1835
|
-
throw new Error('encode boom');
|
|
1836
|
-
};
|
|
1837
|
-
try {
|
|
1838
|
-
let untagged = await parser('* 1 FETCH (UID 5 FLAGS (\\Seen))');
|
|
1839
|
-
// non-ASCII path triggers the encode branch; the throw is swallowed
|
|
1840
|
-
let r = await tools.formatMessageResponse(untagged, { path: 'Tärä', uidValidity: 1n });
|
|
1841
|
-
test.ok(r.id, 'an id is still produced');
|
|
1842
|
-
} finally {
|
|
1843
|
-
iconv.encode = original;
|
|
1844
|
-
}
|
|
1845
|
-
test.done();
|
|
1846
|
-
};
|
|
1847
|
-
|
|
1848
|
-
// ============================================
|
|
1849
|
-
// packMessageRange
|
|
1850
|
-
// ============================================
|
|
1851
|
-
|
|
1852
|
-
module.exports['Tools: packMessageRange packs contiguous and gapped ranges'] = test => {
|
|
1853
|
-
test.equal(tools.packMessageRange([1, 2, 3, 5, 7, 8]), '1:3,5,7:8');
|
|
1854
|
-
test.equal(tools.packMessageRange(7), '7');
|
|
1855
|
-
test.equal(tools.packMessageRange([]), '');
|
|
1856
|
-
test.done();
|
|
1857
|
-
};
|
|
1858
|
-
|
|
1859
|
-
module.exports['Tools: packMessageRange dedupes duplicate values'] = test => {
|
|
1860
|
-
// Duplicates must not produce overlapping/non-canonical tokens like "1,1:3".
|
|
1861
|
-
test.equal(tools.packMessageRange([1, 1, 2, 3]), '1:3');
|
|
1862
|
-
test.equal(tools.packMessageRange([3, 1, 2, 2, 5]), '1:3,5');
|
|
1863
|
-
test.equal(tools.packMessageRange([5, 5, 5]), '5');
|
|
1864
|
-
test.done();
|
|
1865
|
-
};
|
|
1866
|
-
|
|
1867
|
-
// ============================================
|
|
1868
|
-
// Security regression tests: hostile server input
|
|
1869
|
-
// ============================================
|
|
1870
|
-
|
|
1871
|
-
module.exports['Tools: getStructuredParams drops __proto__ continuation parameters'] = test => {
|
|
1872
|
-
// A parameter named "__proto__*0*" groups under "__proto__": without a guard the
|
|
1873
|
-
// grouping wrote attacker-controlled values onto Object.prototype (and then threw)
|
|
1874
|
-
let result = tools.getStructuredParams([{ value: '__proto__*0*' }, { value: "utf-8''polluted" }]);
|
|
1875
|
-
test.deepEqual(result, {});
|
|
1876
|
-
test.equal({}.charset, undefined, 'Object.prototype must stay clean');
|
|
1877
|
-
test.done();
|
|
1878
|
-
};
|
|
1879
|
-
|
|
1880
|
-
module.exports['Tools: getStructuredParams drops a plain __proto__ parameter'] = test => {
|
|
1881
|
-
// A plain "__proto__" parameter never reaches the output and never touches the prototype.
|
|
1882
|
-
// The string value alone cannot pollute (the __proto__ setter ignores primitives) - the
|
|
1883
|
-
// object-valued continuation form above is the case the live guard covers - so this
|
|
1884
|
-
// pins the output contract: the parameter is dropped, the rest of the list survives.
|
|
1885
|
-
let result = tools.getStructuredParams([{ value: '__proto__' }, { value: 'polluted' }, { value: 'a' }, { value: 'b' }]);
|
|
1886
|
-
test.deepEqual(result, { a: 'b' });
|
|
1887
|
-
test.ok(!Object.prototype.hasOwnProperty.call(result, '__proto__'), 'no own __proto__ property is created');
|
|
1888
|
-
test.equal(Object.getPrototypeOf(result), Object.prototype, 'the prototype must stay untouched');
|
|
1889
|
-
test.done();
|
|
1890
|
-
};
|
|
1891
|
-
|
|
1892
|
-
module.exports['Tools: formatMessageResponse ignores a non-numeric MODSEQ'] = async test => {
|
|
1893
|
-
// BigInt() throws on garbage, and the throw used to drop the whole message
|
|
1894
|
-
// from the result set - a malformed MODSEQ must be skipped instead
|
|
1895
|
-
let untagged = await parser('* 1 FETCH (UID 1 MODSEQ (abc) FLAGS (\\Seen))');
|
|
1896
|
-
let result = await tools.formatMessageResponse(untagged, {});
|
|
1897
|
-
test.equal(result.uid, 1);
|
|
1898
|
-
test.equal(result.modseq, undefined);
|
|
1899
|
-
test.ok(result.flags.has('\\Seen'));
|
|
1900
|
-
|
|
1901
|
-
// an unparenthesized MODSEQ yields no usable value either
|
|
1902
|
-
let untagged2 = await parser('* 1 FETCH (UID 1 MODSEQ 123)');
|
|
1903
|
-
let result2 = await tools.formatMessageResponse(untagged2, {});
|
|
1904
|
-
test.equal(result2.uid, 1);
|
|
1905
|
-
test.equal(result2.modseq, undefined);
|
|
1906
|
-
test.done();
|
|
1907
|
-
};
|
|
1908
|
-
|
|
1909
|
-
module.exports['Tools: parseEnvelope skips NIL address entries'] = test => {
|
|
1910
|
-
// A NIL inside an address list used to throw on the dereference, dropping the message
|
|
1911
|
-
let entry = [
|
|
1912
|
-
null, // date
|
|
1913
|
-
null, // subject
|
|
1914
|
-
[null, [{ value: 'Name' }, null, { value: 'user' }, { value: 'example.com' }]], // from: NIL entry + valid entry
|
|
1915
|
-
[], // sender
|
|
1916
|
-
[], // reply-to
|
|
1917
|
-
[], // to
|
|
1918
|
-
[], // cc
|
|
1919
|
-
[], // bcc
|
|
1920
|
-
null, // in-reply-to
|
|
1921
|
-
null // message-id
|
|
1922
|
-
];
|
|
1923
|
-
|
|
1924
|
-
let result = tools.parseEnvelope(entry);
|
|
1925
|
-
test.equal(result.from.length, 1);
|
|
1926
|
-
test.equal(result.from[0].address, 'user@example.com');
|
|
1927
|
-
test.done();
|
|
1928
|
-
};
|
|
1929
|
-
|
|
1930
|
-
// ============================================
|
|
1931
|
-
// Bounded parsing of untrusted numeric values
|
|
1932
|
-
// ============================================
|
|
1933
|
-
|
|
1934
|
-
module.exports['Tools: expandRange returns nothing for a non-string range'] = test => {
|
|
1935
|
-
// untaggedVanished() leaves the sequence set as `false` when the response carries only the
|
|
1936
|
-
// (EARLIER) tag, and throwing here would abort the handler for the rest of the response
|
|
1937
|
-
test.deepEqual(tools.expandRange(false), []);
|
|
1938
|
-
test.deepEqual(tools.expandRange(undefined), []);
|
|
1939
|
-
test.deepEqual(tools.expandRange(null), []);
|
|
1940
|
-
test.deepEqual(tools.expandRange(123), []);
|
|
1941
|
-
test.deepEqual(tools.expandRange(['1:3']), []);
|
|
1942
|
-
test.deepEqual(tools.expandRange('1:3'), [1, 2, 3], 'a real range still expands');
|
|
1943
|
-
test.done();
|
|
1944
|
-
};
|
|
1945
|
-
|
|
1946
|
-
module.exports['Tools: isDecimalString accepts only bounded digit runs'] = test => {
|
|
1947
|
-
test.equal(tools.isDecimalString('12', 10), true);
|
|
1948
|
-
test.equal(tools.isDecimalString('1234567890', 10), true);
|
|
1949
|
-
test.equal(tools.isDecimalString('12345678901', 10), false, 'one digit past the bound is rejected');
|
|
1950
|
-
test.equal(tools.isDecimalString('', 10), false);
|
|
1951
|
-
test.equal(tools.isDecimalString('1e5', 10), false);
|
|
1952
|
-
test.equal(tools.isDecimalString(' 12 ', 10), false);
|
|
1953
|
-
test.equal(tools.isDecimalString('0x10', 10), false);
|
|
1954
|
-
test.equal(tools.isDecimalString('-1', 10), false);
|
|
1955
|
-
test.equal(tools.isDecimalString('1.5', 10), false);
|
|
1956
|
-
test.equal(tools.isDecimalString(12, 10), false, 'only strings are accepted');
|
|
1957
|
-
test.equal(tools.isDecimalString(null, 10), false);
|
|
1958
|
-
test.done();
|
|
1959
|
-
};
|
|
1960
|
-
|
|
1961
|
-
module.exports['Tools: parseBigIntValue rejects everything BigInt would throw on'] = test => {
|
|
1962
|
-
// isNaN() passes '1e5', ' 12 ' and 'Infinity'; BigInt() throws on all three, and the throw
|
|
1963
|
-
// propagates out of a response handler that was only trying to read one field
|
|
1964
|
-
test.equal(tools.parseBigIntValue('9122'), 9122n);
|
|
1965
|
-
for (let bad of ['1e5', ' 12 ', 'Infinity', 'none', '', '-1', '1.5', null, undefined, 12, ['1']]) {
|
|
1966
|
-
test.equal(tools.parseBigIntValue(bad), false, `${JSON.stringify(bad)} must be rejected`);
|
|
1967
|
-
}
|
|
1968
|
-
test.done();
|
|
1969
|
-
};
|
|
1970
|
-
|
|
1971
|
-
module.exports['Tools: parseBigIntValue bounds the digit count before converting'] = test => {
|
|
1972
|
-
// BigInt() on a multi-megabyte digit run costs hundreds of milliseconds of non-yielding
|
|
1973
|
-
// CPU, and a response line may carry up to maxLineLength digits
|
|
1974
|
-
test.equal(tools.parseBigIntValue('9'.repeat(19)), BigInt('9'.repeat(19)), '63-bit values still fit the default bound');
|
|
1975
|
-
test.equal(tools.parseBigIntValue('9'.repeat(20)), false);
|
|
1976
|
-
test.equal(tools.parseBigIntValue('9'.repeat(400000)), false);
|
|
1977
|
-
test.equal(tools.parseBigIntValue('12345678901', tools.MAX_UINT32_DIGITS), false, 'a tighter bound can be requested');
|
|
1978
|
-
test.done();
|
|
1979
|
-
};
|
|
1980
|
-
|
|
1981
|
-
module.exports['Tools: parseUintValue rejects values outside the safe integer range'] = test => {
|
|
1982
|
-
test.equal(tools.parseUintValue('1000'), 1000);
|
|
1983
|
-
test.equal(tools.parseUintValue('0'), 0);
|
|
1984
|
-
test.equal(tools.parseUintValue('9'.repeat(19)), false, 'a value past 2^53-1 is refused, not rounded');
|
|
1985
|
-
test.equal(tools.parseUintValue('9'.repeat(400)), false, 'a digit run that would coerce to Infinity is refused');
|
|
1986
|
-
for (let bad of ['1e3', '0x10', ' 12 ', '-5', '1.5', '']) {
|
|
1987
|
-
test.equal(tools.parseUintValue(bad), false, `${JSON.stringify(bad)} must be rejected`);
|
|
1988
|
-
}
|
|
1989
|
-
test.done();
|
|
1990
|
-
};
|
|
1991
|
-
|
|
1992
|
-
module.exports['Tools: expandRange caps the total expansion, not just each range'] = test => {
|
|
1993
|
-
// A per-range bound alone is multiplied by an unbounded number of ranges. With the budget
|
|
1994
|
-
// already spent inside the second range, the two after it must be abandoned rather than
|
|
1995
|
-
// expanded on top of it.
|
|
1996
|
-
let result = tools.expandRange('1:16777215,16777215:16777220,1:5,7');
|
|
1997
|
-
test.equal(result.length, tools.EXPANDED_RANGE_LIMIT);
|
|
1998
|
-
test.equal(result[0], 1);
|
|
1999
|
-
// the second range gets the single remaining entry, and the two after it never start
|
|
2000
|
-
test.equal(result[result.length - 1], 16777215);
|
|
2001
|
-
test.done();
|
|
2002
|
-
};
|
|
2003
|
-
|
|
2004
|
-
module.exports['Tools: formatMessageResponse drops an unusable sequence number'] = async test => {
|
|
2005
|
-
// Number('9'.repeat(400)) is Infinity, and a seq of Infinity indexes into nothing
|
|
2006
|
-
let untagged = await parser('* 1 FETCH (UID 5)');
|
|
2007
|
-
let ok = await tools.formatMessageResponse(untagged, { path: 'INBOX' });
|
|
2008
|
-
test.equal(ok.seq, 1);
|
|
2009
|
-
|
|
2010
|
-
let overflowing = await tools.formatMessageResponse({ command: '9'.repeat(400), attributes: [] }, { path: 'INBOX' });
|
|
2011
|
-
test.equal(overflowing.seq, undefined);
|
|
2012
|
-
test.done();
|
|
2013
|
-
};
|