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.
Files changed (114) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +608 -2
  3. package/bin/help.txt +98 -0
  4. package/bin/imapkit.js +108 -0
  5. package/cert/server.crt +20 -0
  6. package/cert/server.key +28 -0
  7. package/lib/addressparser.js +283 -0
  8. package/lib/arguments.js +112 -0
  9. package/lib/bodystructure.js +149 -0
  10. package/lib/command-states.js +109 -0
  11. package/lib/commands/append.js +313 -0
  12. package/lib/commands/capability.js +47 -0
  13. package/lib/commands/check.js +21 -0
  14. package/lib/commands/close.js +30 -0
  15. package/lib/commands/copy.js +115 -0
  16. package/lib/commands/create.js +52 -0
  17. package/lib/commands/delete.js +64 -0
  18. package/lib/commands/examine.js +7 -0
  19. package/lib/commands/expunge.js +27 -0
  20. package/lib/commands/fetch.js +229 -0
  21. package/lib/commands/handlers/fetch.js +209 -0
  22. package/lib/commands/handlers/flags.js +42 -0
  23. package/lib/commands/handlers/search.js +519 -0
  24. package/lib/commands/handlers/status.js +85 -0
  25. package/lib/commands/handlers/store.js +127 -0
  26. package/lib/commands/list.js +100 -0
  27. package/lib/commands/login.js +67 -0
  28. package/lib/commands/logout.js +41 -0
  29. package/lib/commands/lsub.js +87 -0
  30. package/lib/commands/noop.js +21 -0
  31. package/lib/commands/rename.js +102 -0
  32. package/lib/commands/search.js +76 -0
  33. package/lib/commands/select.js +289 -0
  34. package/lib/commands/status.js +63 -0
  35. package/lib/commands/store.js +151 -0
  36. package/lib/commands/subscribe.js +53 -0
  37. package/lib/commands/uid copy.js +7 -0
  38. package/lib/commands/uid fetch.js +5 -0
  39. package/lib/commands/uid search.js +5 -0
  40. package/lib/commands/uid store.js +5 -0
  41. package/lib/commands/unsubscribe.js +50 -0
  42. package/lib/dates.js +123 -0
  43. package/lib/deflate-layer.js +232 -0
  44. package/lib/envelope.js +82 -0
  45. package/lib/esearch.js +208 -0
  46. package/lib/framing.js +102 -0
  47. package/lib/list-extensions.js +36 -0
  48. package/lib/load-plugins.js +109 -0
  49. package/lib/mailbox-name.js +133 -0
  50. package/lib/mimeparser.js +778 -0
  51. package/lib/mock-client.js +233 -0
  52. package/lib/numbers.js +52 -0
  53. package/lib/plugins/acl.js +964 -0
  54. package/lib/plugins/appendlimit.js +83 -0
  55. package/lib/plugins/auth-plain.js +94 -0
  56. package/lib/plugins/binary.js +256 -0
  57. package/lib/plugins/catenate.js +253 -0
  58. package/lib/plugins/compress.js +76 -0
  59. package/lib/plugins/condstore.js +563 -0
  60. package/lib/plugins/context-search.js +321 -0
  61. package/lib/plugins/context-sort.js +19 -0
  62. package/lib/plugins/create-special-use.js +108 -0
  63. package/lib/plugins/enable.js +155 -0
  64. package/lib/plugins/esearch.js +156 -0
  65. package/lib/plugins/esort.js +60 -0
  66. package/lib/plugins/id.js +138 -0
  67. package/lib/plugins/idle.js +105 -0
  68. package/lib/plugins/imap4rev2.js +202 -0
  69. package/lib/plugins/list-extended.js +258 -0
  70. package/lib/plugins/list-status.js +31 -0
  71. package/lib/plugins/literalminus.js +20 -0
  72. package/lib/plugins/literalplus.js +18 -0
  73. package/lib/plugins/logindisabled.js +50 -0
  74. package/lib/plugins/messagelimit.js +234 -0
  75. package/lib/plugins/metadata-server.js +13 -0
  76. package/lib/plugins/metadata.js +475 -0
  77. package/lib/plugins/move.js +110 -0
  78. package/lib/plugins/multiappend.js +26 -0
  79. package/lib/plugins/multisearch.js +269 -0
  80. package/lib/plugins/namespace.js +67 -0
  81. package/lib/plugins/notify.js +654 -0
  82. package/lib/plugins/oauthbearer.js +217 -0
  83. package/lib/plugins/objectid.js +243 -0
  84. package/lib/plugins/partial.js +68 -0
  85. package/lib/plugins/preview.js +400 -0
  86. package/lib/plugins/qresync.js +525 -0
  87. package/lib/plugins/quota.js +285 -0
  88. package/lib/plugins/replace.js +145 -0
  89. package/lib/plugins/sasl-ir.js +12 -0
  90. package/lib/plugins/savedate.js +59 -0
  91. package/lib/plugins/savelimit.js +18 -0
  92. package/lib/plugins/searchres.js +82 -0
  93. package/lib/plugins/sort-display.js +23 -0
  94. package/lib/plugins/sort.js +132 -0
  95. package/lib/plugins/special-use.js +95 -0
  96. package/lib/plugins/starttls.js +57 -0
  97. package/lib/plugins/status-size.js +19 -0
  98. package/lib/plugins/thread-orderedsubject.js +16 -0
  99. package/lib/plugins/thread-references.js +16 -0
  100. package/lib/plugins/uidonly.js +135 -0
  101. package/lib/plugins/uidplus.js +124 -0
  102. package/lib/plugins/unauthenticate.js +28 -0
  103. package/lib/plugins/unselect.js +36 -0
  104. package/lib/plugins/utf8-accept.js +68 -0
  105. package/lib/plugins/x-gm-ext-1.js +456 -0
  106. package/lib/plugins/xoauth2.js +188 -0
  107. package/lib/plugins/xtoybird.js +282 -0
  108. package/lib/server.js +2880 -0
  109. package/lib/smtp-listener.js +51 -0
  110. package/lib/sorting.js +373 -0
  111. package/lib/threading.js +357 -0
  112. package/lib/utf8-session.js +123 -0
  113. package/lib/vanished.js +57 -0
  114. 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
+ }
@@ -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-----
@@ -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
+ };
@@ -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 };