imapkit 0.0.0-stage → 4.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/LICENSE +16 -0
- package/README.md +608 -2
- package/bin/help.txt +98 -0
- package/bin/imapkit.js +108 -0
- package/cert/server.crt +20 -0
- package/cert/server.key +28 -0
- package/lib/addressparser.js +283 -0
- package/lib/arguments.js +112 -0
- package/lib/bodystructure.js +149 -0
- package/lib/command-states.js +109 -0
- package/lib/commands/append.js +313 -0
- package/lib/commands/capability.js +47 -0
- package/lib/commands/check.js +21 -0
- package/lib/commands/close.js +30 -0
- package/lib/commands/copy.js +115 -0
- package/lib/commands/create.js +52 -0
- package/lib/commands/delete.js +64 -0
- package/lib/commands/examine.js +7 -0
- package/lib/commands/expunge.js +27 -0
- package/lib/commands/fetch.js +229 -0
- package/lib/commands/handlers/fetch.js +209 -0
- package/lib/commands/handlers/flags.js +42 -0
- package/lib/commands/handlers/search.js +519 -0
- package/lib/commands/handlers/status.js +85 -0
- package/lib/commands/handlers/store.js +127 -0
- package/lib/commands/list.js +100 -0
- package/lib/commands/login.js +67 -0
- package/lib/commands/logout.js +41 -0
- package/lib/commands/lsub.js +87 -0
- package/lib/commands/noop.js +21 -0
- package/lib/commands/rename.js +102 -0
- package/lib/commands/search.js +76 -0
- package/lib/commands/select.js +289 -0
- package/lib/commands/status.js +63 -0
- package/lib/commands/store.js +151 -0
- package/lib/commands/subscribe.js +53 -0
- package/lib/commands/uid copy.js +7 -0
- package/lib/commands/uid fetch.js +5 -0
- package/lib/commands/uid search.js +5 -0
- package/lib/commands/uid store.js +5 -0
- package/lib/commands/unsubscribe.js +50 -0
- package/lib/dates.js +123 -0
- package/lib/deflate-layer.js +232 -0
- package/lib/envelope.js +82 -0
- package/lib/esearch.js +208 -0
- package/lib/framing.js +102 -0
- package/lib/list-extensions.js +36 -0
- package/lib/load-plugins.js +109 -0
- package/lib/mailbox-name.js +133 -0
- package/lib/mimeparser.js +778 -0
- package/lib/mock-client.js +233 -0
- package/lib/numbers.js +52 -0
- package/lib/plugins/acl.js +964 -0
- package/lib/plugins/appendlimit.js +83 -0
- package/lib/plugins/auth-plain.js +94 -0
- package/lib/plugins/binary.js +256 -0
- package/lib/plugins/catenate.js +253 -0
- package/lib/plugins/compress.js +76 -0
- package/lib/plugins/condstore.js +563 -0
- package/lib/plugins/context-search.js +321 -0
- package/lib/plugins/context-sort.js +19 -0
- package/lib/plugins/create-special-use.js +108 -0
- package/lib/plugins/enable.js +155 -0
- package/lib/plugins/esearch.js +156 -0
- package/lib/plugins/esort.js +60 -0
- package/lib/plugins/id.js +138 -0
- package/lib/plugins/idle.js +105 -0
- package/lib/plugins/imap4rev2.js +202 -0
- package/lib/plugins/list-extended.js +258 -0
- package/lib/plugins/list-status.js +31 -0
- package/lib/plugins/literalminus.js +20 -0
- package/lib/plugins/literalplus.js +18 -0
- package/lib/plugins/logindisabled.js +50 -0
- package/lib/plugins/messagelimit.js +234 -0
- package/lib/plugins/metadata-server.js +13 -0
- package/lib/plugins/metadata.js +475 -0
- package/lib/plugins/move.js +110 -0
- package/lib/plugins/multiappend.js +26 -0
- package/lib/plugins/multisearch.js +269 -0
- package/lib/plugins/namespace.js +67 -0
- package/lib/plugins/notify.js +654 -0
- package/lib/plugins/oauthbearer.js +217 -0
- package/lib/plugins/objectid.js +243 -0
- package/lib/plugins/partial.js +68 -0
- package/lib/plugins/preview.js +400 -0
- package/lib/plugins/qresync.js +525 -0
- package/lib/plugins/quota.js +285 -0
- package/lib/plugins/replace.js +145 -0
- package/lib/plugins/sasl-ir.js +12 -0
- package/lib/plugins/savedate.js +59 -0
- package/lib/plugins/savelimit.js +18 -0
- package/lib/plugins/searchres.js +82 -0
- package/lib/plugins/sort-display.js +23 -0
- package/lib/plugins/sort.js +132 -0
- package/lib/plugins/special-use.js +95 -0
- package/lib/plugins/starttls.js +57 -0
- package/lib/plugins/status-size.js +19 -0
- package/lib/plugins/thread-orderedsubject.js +16 -0
- package/lib/plugins/thread-references.js +16 -0
- package/lib/plugins/uidonly.js +135 -0
- package/lib/plugins/uidplus.js +124 -0
- package/lib/plugins/unauthenticate.js +28 -0
- package/lib/plugins/unselect.js +36 -0
- package/lib/plugins/utf8-accept.js +68 -0
- package/lib/plugins/x-gm-ext-1.js +456 -0
- package/lib/plugins/xoauth2.js +188 -0
- package/lib/plugins/xtoybird.js +282 -0
- package/lib/server.js +2880 -0
- package/lib/smtp-listener.js +51 -0
- package/lib/sorting.js +373 -0
- package/lib/threading.js +357 -0
- package/lib/utf8-session.js +123 -0
- package/lib/vanished.js +57 -0
- package/package.json +61 -5
package/bin/help.txt
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
ImapKit (v__VERSION__)
|
|
2
|
+
==========__VBAR__=
|
|
3
|
+
ImapKit starts an IMAP server that is suitable for running client
|
|
4
|
+
integration tests. Nothing is written to disk, all operations are
|
|
5
|
+
performed on memory and the changes are lost after restart, so the
|
|
6
|
+
initial state is always guaranteed when running your tests.
|
|
7
|
+
|
|
8
|
+
Additionally ImapKit offers an incoming SMTP server - every mail
|
|
9
|
+
sent to it, is appended to INBOX. To use the incoming SMTP server,
|
|
10
|
+
use smtpPort option.
|
|
11
|
+
|
|
12
|
+
Usage: imapkit [OPTS]
|
|
13
|
+
|
|
14
|
+
-p <port>, --port=<port> Port number to listen to, defaults to 143
|
|
15
|
+
-s, --secure=true Starts a secure server, defaults port to 993
|
|
16
|
+
-d, --debug=true Writes IMAP traffic to console
|
|
17
|
+
--storage=<path> Path to JSON file with the directory tree
|
|
18
|
+
--config=<config> Path to config JSON file
|
|
19
|
+
--plugin=<plugin> Enables a plugin for the server. See below for
|
|
20
|
+
available plugins.
|
|
21
|
+
--smtpPort=<port> Port number for incoming SMTP server. If not set
|
|
22
|
+
SMTP server is not started.
|
|
23
|
+
|
|
24
|
+
The options can also be set with environment variables: IMAPKIT_PORT,
|
|
25
|
+
IMAPKIT_SECURE, IMAPKIT_DEBUG, IMAPKIT_STORAGE, IMAPKIT_CONFIG,
|
|
26
|
+
IMAPKIT_PLUGINS (comma separated) and IMAPKIT_SMTPPORT.
|
|
27
|
+
|
|
28
|
+
NB! If port or smtpPort values are below 1024 you most probably need to
|
|
29
|
+
use sudo or run the command in Administrator rights.
|
|
30
|
+
|
|
31
|
+
Available plugins
|
|
32
|
+
-----------------
|
|
33
|
+
|
|
34
|
+
Usage: imapkit [OPTS] --plugin=ID --plugin=IDLE
|
|
35
|
+
or: imapkit [OPTS] --plugin=ID,IDLE
|
|
36
|
+
|
|
37
|
+
__PLUGINS__
|
|
38
|
+
|
|
39
|
+
Configuration files
|
|
40
|
+
-------------------
|
|
41
|
+
|
|
42
|
+
Configuration file takes the following structure
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
"plugins": ["ID", "IDLE"],
|
|
46
|
+
"debug": false,
|
|
47
|
+
"secureConnection": false,
|
|
48
|
+
"port": 143,
|
|
49
|
+
"maxLiteralSize": 67108864,
|
|
50
|
+
"appendLimit": 5242880,
|
|
51
|
+
"messageLimit": 1000,
|
|
52
|
+
"users": {
|
|
53
|
+
"testuser": {
|
|
54
|
+
"password": "testpass"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"id":{
|
|
58
|
+
"name": "hoodecrow",
|
|
59
|
+
"version": "__VERSION__",
|
|
60
|
+
"key": "value"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Storage file is a tree like structure starting with namespace values.
|
|
65
|
+
INBOX has its own namespace.
|
|
66
|
+
|
|
67
|
+
{
|
|
68
|
+
"INBOX":{
|
|
69
|
+
"uidnext": 123,
|
|
70
|
+
"messages": [
|
|
71
|
+
{"raw": "Subject: Test\r\n\r\nHello world!"}
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"":{
|
|
75
|
+
"separator": "/",
|
|
76
|
+
"folders": {
|
|
77
|
+
"Parent folder":{
|
|
78
|
+
"folders":{
|
|
79
|
+
"Subfolder": {}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"#news/":{
|
|
85
|
+
"type": "shared"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
If no storage file is specified, an empty "INBOX" is used. If no users
|
|
90
|
+
are configured, the only account is "testuser" with password "testpass".
|
|
91
|
+
The storage and users objects are copied, so the files are never changed.
|
|
92
|
+
|
|
93
|
+
Project homepage: __HOMEPAGE__
|
|
94
|
+
Report issues at: __BUGS__
|
|
95
|
+
|
|
96
|
+
License: MIT
|
|
97
|
+
|
|
98
|
+
Copyright (c) 2013-2026 Postal Systems OÜ
|
package/bin/imapkit.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { parseArgs } = require('node:util');
|
|
7
|
+
const imapkit = require('../lib/server');
|
|
8
|
+
const packageData = require('../package.json');
|
|
9
|
+
|
|
10
|
+
// Non-strict so that boolean flags also accept an explicit value (`--secure=true`)
|
|
11
|
+
const { values: argv } = parseArgs({
|
|
12
|
+
strict: false,
|
|
13
|
+
options: {
|
|
14
|
+
port: { type: 'string', short: 'p' },
|
|
15
|
+
secure: { type: 'boolean', short: 's' },
|
|
16
|
+
debug: { type: 'boolean', short: 'd' },
|
|
17
|
+
help: { type: 'boolean', short: 'h' },
|
|
18
|
+
config: { type: 'string' },
|
|
19
|
+
storage: { type: 'string' },
|
|
20
|
+
plugin: { type: 'string', multiple: true },
|
|
21
|
+
smtpPort: { type: 'string' }
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const isTrue = value => (value || '').toString().trim().toLowerCase() === 'true';
|
|
26
|
+
|
|
27
|
+
const configLocation = argv.config || process.env.IMAPKIT_CONFIG;
|
|
28
|
+
const storageLocation = argv.storage || process.env.IMAPKIT_STORAGE;
|
|
29
|
+
const smtpPort = argv.smtpPort || process.env.IMAPKIT_SMTPPORT;
|
|
30
|
+
const pluginsList = []
|
|
31
|
+
.concat(argv.plugin || process.env.IMAPKIT_PLUGINS || [])
|
|
32
|
+
.flatMap(plugin =>
|
|
33
|
+
String(plugin)
|
|
34
|
+
.toUpperCase()
|
|
35
|
+
.trim()
|
|
36
|
+
.split(/\s*,\s*/)
|
|
37
|
+
)
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
const secure = isTrue(argv.secure || process.env.IMAPKIT_SECURE);
|
|
40
|
+
const debug = isTrue(argv.debug || process.env.IMAPKIT_DEBUG);
|
|
41
|
+
|
|
42
|
+
let config = {};
|
|
43
|
+
|
|
44
|
+
if (configLocation) {
|
|
45
|
+
config = JSON.parse(fs.readFileSync(configLocation, 'utf-8'));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (storageLocation) {
|
|
49
|
+
config.storage = JSON.parse(fs.readFileSync(storageLocation, 'utf-8'));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (pluginsList.length) {
|
|
53
|
+
config.plugins = pluginsList;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (secure) {
|
|
57
|
+
config.secureConnection = true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (debug) {
|
|
61
|
+
config.debug = true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const port = argv.port || process.env.IMAPKIT_PORT || config.port || (secure ? 993 : 143);
|
|
65
|
+
|
|
66
|
+
if (argv.help) {
|
|
67
|
+
const help = fs.readFileSync(path.join(__dirname, 'help.txt'), 'utf-8');
|
|
68
|
+
const pluginsDir = path.join(__dirname, '..', 'lib', 'plugins');
|
|
69
|
+
|
|
70
|
+
const plugins = fs.readdirSync(pluginsDir).map(fileName => {
|
|
71
|
+
const file = fs.readFileSync(path.join(pluginsDir, fileName), 'utf-8');
|
|
72
|
+
return {
|
|
73
|
+
key: fileName.replace(/\.js$/i, '').toUpperCase(),
|
|
74
|
+
value: Array.from(file.matchAll(/@help (.*)/gim), match => match[1])
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const indent = Math.max(0, ...plugins.map(plugin => plugin.key.length)) + 2;
|
|
79
|
+
const pluginText = plugins.flatMap(plugin => [
|
|
80
|
+
' ' + plugin.key.padEnd(indent - 1) + (plugin.value[0] || ''),
|
|
81
|
+
...plugin.value.slice(1).map(line => ' '.repeat(indent) + line)
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
console.log(
|
|
85
|
+
help
|
|
86
|
+
.replace(/__PLUGINS__/g, pluginText.join('\n'))
|
|
87
|
+
.replace(/__VERSION__/g, packageData.version)
|
|
88
|
+
.replace(/__VBAR__/g, '='.repeat(packageData.version.length))
|
|
89
|
+
.replace(/__HOMEPAGE__/g, packageData.homepage)
|
|
90
|
+
.replace(/__BUGS__/g, packageData.bugs.url)
|
|
91
|
+
.trim()
|
|
92
|
+
);
|
|
93
|
+
} else {
|
|
94
|
+
const server = imapkit(config);
|
|
95
|
+
console.log('Starting ImapKit ...');
|
|
96
|
+
server.server.on('error', err => {
|
|
97
|
+
console.error('Failed to start ImapKit on port %s: %s', port, err.message);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
});
|
|
100
|
+
server.listen(port, () => {
|
|
101
|
+
console.log('ImapKit successfully%s listening on port %s', secure ? ' and securely' : '', port);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (smtpPort) {
|
|
105
|
+
// loaded on demand, smtp-server is only needed when SMTP is enabled
|
|
106
|
+
require('../lib/smtp-listener').startSMTPServer(smtpPort, server);
|
|
107
|
+
}
|
|
108
|
+
}
|
package/cert/server.crt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIDOjCCAiKgAwIBAgIUad1YSlfC6YFAqn38plnziyvx1/YwDQYJKoZIhvcNAQEL
|
|
3
|
+
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI2MTAwNjA4NDU1OFoYDzIxMjYw
|
|
4
|
+
OTEyMDg0NTU4WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
|
|
5
|
+
AQUAA4IBDwAwggEKAoIBAQDN0THQ8qpf3mMoNlVzVvjfsw/QHo22ojmmUO+3QjZD
|
|
6
|
+
eB7mSmniyGOFw0M8f2g3FM0Kfa51rKMYXWQVcqRTDFkk9zBnCagFIWCi2DI4VgAr
|
|
7
|
+
zWlu4TqOHfRIHcbAH1bTKVh5KnVmQA6j6leKjvSW2021Ntp8VX+KV5nXM3rRUWaP
|
|
8
|
+
USGj1+n6Pcj8ZrMdm2bd6Q4UscCvKrCi1dBPTPt9BNVO0CXL8zsiQfNOveN4nTBm
|
|
9
|
+
F4T1+P/vvzSqHr199XDkjlhd8j3Pm2LKNS5OnPERgwZhgIg15G7Y/vz9N0vZGmIU
|
|
10
|
+
7ApunyrgOhV1o4AlOzdo31xbVYIVn7/fv44DG7hvX8DVAgMBAAGjgYEwfzAdBgNV
|
|
11
|
+
HQ4EFgQUzO7Y5K1NaU+/GXZJi2C47uK0+tQwHwYDVR0jBBgwFoAUzO7Y5K1NaU+/
|
|
12
|
+
GXZJi2C47uK0+tQwDwYDVR0TAQH/BAUwAwEB/zAsBgNVHREEJTAjgglsb2NhbGhv
|
|
13
|
+
c3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggEBAL/P
|
|
14
|
+
p3lI83Fcw8EpVyzr9QPYA7JZVmy8YlJ7vouOmEUi6X7BnK5Eits1uHGZ6rtLGBCY
|
|
15
|
+
Eu4Z4DnCPXnyXuo4Bzaqabs5qUro3Ztf0FXeatGCmTLx06JSapKVyoXkJpzLzX0u
|
|
16
|
+
pj0WfWUkwD3rGQozn0FOUOrZyjYgPgsqr6doWd5emAWl1L8ZGDFJEWPRvrS0dw7Z
|
|
17
|
+
ZG83/H0UYur8lN2JxV4ITRovftmtruiVNw3mXH5BZCIsoFAEtf3hssn6rekfEOfc
|
|
18
|
+
4vty2NskN112OLQzLK1Oq3dl0C6Ns1MdIoelAHrFdeM3O03Y0D2R+PlNw1cU55if
|
|
19
|
+
EF3engvuSVRtcV8eyxM=
|
|
20
|
+
-----END CERTIFICATE-----
|
package/cert/server.key
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-----BEGIN PRIVATE KEY-----
|
|
2
|
+
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDN0THQ8qpf3mMo
|
|
3
|
+
NlVzVvjfsw/QHo22ojmmUO+3QjZDeB7mSmniyGOFw0M8f2g3FM0Kfa51rKMYXWQV
|
|
4
|
+
cqRTDFkk9zBnCagFIWCi2DI4VgArzWlu4TqOHfRIHcbAH1bTKVh5KnVmQA6j6leK
|
|
5
|
+
jvSW2021Ntp8VX+KV5nXM3rRUWaPUSGj1+n6Pcj8ZrMdm2bd6Q4UscCvKrCi1dBP
|
|
6
|
+
TPt9BNVO0CXL8zsiQfNOveN4nTBmF4T1+P/vvzSqHr199XDkjlhd8j3Pm2LKNS5O
|
|
7
|
+
nPERgwZhgIg15G7Y/vz9N0vZGmIU7ApunyrgOhV1o4AlOzdo31xbVYIVn7/fv44D
|
|
8
|
+
G7hvX8DVAgMBAAECggEABIVNSlEiM+PfGrGC0/s7kTJOLRyWsUXD7WbGtShDp9y6
|
|
9
|
+
FQXaaIxNP1FoJyqHP/p5fHsBO310waK/gYcYm+hgBuNVEVZRdfdjN4B3LeyZqVyk
|
|
10
|
+
LbbNwfRferwIswydLBsy4cpwdLQAflI15Qm5zpx3ivzu9J8uI3xEGUl8r59RmOE9
|
|
11
|
+
tXb+sA9YtWRXUR4mhqApGxMI5ZP1/WbmhlDvLUnartyBNilfS3RTWY7nWMKk3U0N
|
|
12
|
+
yFmYX17WZYnXbJtXQmOX/LHd1nM2NFS2B6yJ6JKY0/jPOGCaTBcVmFgO3x/aDgiJ
|
|
13
|
+
m0XL1mpGBIUugQTio5N1Ewznrg6SA7rzsOzN1g3ZqwKBgQD48+lGyQmFAaaXzf+9
|
|
14
|
+
WiG6F6hQtsiWDwhQ3C1m1fOMk2Fgi/eVPEWxIRbf18XBwgvt4Q7PoyojzR730YZl
|
|
15
|
+
5WsEpliGyaBdrR4ZRh92t3y0fQ6Q/AMhsIEIu62lZbLlLenW5/OZLyVsuctAUZCg
|
|
16
|
+
HBNgwR1wuvBBXrc+TIXjUYgsfwKBgQDTpLEqsjh8ZrCvubH0mlTCHkSS/A3ifk27
|
|
17
|
+
suLZhps0J0JMM1drxpNHcfySZ31sVrMZog1cf5W6z55pYlEDCrlEjK2gjyzJnmUA
|
|
18
|
+
KE+ramKMH1/hfxdVZLY1F1zL2oLuprQEE6AUMDdZiv4JMcNfHA05Ii5VpsGe/sVe
|
|
19
|
+
aCiudjH4qwKBgFEFiMk5ynhYJjDHoVCt0cYvxt6l7jgwIkO2tKwAO6hnmsyCIQyX
|
|
20
|
+
thLEsx4cqka58Yn0sg5Pivpl2otKzJPDC4ZS4QLVAfUaz23oTXskklhnTuNOsXI4
|
|
21
|
+
cHY4JnZq18MQo0cG7FfRaTuuaADByvDg0kmt7/ftTx3nRnnnC7gEo+3fAoGAX4WG
|
|
22
|
+
fNnBSsxiVH2ARwDc3lwg4SIn9iDBXsTnQ/xP0mMhthi6DSer02Qr9bwFzL74yMnu
|
|
23
|
+
M+BVqOIKAlBgdfoCEmFYAA5+zWTS+xY2lwxF/C4w7SqaUm1JG3823774t1PADdL5
|
|
24
|
+
vgLhCGYGMAoovqV/hljaBshjvSoJ1Ik4Sz2HYk8CgYEAnSqiwlpYZKQX7jBo7yWA
|
|
25
|
+
xB6r81uUHwy+XOq7A4OfgbEEuuKQ6tXOg72FX+38rF+Td6iA+6qIPoSoGtK0jANX
|
|
26
|
+
ykroFnXr4N3IL5TafDZVvMGNuWhWwS8JR3B0VDzPobt/jxPJKmGunZoCqwJC+nqT
|
|
27
|
+
ELanzRGdLk0G2chSGO7ZKGg=
|
|
28
|
+
-----END PRIVATE KEY-----
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parses structured e-mail addresses from an address field
|
|
5
|
+
*
|
|
6
|
+
* Example:
|
|
7
|
+
*
|
|
8
|
+
* "Name <address@domain>"
|
|
9
|
+
*
|
|
10
|
+
* will be converted to
|
|
11
|
+
*
|
|
12
|
+
* [{name: "Name", address: "address@domain"}]
|
|
13
|
+
*
|
|
14
|
+
* @param {String} str Address field
|
|
15
|
+
* @return {Array} An array of address objects
|
|
16
|
+
*/
|
|
17
|
+
const parse = (module.exports = function (str, inGroup) {
|
|
18
|
+
const tokenizer = new Tokenizer(str);
|
|
19
|
+
const tokens = tokenizer.tokenize();
|
|
20
|
+
|
|
21
|
+
const addresses = [];
|
|
22
|
+
let address = [];
|
|
23
|
+
let parsedAddresses = [];
|
|
24
|
+
|
|
25
|
+
tokens.forEach(token => {
|
|
26
|
+
if (token.type === 'operator' && (token.value === ',' || token.value === ';')) {
|
|
27
|
+
if (address.length) {
|
|
28
|
+
addresses.push(address);
|
|
29
|
+
}
|
|
30
|
+
address = [];
|
|
31
|
+
} else {
|
|
32
|
+
address.push(token);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (address.length) {
|
|
37
|
+
addresses.push(address);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
addresses.forEach(address => {
|
|
41
|
+
address = _handleAddress(address, inGroup);
|
|
42
|
+
if (address.length) {
|
|
43
|
+
parsedAddresses = parsedAddresses.concat(address);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return parsedAddresses;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Converts tokens for a single address into an address object
|
|
52
|
+
*
|
|
53
|
+
* @param {Array} tokens Tokens object
|
|
54
|
+
* @param {Boolean} [inGroup] If true, the tokens are a member of a group. Groups do not nest
|
|
55
|
+
* (RFC 5322 3.4), so a colon is not a group separator here
|
|
56
|
+
* @return {Object} Address object
|
|
57
|
+
*/
|
|
58
|
+
function _handleAddress(tokens, inGroup) {
|
|
59
|
+
let token;
|
|
60
|
+
let isGroup = false;
|
|
61
|
+
let state = 'text';
|
|
62
|
+
let address;
|
|
63
|
+
const addresses = [];
|
|
64
|
+
const data = {
|
|
65
|
+
address: [],
|
|
66
|
+
comment: [],
|
|
67
|
+
group: [],
|
|
68
|
+
text: []
|
|
69
|
+
};
|
|
70
|
+
let i;
|
|
71
|
+
let len;
|
|
72
|
+
|
|
73
|
+
// Filter out <addresses>, (comments) and regular text
|
|
74
|
+
for (i = 0, len = tokens.length; i < len; i++) {
|
|
75
|
+
token = tokens[i];
|
|
76
|
+
|
|
77
|
+
if (token.type === 'operator') {
|
|
78
|
+
switch (token.value) {
|
|
79
|
+
case '<':
|
|
80
|
+
state = 'address';
|
|
81
|
+
break;
|
|
82
|
+
case '(':
|
|
83
|
+
state = 'comment';
|
|
84
|
+
break;
|
|
85
|
+
case ':':
|
|
86
|
+
if (inGroup) {
|
|
87
|
+
state = 'text';
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
state = 'group';
|
|
91
|
+
isGroup = true;
|
|
92
|
+
break;
|
|
93
|
+
default:
|
|
94
|
+
state = 'text';
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
if (token.value) {
|
|
98
|
+
data[state].push(token.value);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// If there is no text but a comment, replace the two
|
|
104
|
+
if (!data.text.length && data.comment.length) {
|
|
105
|
+
data.text = data.comment;
|
|
106
|
+
data.comment = [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (isGroup) {
|
|
110
|
+
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
|
|
111
|
+
data.text = data.text.join(' ');
|
|
112
|
+
addresses.push({
|
|
113
|
+
name: data.text || address.name,
|
|
114
|
+
group: data.group.length ? parse(data.group.join(','), true) : []
|
|
115
|
+
});
|
|
116
|
+
} else {
|
|
117
|
+
// If no address was found, try to detect one from regular text
|
|
118
|
+
if (!data.address.length && data.text.length) {
|
|
119
|
+
for (i = data.text.length - 1; i >= 0; i--) {
|
|
120
|
+
if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
|
|
121
|
+
data.address = data.text.splice(i, 1);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const _regexHandler = function (address) {
|
|
127
|
+
if (!data.address.length) {
|
|
128
|
+
data.address = [address.trim()];
|
|
129
|
+
return ' ';
|
|
130
|
+
} else {
|
|
131
|
+
return address;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// still no address
|
|
136
|
+
if (!data.address.length) {
|
|
137
|
+
for (i = data.text.length - 1; i >= 0; i--) {
|
|
138
|
+
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^@\s]+\b\s*/, _regexHandler).trim();
|
|
139
|
+
if (data.address.length) {
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// If there's still is no text but a comment exixts, replace the two
|
|
147
|
+
if (!data.text.length && data.comment.length) {
|
|
148
|
+
data.text = data.comment;
|
|
149
|
+
data.comment = [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Keep only the first address occurence, push others to regular text
|
|
153
|
+
if (data.address.length > 1) {
|
|
154
|
+
data.text = data.text.concat(data.address.splice(1));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Join values with spaces
|
|
158
|
+
data.text = data.text.join(' ');
|
|
159
|
+
data.address = data.address.join(' ');
|
|
160
|
+
|
|
161
|
+
if (!data.address && isGroup) {
|
|
162
|
+
return [];
|
|
163
|
+
} else {
|
|
164
|
+
address = {
|
|
165
|
+
address: data.address || data.text || '',
|
|
166
|
+
name: data.text || data.address || ''
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (address.address === address.name) {
|
|
170
|
+
if ((address.address || '').match(/@/)) {
|
|
171
|
+
address.name = '';
|
|
172
|
+
} else {
|
|
173
|
+
address.address = '';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
addresses.push(address);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return addresses;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Creates a Tokenizer object for tokenizing address field strings
|
|
186
|
+
*
|
|
187
|
+
* @constructor
|
|
188
|
+
* @param {String} str Address field string
|
|
189
|
+
*/
|
|
190
|
+
function Tokenizer(str) {
|
|
191
|
+
this.str = (str || '').toString();
|
|
192
|
+
this.operatorCurrent = '';
|
|
193
|
+
this.operatorExpecting = '';
|
|
194
|
+
this.node = null;
|
|
195
|
+
this.escaped = false;
|
|
196
|
+
|
|
197
|
+
this.list = [];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Operator tokens and which tokens are expected to end the sequence
|
|
202
|
+
*/
|
|
203
|
+
Tokenizer.prototype.operators = {
|
|
204
|
+
'"': '"',
|
|
205
|
+
'(': ')',
|
|
206
|
+
'<': '>',
|
|
207
|
+
',': '',
|
|
208
|
+
':': ';'
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Tokenizes the original input string
|
|
213
|
+
*
|
|
214
|
+
* @return {Array} An array of operator|text tokens
|
|
215
|
+
*/
|
|
216
|
+
Tokenizer.prototype.tokenize = function () {
|
|
217
|
+
let chr;
|
|
218
|
+
const list = [];
|
|
219
|
+
for (let i = 0, len = this.str.length; i < len; i++) {
|
|
220
|
+
chr = this.str.charAt(i);
|
|
221
|
+
this.checkChar(chr);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.list.forEach(node => {
|
|
225
|
+
node.value = (node.value || '').toString().trim();
|
|
226
|
+
if (node.value) {
|
|
227
|
+
list.push(node);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return list;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Checks if a character is an operator or text and acts accordingly
|
|
236
|
+
*
|
|
237
|
+
* @param {String} chr Character from the address field
|
|
238
|
+
*/
|
|
239
|
+
Tokenizer.prototype.checkChar = function (chr) {
|
|
240
|
+
if ((chr in this.operators || chr === '\\') && this.escaped) {
|
|
241
|
+
this.escaped = false;
|
|
242
|
+
} else if (this.operatorExpecting && chr === this.operatorExpecting) {
|
|
243
|
+
this.node = {
|
|
244
|
+
type: 'operator',
|
|
245
|
+
value: chr
|
|
246
|
+
};
|
|
247
|
+
this.list.push(this.node);
|
|
248
|
+
this.node = null;
|
|
249
|
+
this.operatorExpecting = '';
|
|
250
|
+
this.escaped = false;
|
|
251
|
+
return;
|
|
252
|
+
} else if (!this.operatorExpecting && chr in this.operators) {
|
|
253
|
+
this.node = {
|
|
254
|
+
type: 'operator',
|
|
255
|
+
value: chr
|
|
256
|
+
};
|
|
257
|
+
this.list.push(this.node);
|
|
258
|
+
this.node = null;
|
|
259
|
+
this.operatorExpecting = this.operators[chr];
|
|
260
|
+
this.escaped = false;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!this.escaped && chr === '\\') {
|
|
265
|
+
this.escaped = true;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!this.node) {
|
|
270
|
+
this.node = {
|
|
271
|
+
type: 'text',
|
|
272
|
+
value: ''
|
|
273
|
+
};
|
|
274
|
+
this.list.push(this.node);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (this.escaped && chr !== '\\') {
|
|
278
|
+
this.node.value += '\\';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
this.node.value += chr;
|
|
282
|
+
this.escaped = false;
|
|
283
|
+
};
|
package/lib/arguments.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Checks of parsed command arguments (imap-handler parser output)
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks if a parsed argument is an atom, optionally a specific one
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} value Parsed argument
|
|
9
|
+
* @param {String} [name] Upper case atom to match, the value is compared case-insensitively
|
|
10
|
+
* @return {Boolean} true for an atom (with that name)
|
|
11
|
+
*/
|
|
12
|
+
function isAtom(value, name) {
|
|
13
|
+
return !!value && !Array.isArray(value) && value.type === 'ATOM' && (name === undefined || String(value.value).toUpperCase() === name);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Checks if a parsed argument is an astring (RFC 3501 section 9): an atom, a quoted string or a literal. An
|
|
18
|
+
* atom the parser split into a section or partial range (like BODY[1]<0.5>) is not
|
|
19
|
+
*
|
|
20
|
+
* @param {Object} value Parsed argument
|
|
21
|
+
* @return {Boolean} true for an astring
|
|
22
|
+
*/
|
|
23
|
+
function isAstring(value) {
|
|
24
|
+
return !!value && !Array.isArray(value) && ['ATOM', 'STRING', 'LITERAL'].indexOf(value.type) >= 0 && !value.section && !value.partial;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// characters that end an atom in a command line (RFC 3501 section 9 atom-specials, and "[" "]" of sections)
|
|
28
|
+
const ATOM_END = /[\s()[\]{"%*\\<]/;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Lists how the client spelled the NIL atoms of a command line, in order. Quoted strings and literals are skipped
|
|
32
|
+
*
|
|
33
|
+
* @param {String} data Raw command line with its literals
|
|
34
|
+
* @return {Array} spellings, e.g. ["nil", "NIL"]
|
|
35
|
+
*/
|
|
36
|
+
function nilSpellings(data) {
|
|
37
|
+
const result = [];
|
|
38
|
+
let i = 0;
|
|
39
|
+
while (i < data.length) {
|
|
40
|
+
const ch = data.charAt(i);
|
|
41
|
+
if (ch === '"') {
|
|
42
|
+
for (i++; i < data.length && data.charAt(i) !== '"'; i++) {
|
|
43
|
+
if (data.charAt(i) === '\\') {
|
|
44
|
+
i++;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const literal = (ch === '{' || ch === '~') && data.substr(i, 32).match(/^~?\{(\d+)\+?\}\r\n/);
|
|
51
|
+
if (literal) {
|
|
52
|
+
i += literal[0].length + Number(literal[1]);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (ATOM_END.test(ch)) {
|
|
56
|
+
i++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
let end = i;
|
|
60
|
+
while (end < data.length && !ATOM_END.test(data.charAt(end))) {
|
|
61
|
+
end++;
|
|
62
|
+
}
|
|
63
|
+
const token = data.substring(i, end);
|
|
64
|
+
if (/^nil$/i.test(token)) {
|
|
65
|
+
result.push(token);
|
|
66
|
+
}
|
|
67
|
+
i = end;
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The parser turns every NIL atom into null, as in an nstring. Where the grammar has an astring (mailbox names, user
|
|
74
|
+
* names, search strings, ...) NIL is an ordinary atom, so these nulls are turned back into atoms with the spelling
|
|
75
|
+
* the client used
|
|
76
|
+
*
|
|
77
|
+
* @param {Object} parsed Parsed command, changed in place
|
|
78
|
+
* @param {String} data Raw command line with its literals
|
|
79
|
+
* @param {Function} select `(path)` returns true for the nulls to restore, `path` lists the indexes from the
|
|
80
|
+
* top-level arguments down, e.g. [2, 0] for the first item of a list in the third argument
|
|
81
|
+
*/
|
|
82
|
+
function restoreNilAtoms(parsed, data, select) {
|
|
83
|
+
if (!parsed.attributes || !/nil/i.test(data || '')) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// every null in the order the parser met them, sections included
|
|
87
|
+
const nulls = [];
|
|
88
|
+
const walk = (list, path) => {
|
|
89
|
+
list.forEach((item, i) => {
|
|
90
|
+
if (item === null) {
|
|
91
|
+
nulls.push({ list, index: i, path: path.concat(i) });
|
|
92
|
+
} else if (Array.isArray(item)) {
|
|
93
|
+
walk(item, path.concat(i));
|
|
94
|
+
} else if (item && Array.isArray(item.section)) {
|
|
95
|
+
walk(item.section, path.concat(i, 'section'));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
walk(parsed.attributes, []);
|
|
100
|
+
if (!nulls.length) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// the tag and the command name come first, so the arguments are the last NIL atoms of the line
|
|
104
|
+
const spellings = nilSpellings(data).slice(-nulls.length);
|
|
105
|
+
nulls.forEach((entry, i) => {
|
|
106
|
+
if (select(entry.path)) {
|
|
107
|
+
entry.list[entry.index] = { type: 'ATOM', value: spellings.length === nulls.length ? spellings[i] : 'NIL' };
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { isAtom, isAstring, restoreNilAtoms };
|