imapflow 1.7.8 → 2.0.1
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 +27 -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 +520 -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 +3956 -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} +387 -517
- 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 +60 -123
- 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 +769 -1790
- 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/special-use-test.js
DELETED
|
@@ -1,418 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const specialUse = require('../lib/special-use');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
|
|
6
|
-
module.exports['Special Use: flags array'] = test => {
|
|
7
|
-
test.ok(Array.isArray(specialUse.flags));
|
|
8
|
-
test.ok(specialUse.flags.includes('\\Sent'));
|
|
9
|
-
test.ok(specialUse.flags.includes('\\Drafts'));
|
|
10
|
-
test.ok(specialUse.flags.includes('\\Trash'));
|
|
11
|
-
test.ok(specialUse.flags.includes('\\Archive'));
|
|
12
|
-
test.done();
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
module.exports['Special Use: names object'] = test => {
|
|
16
|
-
test.ok(typeof specialUse.names === 'object');
|
|
17
|
-
test.ok(specialUse.names['\\Sent']);
|
|
18
|
-
test.ok(Array.isArray(specialUse.names['\\Sent']));
|
|
19
|
-
test.ok(specialUse.names['\\Sent'].includes('sent'));
|
|
20
|
-
test.done();
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
module.exports['Special Use: Sent folder names'] = test => {
|
|
24
|
-
let sentNames = specialUse.names['\\Sent'];
|
|
25
|
-
test.ok(sentNames.includes('sent'));
|
|
26
|
-
test.ok(sentNames.includes('sent items'));
|
|
27
|
-
test.ok(sentNames.includes('sent messages'));
|
|
28
|
-
test.done();
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
module.exports['Special Use: Drafts folder names'] = test => {
|
|
32
|
-
let draftsNames = specialUse.names['\\Drafts'];
|
|
33
|
-
test.ok(draftsNames.includes('drafts'));
|
|
34
|
-
test.done();
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
module.exports['Special Use: Trash folder names'] = test => {
|
|
38
|
-
let trashNames = specialUse.names['\\Trash'];
|
|
39
|
-
test.ok(trashNames.includes('trash'));
|
|
40
|
-
test.ok(trashNames.includes('deleted items'));
|
|
41
|
-
test.ok(trashNames.includes('deleted messages'));
|
|
42
|
-
test.done();
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
module.exports['Special Use: Junk folder names'] = test => {
|
|
46
|
-
let junkNames = specialUse.names['\\Junk'];
|
|
47
|
-
test.ok(junkNames.includes('spam'));
|
|
48
|
-
test.ok(junkNames.includes('junk'));
|
|
49
|
-
test.done();
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
// ============================================
|
|
53
|
-
// specialUse() function branch tests
|
|
54
|
-
// ============================================
|
|
55
|
-
|
|
56
|
-
module.exports['Special Use: specialUse returns extension flag when extension enabled and flag found'] = test => {
|
|
57
|
-
const result = specialUse.specialUse(true, { flags: new Set(['\\Sent']), name: 'Foo' });
|
|
58
|
-
test.equal(result.flag, '\\Sent');
|
|
59
|
-
test.equal(result.source, 'extension');
|
|
60
|
-
test.done();
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
module.exports['Special Use: specialUse falls back to name when extension enabled but no flag'] = test => {
|
|
64
|
-
const result = specialUse.specialUse(true, { flags: new Set(), name: 'Sent' });
|
|
65
|
-
test.equal(result.flag, '\\Sent');
|
|
66
|
-
test.equal(result.source, 'name');
|
|
67
|
-
test.done();
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
module.exports['Special Use: specialUse matches by name when extension disabled'] = test => {
|
|
71
|
-
const result = specialUse.specialUse(false, { flags: new Set(), name: 'Drafts' });
|
|
72
|
-
test.equal(result.flag, '\\Drafts');
|
|
73
|
-
test.equal(result.source, 'name');
|
|
74
|
-
test.done();
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
module.exports['Special Use: specialUse returns null flag when no match'] = test => {
|
|
78
|
-
const result = specialUse.specialUse(false, { flags: new Set(), name: 'CustomFolder' });
|
|
79
|
-
test.equal(result.flag, null);
|
|
80
|
-
test.equal(result.source, undefined);
|
|
81
|
-
test.done();
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
// ============================================
|
|
85
|
-
// Localized folder name detection
|
|
86
|
-
// ============================================
|
|
87
|
-
|
|
88
|
-
// Helper: resolve a folder name the way a server without SPECIAL-USE would be handled.
|
|
89
|
-
const byName = name => specialUse.specialUse(false, { flags: new Set(), name }).flag;
|
|
90
|
-
|
|
91
|
-
// Regression: Exchange/Outlook does not advertise SPECIAL-USE, so a Russian-locale
|
|
92
|
-
// mailbox has to resolve purely by folder name. Junk and Archive used to fall through.
|
|
93
|
-
module.exports['Special Use: Russian Outlook mailbox resolves every special-use folder'] = test => {
|
|
94
|
-
test.equal(byName('Отправленные'), '\\Sent');
|
|
95
|
-
test.equal(byName('Черновики'), '\\Drafts');
|
|
96
|
-
test.equal(byName('Удаленные'), '\\Trash');
|
|
97
|
-
test.equal(byName('Нежелательная почта'), '\\Junk');
|
|
98
|
-
test.equal(byName('Архив'), '\\Archive');
|
|
99
|
-
test.done();
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// Non-mail folders exposed over IMAP by Exchange must stay unclassified.
|
|
103
|
-
module.exports['Special Use: Russian calendar and contacts folders stay unmatched'] = test => {
|
|
104
|
-
test.equal(byName('Календарь'), null);
|
|
105
|
-
test.equal(byName('Контакты'), null);
|
|
106
|
-
test.done();
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
// Russian webmail (Yandex, Mail.ru, Rambler) uses different words than Outlook.
|
|
110
|
-
module.exports['Special Use: Russian webmail folder names'] = test => {
|
|
111
|
-
test.equal(byName('Корзина'), '\\Trash');
|
|
112
|
-
test.equal(byName('Удалённые'), '\\Trash'); // spelled with a real "ё"
|
|
113
|
-
test.equal(byName('Спам'), '\\Junk');
|
|
114
|
-
test.done();
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
module.exports['Special Use: Archive folder names across locales'] = test => {
|
|
118
|
-
test.equal(byName('Archive'), '\\Archive');
|
|
119
|
-
test.equal(byName('Архив'), '\\Archive');
|
|
120
|
-
test.equal(byName('Архів'), '\\Archive');
|
|
121
|
-
test.equal(byName('Archiv'), '\\Archive');
|
|
122
|
-
test.equal(byName('Archivo'), '\\Archive');
|
|
123
|
-
test.equal(byName('Arquivo'), '\\Archive');
|
|
124
|
-
test.equal(byName('Archivio'), '\\Archive');
|
|
125
|
-
test.equal(byName('Archief'), '\\Archive');
|
|
126
|
-
test.equal(byName('Arkiv'), '\\Archive');
|
|
127
|
-
test.equal(byName('Arşiv'), '\\Archive');
|
|
128
|
-
test.equal(byName('アーカイブ'), '\\Archive');
|
|
129
|
-
test.equal(byName('보관함'), '\\Archive');
|
|
130
|
-
test.done();
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
module.exports['Special Use: Junk folder names across locales'] = test => {
|
|
134
|
-
test.equal(byName('Junk Email'), '\\Junk');
|
|
135
|
-
test.equal(byName('Нежелательная почта'), '\\Junk');
|
|
136
|
-
test.equal(byName('Небажана пошта'), '\\Junk');
|
|
137
|
-
test.equal(byName('Ongewenste e-mail'), '\\Junk');
|
|
138
|
-
test.equal(byName('Lixo Eletrônico'), '\\Junk');
|
|
139
|
-
test.equal(byName('Neželjena pošta'), '\\Junk');
|
|
140
|
-
test.equal(byName('迷惑メール'), '\\Junk');
|
|
141
|
-
test.equal(byName('정크 메일'), '\\Junk');
|
|
142
|
-
test.done();
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
module.exports['Special Use: Trash folder names across locales'] = test => {
|
|
146
|
-
test.equal(byName('Deleted Items'), '\\Trash');
|
|
147
|
-
test.equal(byName('Corbeille'), '\\Trash');
|
|
148
|
-
test.equal(byName('Papierkorb'), '\\Trash');
|
|
149
|
-
test.equal(byName('Papelera'), '\\Trash');
|
|
150
|
-
test.equal(byName('Cestino'), '\\Trash');
|
|
151
|
-
test.equal(byName('Kosz'), '\\Trash');
|
|
152
|
-
test.equal(byName('ゴミ箱'), '\\Trash');
|
|
153
|
-
test.equal(byName('휴지통'), '\\Trash');
|
|
154
|
-
test.done();
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
// Matching lowercases the folder name, so non-ASCII scripts must fold too.
|
|
158
|
-
module.exports['Special Use: name matching is case insensitive for non-ASCII names'] = test => {
|
|
159
|
-
test.equal(byName('НЕЖЕЛАТЕЛЬНАЯ ПОЧТА'), '\\Junk');
|
|
160
|
-
test.equal(byName('АРХИВ'), '\\Archive');
|
|
161
|
-
test.equal(byName('Корзина'), '\\Trash');
|
|
162
|
-
test.done();
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
// Surrounding whitespace and the LTR mark injected by some clients are stripped.
|
|
166
|
-
module.exports['Special Use: strips whitespace and LTR marks before matching'] = test => {
|
|
167
|
-
test.equal(byName(' Архив '), '\\Archive');
|
|
168
|
-
test.equal(byName('\u200eНежелательная почта'), '\\Junk');
|
|
169
|
-
test.done();
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
// Exchange and Outlook use a two word naming style ("Sent Items") that differs from
|
|
173
|
-
// the one word style most webmail uses ("Sent"), and they never advertise SPECIAL-USE,
|
|
174
|
-
// so these names have to resolve by name alone.
|
|
175
|
-
module.exports['Special Use: Exchange and Outlook localized folder names'] = test => {
|
|
176
|
-
test.equal(byName('Sent Items'), '\\Sent');
|
|
177
|
-
test.equal(byName('Deleted Items'), '\\Trash');
|
|
178
|
-
test.equal(byName('Junk Email'), '\\Junk');
|
|
179
|
-
test.equal(byName('Gesendete Elemente'), '\\Sent');
|
|
180
|
-
test.equal(byName('Gelöschte Elemente'), '\\Trash');
|
|
181
|
-
test.equal(byName('Éléments envoyés'), '\\Sent');
|
|
182
|
-
test.equal(byName('Elementos eliminados'), '\\Trash');
|
|
183
|
-
test.equal(byName('Posta eliminata'), '\\Trash');
|
|
184
|
-
test.equal(byName('Verzonden items'), '\\Sent');
|
|
185
|
-
test.equal(byName('Elementy usunięte'), '\\Trash');
|
|
186
|
-
test.equal(byName('Elemente șterse'), '\\Trash');
|
|
187
|
-
test.equal(byName('Sendte elementer'), '\\Sent');
|
|
188
|
-
test.equal(byName('Gönderilmiş Öğeler'), '\\Sent');
|
|
189
|
-
test.equal(byName('Önemsiz E-posta'), '\\Junk');
|
|
190
|
-
test.equal(byName('送信済みアイテム'), '\\Sent');
|
|
191
|
-
test.equal(byName('削除済みアイテム'), '\\Trash');
|
|
192
|
-
test.equal(byName('보낸 편지함'), '\\Sent');
|
|
193
|
-
test.done();
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
// Names harvested from the localization catalogs of Roundcube, SOGo and Thunderbird.
|
|
197
|
-
// These are the cases that motivated, and are now covered without, approximate matching.
|
|
198
|
-
module.exports['Special Use: names taken from mail client localization catalogs'] = test => {
|
|
199
|
-
test.equal(byName('Gesendet'), '\\Sent'); // de, Thunderbird
|
|
200
|
-
test.equal(byName('Skickat'), '\\Sent'); // sv
|
|
201
|
-
test.equal(byName('Gelöscht'), '\\Trash'); // de, Roundcube
|
|
202
|
-
test.equal(byName('Poubelle'), '\\Trash'); // fr
|
|
203
|
-
test.equal(byName('Paperera'), '\\Trash'); // ca
|
|
204
|
-
test.equal(byName('Rämps'), '\\Junk'); // et
|
|
205
|
-
test.equal(byName('Skräp'), '\\Junk'); // sv
|
|
206
|
-
test.equal(byName('Ongewenst'), '\\Junk'); // nl
|
|
207
|
-
test.equal(byName('Basura'), '\\Junk'); // es
|
|
208
|
-
test.equal(byName('Arkisto'), '\\Archive'); // fi
|
|
209
|
-
test.done();
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
// ============================================
|
|
213
|
-
// Relaxed matching (decorated names and morphological variants)
|
|
214
|
-
// ============================================
|
|
215
|
-
|
|
216
|
-
const sourceOf = name => specialUse.specialUse(false, { flags: new Set(), name }).source;
|
|
217
|
-
|
|
218
|
-
// Approximate matching is deliberately NOT done. A shared prefix is not enough
|
|
219
|
-
// evidence: at five characters English "conceal" reaches Dutch "concepten" and
|
|
220
|
-
// "article" reaches Romanian "articole", and a wrongly claimed Trash or Junk folder
|
|
221
|
-
// makes a client delete into, or permanently expunge from, an ordinary folder.
|
|
222
|
-
// Morphological variants belong in the tables as real entries instead.
|
|
223
|
-
module.exports['Special Use: morphological variants are not guessed'] = test => {
|
|
224
|
-
test.equal(byName('Prügikast'), '\\Trash'); // exact table entry
|
|
225
|
-
test.equal(byName('Prügi'), null); // truncation, not matched
|
|
226
|
-
test.equal(byName('Prügikorv'), null); // different ending, not matched
|
|
227
|
-
test.equal(byName('Conceal'), null);
|
|
228
|
-
test.equal(byName('Article'), null);
|
|
229
|
-
test.equal(byName('Postal'), null);
|
|
230
|
-
test.equal(byName('Element'), null);
|
|
231
|
-
test.done();
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
// Known name decorated with a generic mail noun that carries no meaning of its own.
|
|
235
|
-
module.exports['Special Use: names decorated with generic mail nouns'] = test => {
|
|
236
|
-
test.equal(byName('Sent Mail'), '\\Sent');
|
|
237
|
-
test.equal(byName('Отправленные письма'), '\\Sent');
|
|
238
|
-
test.equal(byName('Удаленные элементы'), '\\Trash');
|
|
239
|
-
test.equal(byName('Deleted Mail'), '\\Trash');
|
|
240
|
-
test.equal(byName('Spam Messages'), '\\Junk');
|
|
241
|
-
test.equal(byName('Черновики письма'), '\\Drafts');
|
|
242
|
-
test.equal(byName('My Drafts'), '\\Drafts');
|
|
243
|
-
test.equal(byName('Saadetud e-kirjad'), '\\Sent'); // hyphenated "e-" family
|
|
244
|
-
test.done();
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
// Relaxed hits are a guess and must be reported as a distinct, lower priority source
|
|
248
|
-
// so that an exactly named folder wins the slot when both exist in one mailbox.
|
|
249
|
-
module.exports['Special Use: relaxed matches report a distinct source'] = test => {
|
|
250
|
-
test.equal(sourceOf('Prügikast'), 'name');
|
|
251
|
-
test.equal(sourceOf('Sent'), 'name');
|
|
252
|
-
test.equal(sourceOf('Отправленные письма'), 'name-guess');
|
|
253
|
-
test.equal(sourceOf('Sent Mail'), 'name-guess');
|
|
254
|
-
test.done();
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
// The precision guards. A wrongly flagged Trash or Junk folder is destructive, so
|
|
258
|
-
// anything that leaves more than one meaningful word behind must be refused.
|
|
259
|
-
module.exports['Special Use: user folders that merely contain a known word are refused'] = test => {
|
|
260
|
-
test.equal(byName('Sent to clients'), null);
|
|
261
|
-
test.equal(byName('Archive 2023'), null);
|
|
262
|
-
test.equal(byName('Junk food recipes'), null);
|
|
263
|
-
test.equal(byName('Drafts of my novel'), null);
|
|
264
|
-
test.equal(byName('Trash talk'), null);
|
|
265
|
-
test.equal(byName('Spam reports'), null);
|
|
266
|
-
test.equal(byName('Deleted scenes'), null);
|
|
267
|
-
test.equal(byName('Corbeilles de fruits'), null);
|
|
268
|
-
test.done();
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
// Ordinary words that share a prefix with an entry must never be classified.
|
|
272
|
-
module.exports['Special Use: words sharing only a prefix do not match'] = test => {
|
|
273
|
-
test.equal(byName('Draftsman'), null);
|
|
274
|
-
test.equal(byName('Sentinel'), null);
|
|
275
|
-
test.equal(byName('Sentiments'), null);
|
|
276
|
-
test.equal(byName('Junkyard'), null);
|
|
277
|
-
test.equal(byName('Spammers'), null);
|
|
278
|
-
test.equal(byName('Binder'), null);
|
|
279
|
-
test.equal(byName('Postbox'), null);
|
|
280
|
-
test.equal(byName('Papers'), null);
|
|
281
|
-
test.done();
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
// Every source specialUse() can return has to be ranked by the conflict resolution in
|
|
285
|
-
// lib/commands/list.js. A new tier added here without being added there would silently
|
|
286
|
-
// sort ahead of an explicit user hint, so pin the vocabulary from this side.
|
|
287
|
-
module.exports['Special Use: reports only sources that list.js ranks'] = test => {
|
|
288
|
-
const RANKED = ['user', 'extension', 'name', 'name-guess'];
|
|
289
|
-
const listSource = fs.readFileSync(`${__dirname}/../lib/commands/list.js`, 'utf8');
|
|
290
|
-
const declared = listSource.match(/const SOURCE_SORT_ORDER = \[([^\]]*)\]/);
|
|
291
|
-
|
|
292
|
-
test.ok(declared, 'SOURCE_SORT_ORDER not found in lib/commands/list.js');
|
|
293
|
-
test.deepEqual(
|
|
294
|
-
declared[1]
|
|
295
|
-
.split(',')
|
|
296
|
-
.map(part => part.trim().replace(/^'|'$/g, ''))
|
|
297
|
-
.filter(Boolean),
|
|
298
|
-
RANKED
|
|
299
|
-
);
|
|
300
|
-
|
|
301
|
-
// and every source this module actually emits is one of them
|
|
302
|
-
const emitted = new Set();
|
|
303
|
-
emitted.add(specialUse.specialUse(true, { flags: new Set(['\\Sent']), name: 'x' }).source);
|
|
304
|
-
emitted.add(specialUse.specialUse(false, { flags: new Set(), name: 'Sent' }).source);
|
|
305
|
-
emitted.add(specialUse.specialUse(false, { flags: new Set(), name: 'Sent Mail' }).source);
|
|
306
|
-
for (let source of emitted) {
|
|
307
|
-
test.ok(RANKED.includes(source), `unranked source: ${source}`);
|
|
308
|
-
}
|
|
309
|
-
test.done();
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
// ============================================
|
|
313
|
-
// Structural guards for the name tables
|
|
314
|
-
// ============================================
|
|
315
|
-
|
|
316
|
-
// A name listed under two different flags would make detection depend on key order.
|
|
317
|
-
module.exports['Special Use: no folder name is claimed by two flags'] = test => {
|
|
318
|
-
let owner = new Map();
|
|
319
|
-
let collisions = [];
|
|
320
|
-
for (let flag of Object.keys(specialUse.names)) {
|
|
321
|
-
for (let name of specialUse.names[flag]) {
|
|
322
|
-
if (owner.has(name) && owner.get(name) !== flag) {
|
|
323
|
-
collisions.push(`${name}: ${owner.get(name)} vs ${flag}`);
|
|
324
|
-
}
|
|
325
|
-
owner.set(name, flag);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
test.deepEqual(collisions, []);
|
|
329
|
-
test.done();
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
// Lookups normalize the incoming name to NFKC, so an entry stored in any other
|
|
333
|
-
// normalization form can never match, however the server spells the folder.
|
|
334
|
-
module.exports['Special Use: every name entry is stored in NFKC form'] = test => {
|
|
335
|
-
let problems = [];
|
|
336
|
-
for (let flag of Object.keys(specialUse.names)) {
|
|
337
|
-
for (let name of specialUse.names[flag]) {
|
|
338
|
-
if (name !== name.normalize('NFKC')) {
|
|
339
|
-
problems.push(`${flag}: not NFKC: ${name}`);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
test.deepEqual(problems, []);
|
|
344
|
-
test.done();
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
// Servers echo back whatever normalization form the creating client used, so both
|
|
348
|
-
// canonically equivalent spellings of a name must resolve to the same flag.
|
|
349
|
-
module.exports['Special Use: matching is independent of Unicode normalization form'] = test => {
|
|
350
|
-
let mismatches = [];
|
|
351
|
-
for (let flag of Object.keys(specialUse.names)) {
|
|
352
|
-
for (let name of specialUse.names[flag]) {
|
|
353
|
-
for (let form of ['NFC', 'NFD', 'NFKC', 'NFKD']) {
|
|
354
|
-
let resolved = specialUse.specialUse(false, { flags: new Set(), name: name.normalize(form) }).flag;
|
|
355
|
-
if (resolved !== flag) {
|
|
356
|
-
mismatches.push(`${flag}: "${name}" as ${form} resolved to ${resolved}`);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
test.deepEqual(mismatches, []);
|
|
362
|
-
test.done();
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
// Compatibility folding is the reason lookups normalize with NFKC rather than NFC.
|
|
366
|
-
// The Japanese Sent entry was stored with halfwidth katakana, which NFC does not
|
|
367
|
-
// fold, so it never matched the fullwidth spelling that servers actually send.
|
|
368
|
-
module.exports['Special Use: halfwidth and fullwidth forms fold onto the same entry'] = test => {
|
|
369
|
-
// U+FF92 U+FF70 U+FF99 halfwidth vs U+30E1 U+30FC U+30EB fullwidth katakana
|
|
370
|
-
const halfwidth = '送信済み' + String.fromCodePoint(0xff92, 0xff70, 0xff99);
|
|
371
|
-
const fullwidth = '送信済み' + String.fromCodePoint(0x30e1, 0x30fc, 0x30eb);
|
|
372
|
-
|
|
373
|
-
test.equal(byName(halfwidth), '\\Sent');
|
|
374
|
-
test.equal(byName(fullwidth), '\\Sent');
|
|
375
|
-
// Fullwidth Latin, which some CJK clients use for ASCII folder names
|
|
376
|
-
test.equal(byName('Sent'), '\\Sent');
|
|
377
|
-
test.equal(byName('Trash'), '\\Trash');
|
|
378
|
-
test.done();
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
// Devanagari and Bengali nukta letters are Unicode composition exclusions: the
|
|
382
|
-
// precomposed character is NOT the NFC form, so these two entries used to match
|
|
383
|
-
// only if the server happened to send the precomposed spelling.
|
|
384
|
-
module.exports['Special Use: nukta drafts folders match in either spelling'] = test => {
|
|
385
|
-
// U+095E DEVANAGARI LETTER PHA WITH NUKTA vs U+092B U+093C
|
|
386
|
-
const hindi = String.fromCodePoint(0x0921, 0x094d, 0x0930, 0x093e, 0x095e, 0x094d, 0x091f);
|
|
387
|
-
// U+09DC BENGALI LETTER RRA vs U+09A1 U+09BC
|
|
388
|
-
const bengali = String.fromCodePoint(0x0996, 0x09b8, 0x09dc, 0x09be);
|
|
389
|
-
|
|
390
|
-
test.equal(byName(hindi), '\\Drafts');
|
|
391
|
-
test.equal(byName(hindi.normalize('NFC')), '\\Drafts');
|
|
392
|
-
test.equal(byName(bengali), '\\Drafts');
|
|
393
|
-
test.equal(byName(bengali.normalize('NFC')), '\\Drafts');
|
|
394
|
-
test.done();
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
// Lookups compare against a lowercased and trimmed name, so entries stored in any
|
|
398
|
-
// other form are dead weight that can never match.
|
|
399
|
-
module.exports['Special Use: every name entry is lowercase, trimmed and unique'] = test => {
|
|
400
|
-
let problems = [];
|
|
401
|
-
for (let flag of Object.keys(specialUse.names)) {
|
|
402
|
-
let seen = new Set();
|
|
403
|
-
for (let name of specialUse.names[flag]) {
|
|
404
|
-
if (name !== name.toLowerCase()) {
|
|
405
|
-
problems.push(`${flag}: not lowercase: ${name}`);
|
|
406
|
-
}
|
|
407
|
-
if (name !== name.trim()) {
|
|
408
|
-
problems.push(`${flag}: not trimmed: ${JSON.stringify(name)}`);
|
|
409
|
-
}
|
|
410
|
-
if (seen.has(name)) {
|
|
411
|
-
problems.push(`${flag}: duplicate: ${name}`);
|
|
412
|
-
}
|
|
413
|
-
seen.add(name);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
test.deepEqual(problems, []);
|
|
417
|
-
test.done();
|
|
418
|
-
};
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const net = require('net');
|
|
4
|
-
const { ImapFlow } = require('../lib/imap-flow');
|
|
5
|
-
const { ImapStream } = require('../lib/handler/imap-stream');
|
|
6
|
-
|
|
7
|
-
// Minimal pre-STARTTLS IMAP server. Sends the greeting, advertises STARTTLS, and on the
|
|
8
|
-
// STARTTLS command invokes `onStartTls(socket, tag)` so each test controls what (if
|
|
9
|
-
// anything) is written after the tagged OK. It never performs a real TLS handshake.
|
|
10
|
-
const createStartTlsServer = onStartTls =>
|
|
11
|
-
net.createServer(socket => {
|
|
12
|
-
socket.on('error', () => {});
|
|
13
|
-
socket.write('* OK [CAPABILITY IMAP4rev1 STARTTLS LOGINDISABLED] ready\r\n');
|
|
14
|
-
|
|
15
|
-
let buf = '';
|
|
16
|
-
socket.on('data', data => {
|
|
17
|
-
buf += data.toString('binary');
|
|
18
|
-
let idx;
|
|
19
|
-
while ((idx = buf.indexOf('\r\n')) >= 0) {
|
|
20
|
-
let line = buf.slice(0, idx);
|
|
21
|
-
buf = buf.slice(idx + 2);
|
|
22
|
-
let parts = line.split(' ');
|
|
23
|
-
let tag = parts[0];
|
|
24
|
-
let command = (parts[1] || '').toUpperCase();
|
|
25
|
-
if (command === 'CAPABILITY') {
|
|
26
|
-
socket.write('* CAPABILITY IMAP4rev1 STARTTLS LOGINDISABLED\r\n');
|
|
27
|
-
socket.write(`${tag} OK CAPABILITY done\r\n`);
|
|
28
|
-
} else if (command === 'STARTTLS') {
|
|
29
|
-
onStartTls(socket, tag);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const makeClient = port =>
|
|
36
|
-
new ImapFlow({
|
|
37
|
-
host: '127.0.0.1',
|
|
38
|
-
port,
|
|
39
|
-
secure: false,
|
|
40
|
-
doSTARTTLS: true,
|
|
41
|
-
servername: 'localhost',
|
|
42
|
-
tls: { rejectUnauthorized: false },
|
|
43
|
-
disableAutoIdle: true,
|
|
44
|
-
logger: false,
|
|
45
|
-
auth: { user: 'test', pass: 'test' }
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// A MITM injects a response in the SAME segment as the STARTTLS OK, before the handshake.
|
|
49
|
-
// This is caught by the parser-level trailing-data flag (Check 1 in upgradeToSTARTTLS).
|
|
50
|
-
// Injection that instead arrives after the OK in a separate segment is caught by the
|
|
51
|
-
// socket-level read after unpipe (Check 2) or, failing that, by TLS handshake corruption;
|
|
52
|
-
// those fragmented cases are timing-dependent and not asserted deterministically here.
|
|
53
|
-
module.exports['STARTTLS: rejects same-segment plaintext injection'] = async test => {
|
|
54
|
-
let server = createStartTlsServer((socket, tag) => {
|
|
55
|
-
// OK and an injected untagged CAPABILITY in a single write, no TLS handshake.
|
|
56
|
-
socket.write(`${tag} OK Begin TLS\r\n* CAPABILITY IMAP4rev1 INJECTED\r\n`);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
60
|
-
let port = server.address().port;
|
|
61
|
-
|
|
62
|
-
let client = makeClient(port);
|
|
63
|
-
client.on('error', () => {});
|
|
64
|
-
|
|
65
|
-
let connectErr = null;
|
|
66
|
-
try {
|
|
67
|
-
await client.connect();
|
|
68
|
-
test.ok(false, 'connect() must reject when data is injected after the STARTTLS OK');
|
|
69
|
-
} catch (err) {
|
|
70
|
-
connectErr = err;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
test.ok(connectErr, 'connect() rejected');
|
|
74
|
-
test.equal(connectErr.code, 'STARTTLS_INJECTION', 'rejection is flagged as an injection');
|
|
75
|
-
test.ok(connectErr.tlsFailed, 'rejection is treated as a TLS failure');
|
|
76
|
-
// The connection must fail closed: it never becomes usable and never authenticates.
|
|
77
|
-
// (The injected untagged response may be parsed transiently before teardown, so asserting
|
|
78
|
-
// it never reaches the capability set would be a timing-fragile false assurance.)
|
|
79
|
-
test.ok(!client.usable, 'the connection did not become usable (failed closed)');
|
|
80
|
-
test.ok(!client.authenticated, 'the connection never authenticated');
|
|
81
|
-
|
|
82
|
-
client.close();
|
|
83
|
-
server.close();
|
|
84
|
-
test.done();
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
// A compliant server stays silent after the STARTTLS OK. The injection guard must NOT
|
|
88
|
-
// fire (no false positive); the upgrade proceeds to the TLS handshake, which then fails
|
|
89
|
-
// here only because this stub server never speaks TLS — proving the guard let it through.
|
|
90
|
-
module.exports['STARTTLS: clean OK is not flagged as injection'] = async test => {
|
|
91
|
-
let server = createStartTlsServer((socket, tag) => {
|
|
92
|
-
// Only the tagged OK, nothing after it, then drop the connection.
|
|
93
|
-
socket.write(`${tag} OK Begin TLS\r\n`);
|
|
94
|
-
setImmediate(() => socket.destroy());
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
98
|
-
let port = server.address().port;
|
|
99
|
-
|
|
100
|
-
let client = makeClient(port);
|
|
101
|
-
client.on('error', () => {});
|
|
102
|
-
|
|
103
|
-
let connectErr = null;
|
|
104
|
-
try {
|
|
105
|
-
await client.connect();
|
|
106
|
-
test.ok(false, 'connect() rejects because the stub never completes the TLS handshake');
|
|
107
|
-
} catch (err) {
|
|
108
|
-
connectErr = err;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
test.ok(connectErr, 'connect() rejected (no TLS handshake on the stub server)');
|
|
112
|
-
test.notEqual(connectErr.code, 'STARTTLS_INJECTION', 'a clean OK must not be flagged as injection');
|
|
113
|
-
|
|
114
|
-
client.close();
|
|
115
|
-
server.close();
|
|
116
|
-
test.done();
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
// A STARTTLS handshake failure must reject connect() through a single error path: it must
|
|
120
|
-
// not also emit an 'error' event (which would double-report and crash a listener-less client).
|
|
121
|
-
module.exports['STARTTLS: handshake failure has a single error path'] = async test => {
|
|
122
|
-
let server = createStartTlsServer((socket, tag) => {
|
|
123
|
-
// Acknowledge STARTTLS, then drop the connection instead of doing TLS.
|
|
124
|
-
socket.write(`${tag} OK Begin TLS\r\n`);
|
|
125
|
-
setImmediate(() => socket.destroy());
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
129
|
-
let port = server.address().port;
|
|
130
|
-
|
|
131
|
-
let client = makeClient(port);
|
|
132
|
-
|
|
133
|
-
// A second error path would emit here (and crash a listener-less client).
|
|
134
|
-
let errorEvents = 0;
|
|
135
|
-
client.on('error', () => {
|
|
136
|
-
errorEvents++;
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
let connectErr = null;
|
|
140
|
-
try {
|
|
141
|
-
await client.connect();
|
|
142
|
-
test.ok(false, 'connect() should reject on STARTTLS handshake failure');
|
|
143
|
-
} catch (err) {
|
|
144
|
-
connectErr = err;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Give any stray async error handler a chance to (wrongly) fire.
|
|
148
|
-
await new Promise(r => setTimeout(r, 50));
|
|
149
|
-
|
|
150
|
-
test.ok(connectErr, 'connect() rejected');
|
|
151
|
-
test.ok(connectErr.tlsFailed, 'rejection is flagged as a TLS failure');
|
|
152
|
-
test.equal(errorEvents, 0, 'no duplicate error event emitted (single error path)');
|
|
153
|
-
|
|
154
|
-
client.close();
|
|
155
|
-
server.close();
|
|
156
|
-
test.done();
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
// Unit-level check of the per-command trailing flag the guard relies on: each pushed
|
|
160
|
-
// command records whether more input followed it, independently of later commands.
|
|
161
|
-
module.exports['STARTTLS: parser flags trailing data per command'] = test => {
|
|
162
|
-
const stream = new ImapStream({ cid: 'test' });
|
|
163
|
-
let flags = [];
|
|
164
|
-
|
|
165
|
-
stream.on('readable', () => {
|
|
166
|
-
let cmd;
|
|
167
|
-
while ((cmd = stream.read()) !== null) {
|
|
168
|
-
flags.push(cmd.trailingAfterLine);
|
|
169
|
-
cmd.next();
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
stream.on('error', err => test.ifError(err));
|
|
173
|
-
stream.on('end', () => {
|
|
174
|
-
// First command had a second command after it -> true; the last one -> false.
|
|
175
|
-
test.deepEqual(flags, [true, false], 'trailingAfterLine is per-command and not overwritten');
|
|
176
|
-
test.done();
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// Two complete commands in a single write.
|
|
180
|
-
stream.end(Buffer.from('A OK first\r\nB OK second\r\n'));
|
|
181
|
-
};
|