├── .babelrc ├── .editorconfig ├── .gitignore ├── .travis.yml ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── dist ├── client.js ├── command-builder.js ├── command-parser.js ├── common.js ├── compression.js ├── imap.js ├── index.js ├── logger.js └── special-use.js ├── package.json ├── res ├── compression.worker.blob └── fixtures │ ├── envelope.js │ └── mime-torture-bodystructure.js ├── scripts ├── build.sh └── worker.sh ├── src ├── client-integration.js ├── client-unit.js ├── client.js ├── command-builder-unit.js ├── command-builder.js ├── command-parser-unit.js ├── command-parser.js ├── common.js ├── compression-worker.js ├── compression.js ├── imap-unit.js ├── imap.js ├── index.js ├── logger.js ├── special-use-unit.js └── special-use.js ├── testutils.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "node": "7.0.0", 6 | "browsers": ["last 2 Chrome versions"] 7 | }, 8 | }] 9 | ], 10 | "plugins": [ 11 | ["babel-plugin-inline-import", { 12 | "extensions": [ 13 | ".blob" 14 | ] 15 | }] 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | max_line_length = null 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | .DS_Store 4 | package-lock.json 5 | 6 | # VIM Swap Files 7 | [._]*.s[a-v][a-z] 8 | [._]*.sw[a-p] 9 | [._]s[a-v][a-z] 10 | [._]sw[a-p] 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - lts/* 5 | notifications: 6 | email: 7 | recipients: 8 | - felix.hammerl@gmail.com 9 | script: 10 | - npm test 11 | deploy: 12 | provider: npm 13 | email: felix.hammerl+emailjs-deployment-user@gmail.com 14 | api_key: 15 | secure: Qh35Xp0St7jA16zqLBUkwx99YhgwOnCEHELtVhvYZ3MU+w7evQGycgUN4KKJ9iS02M2E9Xic5MeVQ6mF8WZSL9RDeck34p7TGYrYi+4EL54NvanMDJOmQaQ9kAEWN5XdYf1EgG/+GLktxl5jxGJTAY/gzGPMyCPmbdf+huREhAI= 16 | on: 17 | tags: true 18 | all_branches: true 19 | condition: "$TRAVIS_TAG =~ ^v[0-9]+\\.[0-9]+\\.[0-9]+" 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run ES6 Tests", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceRoot}", 9 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 10 | "stopOnEntry": false, 11 | "args": [ 12 | "./src/*-unit.js", 13 | "--require", "@babel/register", 14 | "testutils.js", 15 | "--reporter", "spec", 16 | "--no-timeouts" 17 | ], 18 | "runtimeArgs": [ 19 | "--nolazy" 20 | ], 21 | "sourceMaps": true 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Andris Reinman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /dist/command-builder.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.buildFETCHCommand = buildFETCHCommand; 7 | exports.buildXOAuth2Token = buildXOAuth2Token; 8 | exports.buildSEARCHCommand = buildSEARCHCommand; 9 | exports.buildSTORECommand = buildSTORECommand; 10 | 11 | var _emailjsImapHandler = require("emailjs-imap-handler"); 12 | 13 | var _emailjsMimeCodec = require("emailjs-mime-codec"); 14 | 15 | var _emailjsBase = require("emailjs-base64"); 16 | 17 | var _common = require("./common"); 18 | 19 | function buildFETCHCommand(sequence, items, options) { 20 | const command = { 21 | command: options.byUid ? 'UID FETCH' : 'FETCH', 22 | attributes: [{ 23 | type: 'SEQUENCE', 24 | value: sequence 25 | }] 26 | }; 27 | 28 | if (options.valueAsString !== undefined) { 29 | command.valueAsString = options.valueAsString; 30 | } 31 | 32 | let query = []; 33 | items.forEach(item => { 34 | item = item.toUpperCase().trim(); 35 | 36 | if (/^\w+$/.test(item)) { 37 | // alphanum strings can be used directly 38 | query.push({ 39 | type: 'ATOM', 40 | value: item 41 | }); 42 | } else if (item) { 43 | try { 44 | // parse the value as a fake command, use only the attributes block 45 | const cmd = (0, _emailjsImapHandler.parser)((0, _common.toTypedArray)('* Z ' + item)); 46 | query = query.concat(cmd.attributes || []); 47 | } catch (e) { 48 | // if parse failed, use the original string as one entity 49 | query.push({ 50 | type: 'ATOM', 51 | value: item 52 | }); 53 | } 54 | } 55 | }); 56 | 57 | if (query.length === 1) { 58 | query = query.pop(); 59 | } 60 | 61 | command.attributes.push(query); 62 | 63 | if (options.changedSince) { 64 | command.attributes.push([{ 65 | type: 'ATOM', 66 | value: 'CHANGEDSINCE' 67 | }, { 68 | type: 'ATOM', 69 | value: options.changedSince 70 | }]); 71 | } 72 | 73 | return command; 74 | } 75 | /** 76 | * Builds a login token for XOAUTH2 authentication command 77 | * 78 | * @param {String} user E-mail address of the user 79 | * @param {String} token Valid access token for the user 80 | * @return {String} Base64 formatted login token 81 | */ 82 | 83 | 84 | function buildXOAuth2Token(user = '', token) { 85 | const authData = [`user=${user}`, `auth=Bearer ${token}`, '', '']; 86 | return (0, _emailjsBase.encode)(authData.join('\x01')); 87 | } 88 | /** 89 | * Compiles a search query into an IMAP command. Queries are composed as objects 90 | * where keys are search terms and values are term arguments. Only strings, 91 | * numbers and Dates are used. If the value is an array, the members of it 92 | * are processed separately (use this for terms that require multiple params). 93 | * If the value is a Date, it is converted to the form of "01-Jan-1970". 94 | * Subqueries (OR, NOT) are made up of objects 95 | * 96 | * {unseen: true, header: ["subject", "hello world"]}; 97 | * SEARCH UNSEEN HEADER "subject" "hello world" 98 | * 99 | * @param {Object} query Search query 100 | * @param {Object} [options] Option object 101 | * @param {Boolean} [options.byUid] If ture, use UID SEARCH instead of SEARCH 102 | * @return {Object} IMAP command object 103 | */ 104 | 105 | 106 | function buildSEARCHCommand(query = {}, options = {}) { 107 | const command = { 108 | command: options.byUid ? 'UID SEARCH' : 'SEARCH' 109 | }; 110 | let isAscii = true; 111 | 112 | const buildTerm = query => { 113 | let list = []; 114 | Object.keys(query).forEach(key => { 115 | let params = []; 116 | 117 | const formatDate = date => date.toUTCString().replace(/^\w+, 0?(\d+) (\w+) (\d+).*/, '$1-$2-$3'); 118 | 119 | const escapeParam = param => { 120 | if (typeof param === 'number') { 121 | return { 122 | type: 'number', 123 | value: param 124 | }; 125 | } else if (typeof param === 'string') { 126 | if (/[\u0080-\uFFFF]/.test(param)) { 127 | isAscii = false; 128 | return { 129 | type: 'literal', 130 | value: (0, _common.fromTypedArray)((0, _emailjsMimeCodec.encode)(param)) // cast unicode string to pseudo-binary as imap-handler compiles strings as octets 131 | 132 | }; 133 | } 134 | 135 | return { 136 | type: 'string', 137 | value: param 138 | }; 139 | } else if (Object.prototype.toString.call(param) === '[object Date]') { 140 | // RFC 3501 allows for dates to be placed in 141 | // double-quotes or left without quotes. Some 142 | // servers (Yandex), do not like the double quotes, 143 | // so we treat the date as an atom. 144 | return { 145 | type: 'atom', 146 | value: formatDate(param) 147 | }; 148 | } else if (Array.isArray(param)) { 149 | return param.map(escapeParam); 150 | } else if (typeof param === 'object') { 151 | return buildTerm(param); 152 | } 153 | }; 154 | 155 | params.push({ 156 | type: 'atom', 157 | value: key.toUpperCase() 158 | }); 159 | [].concat(query[key] || []).forEach(param => { 160 | switch (key.toLowerCase()) { 161 | case 'uid': 162 | param = { 163 | type: 'sequence', 164 | value: param 165 | }; 166 | break; 167 | // The Gmail extension values of X-GM-THRID and 168 | // X-GM-MSGID are defined to be unsigned 64-bit integers 169 | // and they must not be quoted strings or the server 170 | // will report a parse error. 171 | 172 | case 'x-gm-thrid': 173 | case 'x-gm-msgid': 174 | param = { 175 | type: 'number', 176 | value: param 177 | }; 178 | break; 179 | 180 | default: 181 | param = escapeParam(param); 182 | } 183 | 184 | if (param) { 185 | params = params.concat(param || []); 186 | } 187 | }); 188 | list = list.concat(params || []); 189 | }); 190 | return list; 191 | }; 192 | 193 | command.attributes = buildTerm(query); // If any string input is using 8bit bytes, prepend the optional CHARSET argument 194 | 195 | if (!isAscii) { 196 | command.attributes.unshift({ 197 | type: 'atom', 198 | value: 'UTF-8' 199 | }); 200 | command.attributes.unshift({ 201 | type: 'atom', 202 | value: 'CHARSET' 203 | }); 204 | } 205 | 206 | return command; 207 | } 208 | /** 209 | * Creates an IMAP STORE command from the selected arguments 210 | */ 211 | 212 | 213 | function buildSTORECommand(sequence, action = '', flags = [], options = {}) { 214 | const command = { 215 | command: options.byUid ? 'UID STORE' : 'STORE', 216 | attributes: [{ 217 | type: 'sequence', 218 | value: sequence 219 | }] 220 | }; 221 | command.attributes.push({ 222 | type: 'atom', 223 | value: action.toUpperCase() + (options.silent ? '.SILENT' : '') 224 | }); 225 | command.attributes.push(flags.map(flag => { 226 | return { 227 | type: 'atom', 228 | value: flag 229 | }; 230 | })); 231 | return command; 232 | } 233 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9jb21tYW5kLWJ1aWxkZXIuanMiXSwibmFtZXMiOlsiYnVpbGRGRVRDSENvbW1hbmQiLCJzZXF1ZW5jZSIsIml0ZW1zIiwib3B0aW9ucyIsImNvbW1hbmQiLCJieVVpZCIsImF0dHJpYnV0ZXMiLCJ0eXBlIiwidmFsdWUiLCJ2YWx1ZUFzU3RyaW5nIiwidW5kZWZpbmVkIiwicXVlcnkiLCJmb3JFYWNoIiwiaXRlbSIsInRvVXBwZXJDYXNlIiwidHJpbSIsInRlc3QiLCJwdXNoIiwiY21kIiwiY29uY2F0IiwiZSIsImxlbmd0aCIsInBvcCIsImNoYW5nZWRTaW5jZSIsImJ1aWxkWE9BdXRoMlRva2VuIiwidXNlciIsInRva2VuIiwiYXV0aERhdGEiLCJqb2luIiwiYnVpbGRTRUFSQ0hDb21tYW5kIiwiaXNBc2NpaSIsImJ1aWxkVGVybSIsImxpc3QiLCJPYmplY3QiLCJrZXlzIiwia2V5IiwicGFyYW1zIiwiZm9ybWF0RGF0ZSIsImRhdGUiLCJ0b1VUQ1N0cmluZyIsInJlcGxhY2UiLCJlc2NhcGVQYXJhbSIsInBhcmFtIiwicHJvdG90eXBlIiwidG9TdHJpbmciLCJjYWxsIiwiQXJyYXkiLCJpc0FycmF5IiwibWFwIiwidG9Mb3dlckNhc2UiLCJ1bnNoaWZ0IiwiYnVpbGRTVE9SRUNvbW1hbmQiLCJhY3Rpb24iLCJmbGFncyIsInNpbGVudCIsImZsYWciXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7QUFBQTs7QUFDQTs7QUFDQTs7QUFDQTs7QUFhTyxTQUFTQSxpQkFBVCxDQUE0QkMsUUFBNUIsRUFBc0NDLEtBQXRDLEVBQTZDQyxPQUE3QyxFQUFzRDtBQUMzRCxRQUFNQyxPQUFPLEdBQUc7QUFDZEEsSUFBQUEsT0FBTyxFQUFFRCxPQUFPLENBQUNFLEtBQVIsR0FBZ0IsV0FBaEIsR0FBOEIsT0FEekI7QUFFZEMsSUFBQUEsVUFBVSxFQUFFLENBQUM7QUFDWEMsTUFBQUEsSUFBSSxFQUFFLFVBREs7QUFFWEMsTUFBQUEsS0FBSyxFQUFFUDtBQUZJ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b3J0IHsgcGFyc2VyIH0gZnJvbSAnZW1haWxqcy1pbWFwLWhhbmRsZXInXG5pbXBvcnQgeyBlbmNvZGUgfSBmcm9tICdlbWFpbGpzLW1pbWUtY29kZWMnXG5pbXBvcnQgeyBlbmNvZGUgYXMgZW5jb2RlQmFzZTY0IH0gZnJvbSAnZW1haWxqcy1iYXNlNjQnXG5pbXBvcnQge1xuICBmcm9tVHlwZWRBcnJheSxcbiAgdG9UeXBlZEFycmF5XG59IGZyb20gJy4vY29tbW9uJ1xuXG4vKipcbiAqIEJ1aWxkcyBhIEZFVENIIGNvbW1hbmRcbiAqXG4gKiBAcGFyYW0ge1N0cmluZ30gc2VxdWVuY2UgTWVzc2FnZSByYW5nZSBzZWxlY3RvclxuICogQHBhcmFtIHtBcnJheX0gaXRlbXMgTGlzdCBvZiBlbGVtZW50cyB0byBmZXRjaCAoZWcuIGBbJ3VpZCcsICdlbnZlbG9wZSddYCkuXG4gKiBAcGFyYW0ge09iamVjdH0gW29wdGlvbnNdIE9wdGlvbmFsIG9wdGlvbnMgb2JqZWN0LiBVc2UgYHtieVVpZDp0cnVlfWAgZm9yIGBVSUQgRkVUQ0hgXG4gKiBAcmV0dXJucyB7T2JqZWN0fSBTdHJ1Y3R1cmVkIElNQVAgY29tbWFuZFxuICovXG5leHBvcnQgZnVuY3Rpb24gYnVpbGRGRVRDSENvbW1hbmQgKHNlcXVlbmNlLCBpdGVtcywgb3B0aW9ucykge1xuICBjb25zdCBjb21tYW5kID0ge1xuICAgIGNvbW1hbmQ6IG9wdGlvbnMuYnlVaWQgPyAnVUlEIEZFVENIJyA6ICdGRVRDSCcsXG4gICAgYXR0cmlidXRlczogW3tcbiAgICAgIHR5cGU6ICdTRVFVRU5DRScsXG4gICAgICB2YWx1ZTogc2VxdWVuY2VcbiAgICB9XVxuICB9XG5cbiAgaWYgKG9wdGlvbnMudmFsdWVBc1N0cmluZyAhPT0gdW5kZWZpbmVkKSB7XG4gICAgY29tbWFuZC52YWx1ZUFzU3RyaW5nID0gb3B0aW9ucy52YWx1ZUFzU3RyaW5nXG4gIH1cblxuICBsZXQgcXVlcnkgPSBbXVxuXG4gIGl0ZW1zLmZvckVhY2goKGl0ZW0pID0+IHtcbiAgICBpdGVtID0gaXRlbS50b1VwcGVyQ2FzZSgpLnRyaW0oKVxuXG4gICAgaWYgKC9eXFx3KyQvLnRlc3QoaXRlbSkpIHtcbiAgICAgIC8vIGFscGhhbnVtIHN0cmluZ3MgY2FuIGJlIHVzZWQgZGlyZWN0bHlcbiAgICAgIHF1ZXJ5LnB1c2goe1xuICAgICAgICB0eXBlOiAnQVRPTScsXG4gICAgICAgIHZhbHVlOiBpdGVtXG4gICAgICB9KVxuICAgIH0gZWxzZSBpZiAoaXRlbSkge1xuICAgICAgdHJ5IHtcbiAgICAgICAgLy8gcGFyc2UgdGhlIHZhbHVlIGFzIGEgZmFrZSBjb21tYW5kLCB1c2Ugb25seSB0aGUgYXR0cmlidXRlcyBibG9ja1xuICAgICAgICBjb25zdCBjbWQgPSBwYXJzZXIodG9UeXBlZEFycmF5KCcqIFogJyArIGl0ZW0pKVxuICAgICAgICBxdWVyeSA9IHF1ZXJ5LmNvbmNhdChjbWQuYXR0cmlidXRlcyB8fCBbXSlcbiAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgLy8gaWYgcGFyc2UgZmFpbGVkLCB1c2UgdGhlIG9yaWdpbmFsIHN0cmluZyBhcyBvbmUgZW50aXR5XG4gICAgICAgIHF1ZXJ5LnB1c2goe1xuICAgICAgICAgIHR5cGU6ICdBVE9NJyxcbiAgICAgICAgICB2YWx1ZTogaXRlbVxuICAgICAgICB9KVxuICAgICAgfVxuICAgIH1cbiAgfSlcblxuICBpZiAocXVlcnkubGVuZ3RoID09PSAxKSB7XG4gICAgcXVlcnkgPSBxdWVyeS5wb3AoKVxuICB9XG5cbiAgY29tbWFuZC5hdHRyaWJ1dGVzLnB1c2gocXVlcnkpXG5cbiAgaWYgKG9wdGlvbnMuY2hhbmdlZFNpbmNlKSB7XG4gICAgY29tbWFuZC5hdHRyaWJ1dGVzLnB1c2goW3tcbiAgICAgIHR5cGU6ICdBVE9NJyxcbiAgICAgIHZhbHVlOiAnQ0hBTkdFRFNJTkNFJ1xuICAgIH0sIHtcbiAgICAgIHR5cGU6ICdBVE9NJyxcbiAgICAgIHZhbHVlOiBvcHRpb25zLmNoYW5nZWRTaW5jZVxuICAgIH1dKVxuICB9XG5cbiAgcmV0dXJuIGNvbW1hbmRcbn1cblxuLyoqXG4gKiBCdWlsZHMgYSBsb2dpbiB0b2tlbiBmb3IgWE9BVVRIMiBhdXRoZW50aWNhdGlvbiBjb21tYW5kXG4gKlxuICogQHBhcmFtIHtTdHJpbmd9IHVzZXIgRS1tYWlsIGFkZHJlc3Mgb2YgdGhlIHVzZXJcbiAqIEBwYXJhbSB7U3RyaW5nfSB0b2tlbiBWYWxpZCBhY2Nlc3MgdG9rZW4gZm9yIHRoZSB1c2VyXG4gKiBAcmV0dXJuIHtTdHJpbmd9IEJhc2U2NCBmb3JtYXR0ZWQgbG9naW4gdG9rZW5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGJ1aWxkWE9BdXRoMlRva2VuICh1c2VyID0gJycsIHRva2VuKSB7XG4gIGNvbnN0IGF1dGhEYXRhID0gW1xuICAgIGB1c2VyPSR7dXNlcn1gLFxuICAgIGBhdXRoPUJlYXJlciAke3Rva2VufWAsXG4gICAgJycsXG4gICAgJydcbiAgXVxuICByZXR1cm4gZW5jb2RlQmFzZTY0KGF1dGhEYXRhLmpvaW4oJ1xceDAxJykpXG59XG5cbi8qKlxuICogQ29tcGlsZXMgYSBzZWFyY2ggcXVlcnkgaW50byBhbiBJTUFQIGNvbW1hbmQuIFF1ZXJpZXMgYXJlIGNvbXBvc2VkIGFzIG9iamVjdHNcbiAqIHdoZXJlIGtleXMgYXJlIHNlYXJjaCB0ZXJtcyBhbmQgdmFsdWVzIGFyZSB0ZXJtIGFyZ3VtZW50cy4gT25seSBzdHJpbmdzLFxuICogbnVtYmVycyBhbmQgRGF0ZXMgYXJlIHVzZWQuIElmIHRoZSB2YWx1ZSBpcyBhbiBhcnJheSwgdGhlIG1lbWJlcnMgb2YgaXRcbiAqIGFyZSBwcm9jZXNzZWQgc2VwYXJhdGVseSAodXNlIHRoaXMgZm9yIHRlcm1zIHRoYXQgcmVxdWlyZSBtdWx0aXBsZSBwYXJhbXMpLlxuICogSWYgdGhlIHZhbHVlIGlzIGEgRGF0ZSwgaXQgaXMgY29udmVydGVkIHRvIHRoZSBmb3JtIG9mIFwiMDEtSmFuLTE5NzBcIi5cbiAqIFN1YnF1ZXJpZXMgKE9SLCBOT1QpIGFyZSBtYWRlIHVwIG9mIG9iamVjdHNcbiAqXG4gKiAgICB7dW5zZWVuOiB0cnVlLCBoZWFkZXI6IFtcInN1YmplY3RcIiwgXCJoZWxsbyB3b3JsZFwiXX07XG4gKiAgICBTRUFSQ0ggVU5TRUVOIEhFQURFUiBcInN1YmplY3RcIiBcImhlbGxvIHdvcmxkXCJcbiAqXG4gKiBAcGFyYW0ge09iamVjdH0gcXVlcnkgU2VhcmNoIHF1ZXJ5XG4gKiBAcGFyYW0ge09iamVjdH0gW29wdGlvbnNdIE9wdGlvbiBvYmplY3RcbiAqIEBwYXJhbSB7Qm9vbGVhbn0gW29wdGlvbnMuYnlVaWRdIElmIHR1cmUsIHVzZSBVSUQgU0VBUkNIIGluc3RlYWQgb2YgU0VBUkNIXG4gKiBAcmV0dXJuIHtPYmplY3R9IElNQVAgY29tbWFuZCBvYmplY3RcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGJ1aWxkU0VBUkNIQ29tbWFuZCAocXVlcnkgPSB7fSwgb3B0aW9ucyA9IHt9KSB7XG4gIGNvbnN0IGNvbW1hbmQgPSB7XG4gICAgY29tbWFuZDogb3B0aW9ucy5ieVVpZCA/ICdVSUQgU0VBUkNIJyA6ICdTRUFSQ0gnXG4gIH1cblxuICBsZXQgaXNBc2NpaSA9IHRydWVcblxuICBjb25zdCBidWlsZFRlcm0gPSAocXVlcnkpID0+IHtcbiAgICBsZXQgbGlzdCA9IFtdXG5cbiAgICBPYmplY3Qua2V5cyhxdWVyeSkuZm9yRWFjaCgoa2V5KSA9PiB7XG4gICAgICBsZXQgcGFyYW1zID0gW11cbiAgICAgIGNvbnN0IGZvcm1hdERhdGUgPSAoZGF0ZSkgPT4gZGF0ZS50b1VUQ1N0cmluZygpLnJlcGxhY2UoL15cXHcrLCAwPyhcXGQrKSAoXFx3KykgKFxcZCspLiovLCAnJDEtJDItJDMnKVxuICAgICAgY29uc3QgZXNjYXBlUGFyYW0gPSAocGFyYW0pID0+IHtcbiAgICAgICAgaWYgKHR5cGVvZiBwYXJhbSA9PT0gJ251bWJlcicpIHtcbiAgICAgICAgICByZXR1cm4ge1xuICAgICAgICAgICAgdHlwZTogJ251bWJlcicsXG4gICAgICAgICAgICB2YWx1ZTogcGFyYW1cbiAgICAgICAgICB9XG4gICAgICAgIH0gZWxzZSBpZiAodHlwZW9mIHBhcmFtID09PSAnc3RyaW5nJykge1xuICAgICAgICAgIGlmICgvW1xcdTAwODAtXFx1RkZGRl0vLnRlc3QocGFyYW0pKSB7XG4gICAgICAgICAgICBpc0FzY2lpID0gZmFsc2VcbiAgICAgICAgICAgIHJldHVybiB7XG4gICAgICAgICAgICAgIHR5cGU6ICdsaXRlcmFsJyxcbiAgICAgICAgICAgICAgdmFsdWU6IGZyb21UeXBlZEFycmF5KGVuY29kZShwYXJhbSkpIC8vIGNhc3QgdW5pY29kZSBzdHJpbmcgdG8gcHNldWRvLWJpbmFyeSBhcyBpbWFwLWhhbmRsZXIgY29tcGlsZXMgc3RyaW5ncyBhcyBvY3RldHNcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgICAgcmV0dXJuIHtcbiAgICAgICAgICAgIHR5cGU6ICdzdHJpbmcnLFxuICAgICAgICAgICAgdmFsdWU6IHBhcmFtXG4gICAgICAgICAgfVxuICAgICAgICB9IGVsc2UgaWYgKE9iamVjdC5wcm90b3R5cGUudG9TdHJpbmcuY2FsbChwYXJhbSkgPT09ICdbb2JqZWN0IERhdGVdJykge1xuICAgICAgICAgIC8vIFJGQyAzNTAxIGFsbG93cyBmb3IgZGF0ZXMgdG8gYmUgcGxhY2VkIGluXG4gICAgICAgICAgLy8gZG91YmxlLXF1b3RlcyBvciBsZWZ0IHdpdGhvdXQgcXVvdGVzLiAgU29tZVxuICAgICAgICAgIC8vIHNlcnZlcnMgKFlhbmRleCksIGRvIG5vdCBsaWtlIHRoZSBkb3VibGUgcXVvdGVzLFxuICAgICAgICAgIC8vIHNvIHdlIHRyZWF0IHRoZSBkYXRlIGFzIGFuIGF0b20uXG4gICAgICAgICAgcmV0dXJuIHtcbiAgICAgICAgICAgIHR5cGU6ICdhdG9tJyxcbiAgICAgICAgICAgIHZhbHVlOiBmb3JtYXREYXRlKHBhcmFtKVxuICAgICAgICAgIH1cbiAgICAgICAgfSBlbHNlIGlmIChBcnJheS5pc0FycmF5KHBhcmFtKSkge1xuICAgICAgICAgIHJldHVybiBwYXJhbS5tYXAoZXNjYXBlUGFyYW0pXG4gICAgICAgIH0gZWxzZSBpZiAodHlwZW9mIHBhcmFtID09PSAnb2JqZWN0Jykge1xuICAgICAgICAgIHJldHVybiBidWlsZFRlcm0ocGFyYW0pXG4gICAgICAgIH1cbiAgICAgIH1cblxuICAgICAgcGFyYW1zLnB1c2goe1xuICAgICAgICB0eXBlOiAnYXRvbScsXG4gICAgICAgIHZhbHVlOiBrZXkudG9VcHBlckNhc2UoKVxuICAgICAgfSk7XG5cbiAgICAgIFtdLmNvbmNhdChxdWVyeVtrZXldIHx8IFtdKS5mb3JFYWNoKChwYXJhbSkgPT4ge1xuICAgICAgICBzd2l0Y2ggKGtleS50b0xvd2VyQ2FzZSgpKSB7XG4gICAgICAgICAgY2FzZSAndWlkJzpcbiAgICAgICAgICAgIHBhcmFtID0ge1xuICAgICAgICAgICAgICB0eXBlOiAnc2VxdWVuY2UnLFxuICAgICAgICAgICAgICB2YWx1ZTogcGFyYW1cbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGJyZWFrXG4gICAgICAgICAgLy8gVGhlIEdtYWlsIGV4dGVuc2lvbiB2YWx1ZXMgb2YgWC1HTS1USFJJRCBhbmRcbiAgICAgICAgICAvLyBYLUdNLU1TR0lEIGFyZSBkZWZpbmVkIHRvIGJlIHVuc2lnbmVkIDY0LWJpdCBpbnRlZ2Vyc1xuICAgICAgICAgIC8vIGFuZCB0aGV5IG11c3Qgbm90IGJlIHF1b3RlZCBzdHJpbmdzIG9yIHRoZSBzZXJ2ZXJcbiAgICAgICAgICAvLyB3aWxsIHJlcG9ydCBhIHBhcnNlIGVycm9yLlxuICAgICAgICAgIGNhc2UgJ3gtZ20tdGhyaWQnOlxuICAgICAgICAgIGNhc2UgJ3gtZ20tbXNnaWQnOlxuICAgICAgICAgICAgcGFyYW0gPSB7XG4gICAgICAgICAgICAgIHR5cGU6ICdudW1iZXInLFxuICAgICAgICAgICAgICB2YWx1ZTogcGFyYW1cbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGJyZWFrXG4gICAgICAgICAgZGVmYXVsdDpcbiAgICAgICAgICAgIHBhcmFtID0gZXNjYXBlUGFyYW0ocGFyYW0pXG4gICAgICAgIH1cbiAgICAgICAgaWYgKHBhcmFtKSB7XG4gICAgICAgICAgcGFyYW1zID0gcGFyYW1zLmNvbmNhdChwYXJhbSB8fCBbXSlcbiAgICAgICAgfVxuICAgICAgfSlcbiAgICAgIGxpc3QgPSBsaXN0LmNvbmNhdChwYXJhbXMgfHwgW10pXG4gICAgfSlcblxuICAgIHJldHVybiBsaXN0XG4gIH1cblxuICBjb21tYW5kLmF0dHJpYnV0ZXMgPSBidWlsZFRlcm0ocXVlcnkpXG5cbiAgLy8gSWYgYW55IHN0cmluZyBpbnB1dCBpcyB1c2luZyA4Yml0IGJ5dGVzLCBwcmVwZW5kIHRoZSBvcHRpb25hbCBDSEFSU0VUIGFyZ3VtZW50XG4gIGlmICghaXNBc2NpaSkge1xuICAgIGNvbW1hbmQuYXR0cmlidXRlcy51bnNoaWZ0KHtcbiAgICAgIHR5cGU6ICdhdG9tJyxcbiAgICAgIHZhbHVlOiAnVVRGLTgnXG4gICAgfSlcbiAgICBjb21tYW5kLmF0dHJpYnV0ZXMudW5zaGlmdCh7XG4gICAgICB0eXBlOiAnYXRvbScsXG4gICAgICB2YWx1ZTogJ0NIQVJTRVQnXG4gICAgfSlcbiAgfVxuXG4gIHJldHVybiBjb21tYW5kXG59XG5cbi8qKlxuICogQ3JlYXRlcyBhbiBJTUFQIFNUT1JFIGNvbW1hbmQgZnJvbSB0aGUgc2VsZWN0ZWQgYXJndW1lbnRzXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBidWlsZFNUT1JFQ29tbWFuZCAoc2VxdWVuY2UsIGFjdGlvbiA9ICcnLCBmbGFncyA9IFtdLCBvcHRpb25zID0ge30pIHtcbiAgY29uc3QgY29tbWFuZCA9IHtcbiAgICBjb21tYW5kOiBvcHRpb25zLmJ5VWlkID8gJ1VJRCBTVE9SRScgOiAnU1RPUkUnLFxuICAgIGF0dHJpYnV0ZXM6IFt7XG4gICAgICB0eXBlOiAnc2VxdWVuY2UnLFxuICAgICAgdmFsdWU6IHNlcXVlbmNlXG4gICAgfV1cbiAgfVxuXG4gIGNvbW1hbmQuYXR0cmlidXRlcy5wdXNoKHtcbiAgICB0eXBlOiAnYXRvbScsXG4gICAgdmFsdWU6IGFjdGlvbi50b1VwcGVyQ2FzZSgpICsgKG9wdGlvbnMuc2lsZW50ID8gJy5TSUxFTlQnIDogJycpXG4gIH0pXG5cbiAgY29tbWFuZC5hdHRyaWJ1dGVzLnB1c2goZmxhZ3MubWFwKChmbGFnKSA9PiB7XG4gICAgcmV0dXJuIHtcbiAgICAgIHR5cGU6ICdhdG9tJyxcbiAgICAgIHZhbHVlOiBmbGFnXG4gICAgfVxuICB9KSlcblxuICByZXR1cm4gY29tbWFuZFxufVxuIl19 -------------------------------------------------------------------------------- /dist/common.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.fromTypedArray = exports.toTypedArray = exports.LOG_LEVEL_ALL = exports.LOG_LEVEL_DEBUG = exports.LOG_LEVEL_INFO = exports.LOG_LEVEL_WARN = exports.LOG_LEVEL_ERROR = exports.LOG_LEVEL_NONE = void 0; 7 | const LOG_LEVEL_NONE = 1000; 8 | exports.LOG_LEVEL_NONE = LOG_LEVEL_NONE; 9 | const LOG_LEVEL_ERROR = 40; 10 | exports.LOG_LEVEL_ERROR = LOG_LEVEL_ERROR; 11 | const LOG_LEVEL_WARN = 30; 12 | exports.LOG_LEVEL_WARN = LOG_LEVEL_WARN; 13 | const LOG_LEVEL_INFO = 20; 14 | exports.LOG_LEVEL_INFO = LOG_LEVEL_INFO; 15 | const LOG_LEVEL_DEBUG = 10; 16 | exports.LOG_LEVEL_DEBUG = LOG_LEVEL_DEBUG; 17 | const LOG_LEVEL_ALL = 0; 18 | exports.LOG_LEVEL_ALL = LOG_LEVEL_ALL; 19 | 20 | const toTypedArray = str => new Uint8Array(str.split('').map(char => char.charCodeAt(0))); 21 | 22 | exports.toTypedArray = toTypedArray; 23 | 24 | const fromTypedArray = arr => String.fromCharCode.apply(null, arr); 25 | 26 | exports.fromTypedArray = fromTypedArray; 27 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9jb21tb24uanMiXSwibmFtZXMiOlsiTE9HX0xFVkVMX05PTkUiLCJMT0dfTEVWRUxfRVJST1IiLCJMT0dfTEVWRUxfV0FSTiIsIkxPR19MRVZFTF9JTkZPIiwiTE9HX0xFVkVMX0RFQlVHIiwiTE9HX0xFVkVMX0FMTCIsInRvVHlwZWRBcnJheSIsInN0ciIsIlVpbnQ4QXJyYXkiLCJzcGxpdCIsIm1hcCIsImNoYXIiLCJjaGFyQ29kZUF0IiwiZnJvbVR5cGVkQXJyYXkiLCJhcnIiLCJTdHJpbmciLCJmcm9tQ2hhckNvZGUiLCJhcHBseSJdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBQU8sTUFBTUEsY0FBYyxHQUFHLElBQXZCOztBQUNBLE1BQU1DLGVBQWUsR0FBRyxFQUF4Qjs7QUFDQSxNQUFNQyxjQUFjLEdBQUcsRUFBdkI7O0FBQ0EsTUFBTUMsY0FBYyxHQUFHLEVBQXZCOztBQUNBLE1BQU1DLGVBQWUsR0FBRyxFQUF4Qjs7QUFDQSxNQUFNQyxhQUFhLEdBQUcsQ0FBdEI7OztBQUVBLE1BQU1DLFlBQVksR0FBR0MsR0FBRyxJQUFJLElBQUlDLFVBQUosQ0FBZUQsR0FBRyxDQUFDRSxLQUFKLENBQVUsRUFBVixFQUFjQyxHQUFkLENBQWtCQyxJQUFJLElBQUlBLElBQUksQ0FBQ0MsVUFBTCxDQUFnQixDQUFoQixDQUExQixDQUFmLENBQTVCOzs7O0FBQ0EsTUFBTUMsY0FBYyxHQUFHQyxHQUFHLElBQUlDLE1BQU0sQ0FBQ0MsWUFBUCxDQUFvQkMsS0FBcEIsQ0FBMEIsSUFBMUIsRUFBZ0NILEdBQWhDLENBQTlCIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGNvbnN0IExPR19MRVZFTF9OT05FID0gMTAwMFxuZXhwb3J0IGNvbnN0IExPR19MRVZFTF9FUlJPUiA9IDQwXG5leHBvcnQgY29uc3QgTE9HX0xFVkVMX1dBUk4gPSAzMFxuZXhwb3J0IGNvbnN0IExPR19MRVZFTF9JTkZPID0gMjBcbmV4cG9ydCBjb25zdCBMT0dfTEVWRUxfREVCVUcgPSAxMFxuZXhwb3J0IGNvbnN0IExPR19MRVZFTF9BTEwgPSAwXG5cbmV4cG9ydCBjb25zdCB0b1R5cGVkQXJyYXkgPSBzdHIgPT4gbmV3IFVpbnQ4QXJyYXkoc3RyLnNwbGl0KCcnKS5tYXAoY2hhciA9PiBjaGFyLmNoYXJDb2RlQXQoMCkpKVxuZXhwb3J0IGNvbnN0IGZyb21UeXBlZEFycmF5ID0gYXJyID0+IFN0cmluZy5mcm9tQ2hhckNvZGUuYXBwbHkobnVsbCwgYXJyKVxuIl19 -------------------------------------------------------------------------------- /dist/compression.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = Compressor; 7 | 8 | var _zstream = _interopRequireDefault(require("pako/lib/zlib/zstream")); 9 | 10 | var _deflate = require("pako/lib/zlib/deflate"); 11 | 12 | var _inflate = require("pako/lib/zlib/inflate"); 13 | 14 | var _messages = _interopRequireDefault(require("pako/lib/zlib/messages.js")); 15 | 16 | var _constants = require("pako/lib/zlib/constants"); 17 | 18 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 19 | 20 | const CHUNK_SIZE = 16384; 21 | const WINDOW_BITS = 15; 22 | /** 23 | * Handles de-/compression via #inflate() and #deflate(), calls you back via #deflatedReady() and #inflatedReady(). 24 | * The chunk we get from deflater is actually a view of a 16kB arraybuffer, so we need to copy the relevant parts 25 | * memory to a new arraybuffer. 26 | */ 27 | 28 | function Compressor(inflatedReady, deflatedReady) { 29 | this.inflatedReady = inflatedReady; 30 | this.deflatedReady = deflatedReady; 31 | this._inflate = inflater(chunk => this.inflatedReady(chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.length))); 32 | this._deflate = deflater(chunk => this.deflatedReady(chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.length))); 33 | } 34 | 35 | Compressor.prototype.inflate = function (buffer) { 36 | this._inflate(new Uint8Array(buffer)); 37 | }; 38 | 39 | Compressor.prototype.deflate = function (buffer) { 40 | this._deflate(new Uint8Array(buffer)); 41 | }; 42 | 43 | function deflater(emit) { 44 | const stream = new _zstream.default(); 45 | const status = (0, _deflate.deflateInit2)(stream, _constants.Z_DEFAULT_COMPRESSION, _constants.Z_DEFLATED, WINDOW_BITS, 8, _constants.Z_DEFAULT_STRATEGY); 46 | 47 | if (status !== _constants.Z_OK) { 48 | throw new Error('Problem initializing deflate stream: ' + _messages.default[status]); 49 | } 50 | 51 | return function (data) { 52 | if (data === undefined) return emit(); // Attach the input data 53 | 54 | stream.input = data; 55 | stream.next_in = 0; 56 | stream.avail_in = stream.input.length; 57 | let status; 58 | let output; 59 | let start; 60 | let ret = true; 61 | 62 | do { 63 | // When the stream gets full, we need to create new space. 64 | if (stream.avail_out === 0) { 65 | stream.output = new Uint8Array(CHUNK_SIZE); 66 | start = stream.next_out = 0; 67 | stream.avail_out = CHUNK_SIZE; 68 | } // Perform the deflate 69 | 70 | 71 | status = (0, _deflate.deflate)(stream, _constants.Z_SYNC_FLUSH); 72 | 73 | if (status !== _constants.Z_STREAM_END && status !== _constants.Z_OK) { 74 | throw new Error('Deflate problem: ' + _messages.default[status]); 75 | } // If the output buffer got full, flush the data. 76 | 77 | 78 | if (stream.avail_out === 0 && stream.next_out > start) { 79 | output = stream.output.subarray(start, start = stream.next_out); 80 | ret = emit(output); 81 | } 82 | } while ((stream.avail_in > 0 || stream.avail_out === 0) && status !== _constants.Z_STREAM_END); // Emit whatever is left in output. 83 | 84 | 85 | if (stream.next_out > start) { 86 | output = stream.output.subarray(start, start = stream.next_out); 87 | ret = emit(output); 88 | } 89 | 90 | return ret; 91 | }; 92 | } 93 | 94 | function inflater(emit) { 95 | const stream = new _zstream.default(); 96 | const status = (0, _inflate.inflateInit2)(stream, WINDOW_BITS); 97 | 98 | if (status !== _constants.Z_OK) { 99 | throw new Error('Problem initializing inflate stream: ' + _messages.default[status]); 100 | } 101 | 102 | return function (data) { 103 | if (data === undefined) return emit(); 104 | let start; 105 | stream.input = data; 106 | stream.next_in = 0; 107 | stream.avail_in = stream.input.length; 108 | let status, output; 109 | let ret = true; 110 | 111 | do { 112 | if (stream.avail_out === 0) { 113 | stream.output = new Uint8Array(CHUNK_SIZE); 114 | start = stream.next_out = 0; 115 | stream.avail_out = CHUNK_SIZE; 116 | } 117 | 118 | status = (0, _inflate.inflate)(stream, _constants.Z_NO_FLUSH); 119 | 120 | if (status !== _constants.Z_STREAM_END && status !== _constants.Z_OK) { 121 | throw new Error('inflate problem: ' + _messages.default[status]); 122 | } 123 | 124 | if (stream.next_out) { 125 | if (stream.avail_out === 0 || status === _constants.Z_STREAM_END) { 126 | output = stream.output.subarray(start, start = stream.next_out); 127 | ret = emit(output); 128 | } 129 | } 130 | } while (stream.avail_in > 0 && status !== _constants.Z_STREAM_END); 131 | 132 | if (stream.next_out > start) { 133 | output = stream.output.subarray(start, start = stream.next_out); 134 | ret = emit(output); 135 | } 136 | 137 | return ret; 138 | }; 139 | } 140 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9jb21wcmVzc2lvbi5qcyJdLCJuYW1lcyI6WyJDSFVOS19TSVpFIiwiV0lORE9XX0JJVFMiLCJDb21wcmVzc29yIiwiaW5mbGF0ZWRSZWFkeSIsImRlZmxhdGVkUmVhZHkiLCJfaW5mbGF0ZSIsImluZmxhdGVyIiwiY2h1bmsiLCJidWZmZXIiLCJzbGljZSIsImJ5dGVPZmZzZXQiLCJsZW5ndGgiLCJfZGVmbGF0ZSIsImRlZmxhdGVyIiwicHJvdG90eXBlIiwiaW5mbGF0ZSIsIlVpbnQ4QXJyYXkiLCJkZWZsYXRlIiwiZW1pdCIsInN0cmVhbSIsIlpTdHJlYW0iLCJzdGF0dXMiLCJaX0RFRkFVTFRfQ09NUFJFU1NJT04iLCJaX0RFRkxBVEVEIiwiWl9ERUZBVUxUX1NUUkFURUdZIiwiWl9PSyIsIkVycm9yIiwibWVzc2FnZXMiLCJkYXRhIiwidW5kZWZpbmVkIiwiaW5wdXQiLCJuZXh0X2luIiwiYXZhaWxfaW4iLCJvdXRwdXQiLCJzdGFydCIsInJldCIsImF2YWlsX291dCIsIm5leHRfb3V0IiwiWl9TWU5DX0ZMVVNIIiwiWl9TVFJFQU1fRU5EIiwic3ViYXJyYXkiLCJaX05PX0ZMVVNIIl0sIm1hcHBpbmdzIjoiOzs7Ozs7O0FBQUE7O0FBQ0E7O0FBQ0E7O0FBQ0E7O0FBQ0E7Ozs7QUFNQSxNQUFNQSxVQUFVLEdBQUcsS0FBbkI7QUFDQSxNQUFNQyxXQUFXLEdBQUcsRUFBcEI7QUFFQTs7Ozs7O0FBS2UsU0FBU0MsVUFBVCxDQUFxQkMsYUFBckIsRUFBb0NDLGFBQXBDLEVBQW1EO0FBQ2hFLE9BQUtELGFBQUwsR0FBcUJBLGFBQXJCO0FBQ0EsT0FBS0MsYUFBTCxHQUFxQkEsYUFBckI7QUFDQSxPQUFLQyxRQUFMLEdBQWdCQyxRQUFRLENBQUNDLEtBQUssSUFBSSxLQUFLSixhQUFMLENBQW1CSSxLQUFLLENBQUNDLE1BQU4sQ0FBYUMsS0FBYixDQUFtQkYsS0FBSyxDQUFDRyxVQUF6QixFQUFxQ0gsS0FBSyxDQUFDRyxVQUFOLEdBQW1CSCxL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OlsiaW1wb3J0IFpTdHJlYW0gZnJvbSAncGFrby9saWIvemxpYi96c3RyZWFtJ1xuaW1wb3J0IHsgZGVmbGF0ZUluaXQyLCBkZWZsYXRlIH0gZnJvbSAncGFrby9saWIvemxpYi9kZWZsYXRlJ1xuaW1wb3J0IHsgaW5mbGF0ZSwgaW5mbGF0ZUluaXQyIH0gZnJvbSAncGFrby9saWIvemxpYi9pbmZsYXRlJ1xuaW1wb3J0IG1lc3NhZ2VzIGZyb20gJ3Bha28vbGliL3psaWIvbWVzc2FnZXMuanMnXG5pbXBvcnQge1xuICBaX05PX0ZMVVNILCBaX1NZTkNfRkxVU0gsIFpfT0ssXG4gIFpfU1RSRUFNX0VORCwgWl9ERUZBVUxUX0NPTVBSRVNTSU9OLFxuICBaX0RFRkFVTFRfU1RSQVRFR1ksIFpfREVGTEFURURcbn0gZnJvbSAncGFrby9saWIvemxpYi9jb25zdGFudHMnXG5cbmNvbnN0IENIVU5LX1NJWkUgPSAxNjM4NFxuY29uc3QgV0lORE9XX0JJVFMgPSAxNVxuXG4vKipcbiAqIEhhbmRsZXMgZGUtL2NvbXByZXNzaW9uIHZpYSAjaW5mbGF0ZSgpIGFuZCAjZGVmbGF0ZSgpLCBjYWxscyB5b3UgYmFjayB2aWEgI2RlZmxhdGVkUmVhZHkoKSBhbmQgI2luZmxhdGVkUmVhZHkoKS5cbiAqIFRoZSBjaHVuayB3ZSBnZXQgZnJvbSBkZWZsYXRlciBpcyBhY3R1YWxseSBhIHZpZXcgb2YgYSAxNmtCIGFycmF5YnVmZmVyLCBzbyB3ZSBuZWVkIHRvIGNvcHkgdGhlIHJlbGV2YW50IHBhcnRzXG4gKiBtZW1vcnkgdG8gYSBuZXcgYXJyYXlidWZmZXIuXG4gKi9cbmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIENvbXByZXNzb3IgKGluZmxhdGVkUmVhZHksIGRlZmxhdGVkUmVhZHkpIHtcbiAgdGhpcy5pbmZsYXRlZFJlYWR5ID0gaW5mbGF0ZWRSZWFkeVxuICB0aGlzLmRlZmxhdGVkUmVhZHkgPSBkZWZsYXRlZFJlYWR5XG4gIHRoaXMuX2luZmxhdGUgPSBpbmZsYXRlcihjaHVuayA9PiB0aGlzLmluZmxhdGVkUmVhZHkoY2h1bmsuYnVmZmVyLnNsaWNlKGNodW5rLmJ5dGVPZmZzZXQsIGNodW5rLmJ5dGVPZmZzZXQgKyBjaHVuay5sZW5ndGgpKSlcbiAgdGhpcy5fZGVmbGF0ZSA9IGRlZmxhdGVyKGNodW5rID0+IHRoaXMuZGVmbGF0ZWRSZWFkeShjaHVuay5idWZmZXIuc2xpY2UoY2h1bmsuYnl0ZU9mZnNldCwgY2h1bmsuYnl0ZU9mZnNldCArIGNodW5rLmxlbmd0aCkpKVxufVxuXG5Db21wcmVzc29yLnByb3RvdHlwZS5pbmZsYXRlID0gZnVuY3Rpb24gKGJ1ZmZlcikge1xuICB0aGlzLl9pbmZsYXRlKG5ldyBVaW50OEFycmF5KGJ1ZmZlcikpXG59XG5cbkNvbXByZXNzb3IucHJvdG90eXBlLmRlZmxhdGUgPSBmdW5jdGlvbiAoYnVmZmVyKSB7XG4gIHRoaXMuX2RlZmxhdGUobmV3IFVpbnQ4QXJyYXkoYnVmZmVyKSlcbn1cblxuZnVuY3Rpb24gZGVmbGF0ZXIgKGVtaXQpIHtcbiAgY29uc3Qgc3RyZWFtID0gbmV3IFpTdHJlYW0oKVxuICBjb25zdCBzdGF0dXMgPSBkZWZsYXRlSW5pdDIoc3RyZWFtLCBaX0RFRkFVTFRfQ09NUFJFU1NJT04sIFpfREVGTEFURUQsIFdJTkRPV19CSVRTLCA4LCBaX0RFRkFVTFRfU1RSQVRFR1kpXG4gIGlmIChzdGF0dXMgIT09IFpfT0spIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoJ1Byb2JsZW0gaW5pdGlhbGl6aW5nIGRlZmxhdGUgc3RyZWFtOiAnICsgbWVzc2FnZXNbc3RhdHVzXSlcbiAgfVxuXG4gIHJldHVybiBmdW5jdGlvbiAoZGF0YSkge1xuICAgIGlmIChkYXRhID09PSB1bmRlZmluZWQpIHJldHVybiBlbWl0KClcblxuICAgIC8vIEF0dGFjaCB0aGUgaW5wdXQgZGF0YVxuICAgIHN0cmVhbS5pbnB1dCA9IGRhdGFcbiAgICBzdHJlYW0ubmV4dF9pbiA9IDBcbiAgICBzdHJlYW0uYXZhaWxfaW4gPSBzdHJlYW0uaW5wdXQubGVuZ3RoXG5cbiAgICBsZXQgc3RhdHVzXG4gICAgbGV0IG91dHB1dFxuICAgIGxldCBzdGFydFxuICAgIGxldCByZXQgPSB0cnVlXG5cbiAgICBkbyB7XG4gICAgICAvLyBXaGVuIHRoZSBzdHJlYW0gZ2V0cyBmdWxsLCB3ZSBuZWVkIHRvIGNyZWF0ZSBuZXcgc3BhY2UuXG4gICAgICBpZiAoc3RyZWFtLmF2YWlsX291dCA9PT0gMCkge1xuICAgICAgICBzdHJlYW0ub3V0cHV0ID0gbmV3IFVpbnQ4QXJyYXkoQ0hVTktfU0laRSlcbiAgICAgICAgc3RhcnQgPSBzdHJlYW0ubmV4dF9vdXQgPSAwXG4gICAgICAgIHN0cmVhbS5hdmFpbF9vdXQgPSBDSFVOS19TSVpFXG4gICAgICB9XG5cbiAgICAgIC8vIFBlcmZvcm0gdGhlIGRlZmxhdGVcbiAgICAgIHN0YXR1cyA9IGRlZmxhdGUoc3RyZWFtLCBaX1NZTkNfRkxVU0gpXG4gICAgICBpZiAoc3RhdHVzICE9PSBaX1NUUkVBTV9FTkQgJiYgc3RhdHVzICE9PSBaX09LKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcignRGVmbGF0ZSBwcm9ibGVtOiAnICsgbWVzc2FnZXNbc3RhdHVzXSlcbiAgICAgIH1cblxuICAgICAgLy8gSWYgdGhlIG91dHB1dCBidWZmZXIgZ290IGZ1bGwsIGZsdXNoIHRoZSBkYXRhLlxuICAgICAgaWYgKHN0cmVhbS5hdmFpbF9vdXQgPT09IDAgJiYgc3RyZWFtLm5leHRfb3V0ID4gc3RhcnQpIHtcbiAgICAgICAgb3V0cHV0ID0gc3RyZWFtLm91dHB1dC5zdWJhcnJheShzdGFydCwgc3RhcnQgPSBzdHJlYW0ubmV4dF9vdXQpXG4gICAgICAgIHJldCA9IGVtaXQob3V0cHV0KVxuICAgICAgfVxuICAgIH0gd2hpbGUgKChzdHJlYW0uYXZhaWxfaW4gPiAwIHx8IHN0cmVhbS5hdmFpbF9vdXQgPT09IDApICYmIHN0YXR1cyAhPT0gWl9TVFJFQU1fRU5EKVxuXG4gICAgLy8gRW1pdCB3aGF0ZXZlciBpcyBsZWZ0IGluIG91dHB1dC5cbiAgICBpZiAoc3RyZWFtLm5leHRfb3V0ID4gc3RhcnQpIHtcbiAgICAgIG91dHB1dCA9IHN0cmVhbS5vdXRwdXQuc3ViYXJyYXkoc3RhcnQsIHN0YXJ0ID0gc3RyZWFtLm5leHRfb3V0KVxuICAgICAgcmV0ID0gZW1pdChvdXRwdXQpXG4gICAgfVxuICAgIHJldHVybiByZXRcbiAgfVxufVxuXG5mdW5jdGlvbiBpbmZsYXRlciAoZW1pdCkge1xuICBjb25zdCBzdHJlYW0gPSBuZXcgWlN0cmVhbSgpXG5cbiAgY29uc3Qgc3RhdHVzID0gaW5mbGF0ZUluaXQyKHN0cmVhbSwgV0lORE9XX0JJVFMpXG4gIGlmIChzdGF0dXMgIT09IFpfT0spIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoJ1Byb2JsZW0gaW5pdGlhbGl6aW5nIGluZmxhdGUgc3RyZWFtOiAnICsgbWVzc2FnZXNbc3RhdHVzXSlcbiAgfVxuXG4gIHJldHVybiBmdW5jdGlvbiAoZGF0YSkge1xuICAgIGlmIChkYXRhID09PSB1bmRlZmluZWQpIHJldHVybiBlbWl0KClcblxuICAgIGxldCBzdGFydFxuICAgIHN0cmVhbS5pbnB1dCA9IGRhdGFcbiAgICBzdHJlYW0ubmV4dF9pbiA9IDBcbiAgICBzdHJlYW0uYXZhaWxfaW4gPSBzdHJlYW0uaW5wdXQubGVuZ3RoXG5cbiAgICBsZXQgc3RhdHVzLCBvdXRwdXRcbiAgICBsZXQgcmV0ID0gdHJ1ZVxuXG4gICAgZG8ge1xuICAgICAgaWYgKHN0cmVhbS5hdmFpbF9vdXQgPT09IDApIHtcbiAgICAgICAgc3RyZWFtLm91dHB1dCA9IG5ldyBVaW50OEFycmF5KENIVU5LX1NJWkUpXG4gICAgICAgIHN0YXJ0ID0gc3RyZWFtLm5leHRfb3V0ID0gMFxuICAgICAgICBzdHJlYW0uYXZhaWxfb3V0ID0gQ0hVTktfU0laRVxuICAgICAgfVxuXG4gICAgICBzdGF0dXMgPSBpbmZsYXRlKHN0cmVhbSwgWl9OT19GTFVTSClcbiAgICAgIGlmIChzdGF0dXMgIT09IFpfU1RSRUFNX0VORCAmJiBzdGF0dXMgIT09IFpfT0spIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKCdpbmZsYXRlIHByb2JsZW06ICcgKyBtZXNzYWdlc1tzdGF0dXNdKVxuICAgICAgfVxuXG4gICAgICBpZiAoc3RyZWFtLm5leHRfb3V0KSB7XG4gICAgICAgIGlmIChzdHJlYW0uYXZhaWxfb3V0ID09PSAwIHx8IHN0YXR1cyA9PT0gWl9TVFJFQU1fRU5EKSB7XG4gICAgICAgICAgb3V0cHV0ID0gc3RyZWFtLm91dHB1dC5zdWJhcnJheShzdGFydCwgc3RhcnQgPSBzdHJlYW0ubmV4dF9vdXQpXG4gICAgICAgICAgcmV0ID0gZW1pdChvdXRwdXQpXG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9IHdoaWxlICgoc3RyZWFtLmF2YWlsX2luID4gMCkgJiYgc3RhdHVzICE9PSBaX1NUUkVBTV9FTkQpXG5cbiAgICBpZiAoc3RyZWFtLm5leHRfb3V0ID4gc3RhcnQpIHtcbiAgICAgIG91dHB1dCA9IHN0cmVhbS5vdXRwdXQuc3ViYXJyYXkoc3RhcnQsIHN0YXJ0ID0gc3RyZWFtLm5leHRfb3V0KVxuICAgICAgcmV0ID0gZW1pdChvdXRwdXQpXG4gICAgfVxuXG4gICAgcmV0dXJuIHJldFxuICB9XG59XG4iXX0= -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | Object.defineProperty(exports, "LOG_LEVEL_NONE", { 7 | enumerable: true, 8 | get: function () { 9 | return _common.LOG_LEVEL_NONE; 10 | } 11 | }); 12 | Object.defineProperty(exports, "LOG_LEVEL_ERROR", { 13 | enumerable: true, 14 | get: function () { 15 | return _common.LOG_LEVEL_ERROR; 16 | } 17 | }); 18 | Object.defineProperty(exports, "LOG_LEVEL_WARN", { 19 | enumerable: true, 20 | get: function () { 21 | return _common.LOG_LEVEL_WARN; 22 | } 23 | }); 24 | Object.defineProperty(exports, "LOG_LEVEL_INFO", { 25 | enumerable: true, 26 | get: function () { 27 | return _common.LOG_LEVEL_INFO; 28 | } 29 | }); 30 | Object.defineProperty(exports, "LOG_LEVEL_DEBUG", { 31 | enumerable: true, 32 | get: function () { 33 | return _common.LOG_LEVEL_DEBUG; 34 | } 35 | }); 36 | Object.defineProperty(exports, "LOG_LEVEL_ALL", { 37 | enumerable: true, 38 | get: function () { 39 | return _common.LOG_LEVEL_ALL; 40 | } 41 | }); 42 | exports.default = void 0; 43 | 44 | var _client = _interopRequireDefault(require("./client")); 45 | 46 | var _common = require("./common"); 47 | 48 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 49 | 50 | var _default = _client.default; 51 | exports.default = _default; 52 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9pbmRleC5qcyJdLCJuYW1lcyI6WyJJbWFwQ2xpZW50Il0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBQUE7O0FBRUE7Ozs7ZUFTZUEsZSIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBJbWFwQ2xpZW50IGZyb20gJy4vY2xpZW50J1xuXG5leHBvcnQge1xuICBMT0dfTEVWRUxfTk9ORSxcbiAgTE9HX0xFVkVMX0VSUk9SLFxuICBMT0dfTEVWRUxfV0FSTixcbiAgTE9HX0xFVkVMX0lORk8sXG4gIExPR19MRVZFTF9ERUJVRyxcbiAgTE9HX0xFVkVMX0FMTFxufSBmcm9tICcuL2NvbW1vbidcblxuZXhwb3J0IGRlZmF1bHQgSW1hcENsaWVudFxuIl19 -------------------------------------------------------------------------------- /dist/logger.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = createDefaultLogger; 7 | 8 | var _common = require("./common"); 9 | 10 | let SESSIONCOUNTER = 0; 11 | 12 | function createDefaultLogger(username, hostname) { 13 | const session = ++SESSIONCOUNTER; 14 | 15 | const log = (level, messages) => { 16 | messages = messages.map(msg => typeof msg === 'function' ? msg() : msg); 17 | const date = new Date().toISOString(); 18 | const logMessage = `[${date}][${session}][${username}][${hostname}] ${messages.join(' ')}`; 19 | 20 | if (level === _common.LOG_LEVEL_DEBUG) { 21 | console.log('[DEBUG]' + logMessage); 22 | } else if (level === _common.LOG_LEVEL_INFO) { 23 | console.info('[INFO]' + logMessage); 24 | } else if (level === _common.LOG_LEVEL_WARN) { 25 | console.warn('[WARN]' + logMessage); 26 | } else if (level === _common.LOG_LEVEL_ERROR) { 27 | console.error('[ERROR]' + logMessage); 28 | } 29 | }; 30 | 31 | return { 32 | debug: msgs => log(_common.LOG_LEVEL_DEBUG, msgs), 33 | info: msgs => log(_common.LOG_LEVEL_INFO, msgs), 34 | warn: msgs => log(_common.LOG_LEVEL_WARN, msgs), 35 | error: msgs => log(_common.LOG_LEVEL_ERROR, msgs) 36 | }; 37 | } 38 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9sb2dnZXIuanMiXSwibmFtZXMiOlsiU0VTU0lPTkNPVU5URVIiLCJjcmVhdGVEZWZhdWx0TG9nZ2VyIiwidXNlcm5hbWUiLCJob3N0bmFtZSIsInNlc3Npb24iLCJsb2ciLCJsZXZlbCIsIm1lc3NhZ2VzIiwibWFwIiwibXNnIiwiZGF0ZSIsIkRhdGUiLCJ0b0lTT1N0cmluZyIsImxvZ01lc3NhZ2UiLCJqb2luIiwiTE9HX0xFVkVMX0RFQlVHIiwiY29uc29sZSIsIkxPR19MRVZFTF9JTkZPIiwiaW5mbyIsIkxPR19MRVZFTF9XQVJOIiwid2FybiIsIkxPR19MRVZFTF9FUlJPUiIsImVycm9yIiwiZGVidWciLCJtc2dzIl0sIm1hcHBpbmdzIjoiOzs7Ozs7O0FBQUE7O0FBT0EsSUFBSUEsY0FBYyxHQUFHLENBQXJCOztBQUVlLFNBQVNDLG1CQUFULENBQThCQyxRQUE5QixFQUF3Q0MsUUFBeEMsRUFBa0Q7QUFDL0QsUUFBTUMsT0FBTyxHQUFHLEVBQUVKLGNBQWxCOztBQUNBLFFBQU1LLEdBQUcsR0FBRyxDQUFDQyxLQUFELEVBQVFDLFFBQVIsS0FBcUI7QUFDL0JBLElBQUFBLFFBQVEsR0FBR0EsUUFBUSxDQUFDQyxHQUFULENBQWFDLEdBQUcsSUFBSSxPQUFPQSxHQUFQLEtBQWUsVUFBZixHQUE0QkEsR0FBRyxFQUEvQixHQUFvQ0EsR0FBeEQsQ0FBWDtBQUNBLFVBQU1DLElBQUksR0FBRyxJQUFJQyxJQUFKLEdBQVdDLFdBQVgsRUFBYjtBQUNBLFVBQU1DLFVBQVUsR0FBSSxJQUFHSCxJQUFLLEtBQUlOLE9BQVEsS0FBSUYsUUFBUyxLQUFJQyxRQUFTLEtBQUlJLFFBQVEsQ0FBQ08sSUFBVCxDQUFjLEdBQWQsQ0FBbUIsRUFBekY7O0FBQ0EsUUFBSVIsS0FBSyxLQUFLUyx1QkFBZCxFQUErQjtBQUM3QkMsTUFBQUEsT0FBTyxDQUFDWCxHQUFSLENBQVksWUFBWVEsVUFBeEI7QUFDRCxLQUZELE1BRU8sSUFBSVAsS0FBSyxLQUFLVyxzQkFBZCxFQUE4QjtBQUNuQ0QsTUFBQUEsT0FBTyxDQUFDRSxJQUFSLENBQWEsV0FBV0wsVUFBeEI7QUFDRCxLQUZNLE1BRUEsSUFBSVAsS0FBSyxLQUFLYSxzQkFBZCxFQUE4QjtBQUNuQ0gsTUFBQUEsT0FBTyxDQUFDSSxJQUFSLENBQWEsV0FBV1AsVUFBeEI7QUFDRCxLQUZNLE1BRUEsSUFBSVAsS0FBSyxLQUFLZSx1QkFBZCxFQUErQjtBQUNwQ0wsTUFBQUEsT0FBTyxDQUFDTSxLQUFSLENBQWMsWUFBWVQsVUFBMUI7QUFDRDtBQUNGLEdBYkQ7O0FBZUEsU0FBTztBQUNMVSxJQUFBQSxLQUFLLEVBQUVDLElBQUksSUFBSW5CLEdBQUcsQ0FBQ1UsdUJBQUQsRUFBa0JTLElBQWxCLENBRGI7QUFFTE4sSUFBQUEsSUFBSSxFQUFFTSxJQUFJLElBQUluQixHQUFHLENBQUNZLHNCQUFELEVBQWlCTyxJQUFqQixDQUZaO0FBR0xKLElBQUFBLElBQUksRUFBRUksSUFBSSxJQUFJbkIsR0FBRyxDQUFDYyxzQkFBRCxFQUFpQkssSUFBakIsQ0FIWjtBQUlMRixJQUFBQSxLQUFLLEVBQUVFLElBQUksSUFBSW5CLEdBQUcsQ0FBQ2dCLHVCQUFELEVBQWtCRyxJQUFsQjtBQUpiLEdBQVA7QUFNRCIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7XG4gIExPR19MRVZFTF9FUlJPUixcbiAgTE9HX0xFVkVMX1dBUk4sXG4gIExPR19MRVZFTF9JTkZPLFxuICBMT0dfTEVWRUxfREVCVUdcbn0gZnJvbSAnLi9jb21tb24nXG5cbmxldCBTRVNTSU9OQ09VTlRFUiA9IDBcblxuZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gY3JlYXRlRGVmYXVsdExvZ2dlciAodXNlcm5hbWUsIGhvc3RuYW1lKSB7XG4gIGNvbnN0IHNlc3Npb24gPSArK1NFU1NJT05DT1VOVEVSXG4gIGNvbnN0IGxvZyA9IChsZXZlbCwgbWVzc2FnZXMpID0+IHtcbiAgICBtZXNzYWdlcyA9IG1lc3NhZ2VzLm1hcChtc2cgPT4gdHlwZW9mIG1zZyA9PT0gJ2Z1bmN0aW9uJyA/IG1zZygpIDogbXNnKVxuICAgIGNvbnN0IGRhdGUgPSBuZXcgRGF0ZSgpLnRvSVNPU3RyaW5nKClcbiAgICBjb25zdCBsb2dNZXNzYWdlID0gYFske2RhdGV9XVske3Nlc3Npb259XVske3VzZXJuYW1lfV1bJHtob3N0bmFtZX1dICR7bWVzc2FnZXMuam9pbignICcpfWBcbiAgICBpZiAobGV2ZWwgPT09IExPR19MRVZFTF9ERUJVRykge1xuICAgICAgY29uc29sZS5sb2coJ1tERUJVR10nICsgbG9nTWVzc2FnZSlcbiAgICB9IGVsc2UgaWYgKGxldmVsID09PSBMT0dfTEVWRUxfSU5GTykge1xuICAgICAgY29uc29sZS5pbmZvKCdbSU5GT10nICsgbG9nTWVzc2FnZSlcbiAgICB9IGVsc2UgaWYgKGxldmVsID09PSBMT0dfTEVWRUxfV0FSTikge1xuICAgICAgY29uc29sZS53YXJuKCdbV0FSTl0nICsgbG9nTWVzc2FnZSlcbiAgICB9IGVsc2UgaWYgKGxldmVsID09PSBMT0dfTEVWRUxfRVJST1IpIHtcbiAgICAgIGNvbnNvbGUuZXJyb3IoJ1tFUlJPUl0nICsgbG9nTWVzc2FnZSlcbiAgICB9XG4gIH1cblxuICByZXR1cm4ge1xuICAgIGRlYnVnOiBtc2dzID0+IGxvZyhMT0dfTEVWRUxfREVCVUcsIG1zZ3MpLFxuICAgIGluZm86IG1zZ3MgPT4gbG9nKExPR19MRVZFTF9JTkZPLCBtc2dzKSxcbiAgICB3YXJuOiBtc2dzID0+IGxvZyhMT0dfTEVWRUxfV0FSTiwgbXNncyksXG4gICAgZXJyb3I6IG1zZ3MgPT4gbG9nKExPR19MRVZFTF9FUlJPUiwgbXNncylcbiAgfVxufVxuIl19 -------------------------------------------------------------------------------- /dist/special-use.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.checkSpecialUse = checkSpecialUse; 7 | 8 | var _ramda = require("ramda"); 9 | 10 | const SPECIAL_USE_FLAGS = ['\\All', '\\Archive', '\\Drafts', '\\Flagged', '\\Junk', '\\Sent', '\\Trash']; 11 | const SPECIAL_USE_BOXES = { 12 | '\\Sent': ['aika', 'bidaliak', 'bidalita', 'dihantar', 'e rometsweng', 'e tindami', 'elküldött', 'elküldöttek', 'enviadas', 'enviadas', 'enviados', 'enviats', 'envoyés', 'ethunyelweyo', 'expediate', 'ezipuru', 'gesendete', 'gestuur', 'gönderilmiş öğeler', 'göndərilənlər', 'iberilen', 'inviati', 'išsiųstieji', 'kuthunyelwe', 'lasa', 'lähetetyt', 'messages envoyés', 'naipadala', 'nalefa', 'napadala', 'nosūtītās ziņas', 'odeslané', 'padala', 'poslane', 'poslano', 'poslano', 'poslané', 'poslato', 'saadetud', 'saadetud kirjad', 'sendt', 'sendt', 'sent', 'sent items', 'sent messages', 'sända poster', 'sänt', 'terkirim', 'ti fi ranṣẹ', 'të dërguara', 'verzonden', 'vilivyotumwa', 'wysłane', 'đã gửi', 'σταλθέντα', 'жиберилген', 'жіберілгендер', 'изпратени', 'илгээсэн', 'ирсол шуд', 'испратено', 'надіслані', 'отправленные', 'пасланыя', 'юборилган', 'ուղարկված', 'נשלחו', 'פריטים שנשלחו', 'المرسلة', 'بھیجے گئے', 'سوزمژہ', 'لېګل شوی', 'موارد ارسال شده', 'पाठविले', 'पाठविलेले', 'प्रेषित', 'भेजा गया', 'প্রেরিত', 'প্রেরিত', 'প্ৰেৰিত', 'ਭੇਜੇ', 'મોકલેલા', 'ପଠାଗଲା', 'அனுப்பியவை', 'పంపించబడింది', 'ಕಳುಹಿಸಲಾದ', 'അയച്ചു', 'යැවු පණිවුඩ', 'ส่งแล้ว', 'გაგზავნილი', 'የተላኩ', 'បាន​ផ្ញើ', '寄件備份', '寄件備份', '已发信息', '送信済みメール', '발신 메시지', '보낸 편지함'], 13 | '\\Trash': ['articole șterse', 'bin', 'borttagna objekt', 'deleted', 'deleted items', 'deleted messages', 'elementi eliminati', 'elementos borrados', 'elementos eliminados', 'gelöschte objekte', 'item dipadam', 'itens apagados', 'itens excluídos', 'mục đã xóa', 'odstraněné položky', 'pesan terhapus', 'poistetut', 'praht', 'prügikast', 'silinmiş öğeler', 'slettede beskeder', 'slettede elementer', 'trash', 'törölt elemek', 'usunięte wiadomości', 'verwijderde items', 'vymazané správy', 'éléments supprimés', 'видалені', 'жойылғандар', 'удаленные', 'פריטים שנמחקו', 'العناصر المحذوفة', 'موارد حذف شده', 'รายการที่ลบ', '已删除邮件', '已刪除項目', '已刪除項目'], 14 | '\\Junk': ['bulk mail', 'correo no deseado', 'courrier indésirable', 'istenmeyen', 'istenmeyen e-posta', 'junk', 'levélszemét', 'nevyžiadaná pošta', 'nevyžádaná pošta', 'no deseado', 'posta indesiderata', 'pourriel', 'roskaposti', 'skräppost', 'spam', 'spam', 'spamowanie', 'søppelpost', 'thư rác', 'спам', 'דואר זבל', 'الرسائل العشوائية', 'هرزنامه', 'สแปม', '‎垃圾郵件', '垃圾邮件', '垃圾電郵'], 15 | '\\Drafts': ['ba brouillon', 'borrador', 'borrador', 'borradores', 'bozze', 'brouillons', 'bản thảo', 'ciorne', 'concepten', 'draf', 'drafts', 'drög', 'entwürfe', 'esborranys', 'garalamalar', 'ihe edeturu', 'iidrafti', 'izinhlaka', 'juodraščiai', 'kladd', 'kladder', 'koncepty', 'koncepty', 'konsep', 'konsepte', 'kopie robocze', 'layihələr', 'luonnokset', 'melnraksti', 'meralo', 'mesazhe të padërguara', 'mga draft', 'mustandid', 'nacrti', 'nacrti', 'osnutki', 'piszkozatok', 'rascunhos', 'rasimu', 'skice', 'taslaklar', 'tsararrun saƙonni', 'utkast', 'vakiraoka', 'vázlatok', 'zirriborroak', 'àwọn àkọpamọ́', 'πρόχειρα', 'жобалар', 'нацрти', 'нооргууд', 'сиёҳнавис', 'хомаки хатлар', 'чарнавікі', 'чернетки', 'чернови', 'черновики', 'черновиктер', 'սևագրեր', 'טיוטות', 'مسودات', 'مسودات', 'موسودې', 'پیش نویسها', 'ڈرافٹ/', 'ड्राफ़्ट', 'प्रारूप', 'খসড়া', 'খসড়া', 'ড্ৰাফ্ট', 'ਡ੍ਰਾਫਟ', 'ડ્રાફ્ટસ', 'ଡ୍ରାଫ୍ଟ', 'வரைவுகள்', 'చిత్తు ప్రతులు', 'ಕರಡುಗಳು', 'കരടുകള്‍', 'කෙටුම් පත්', 'ฉบับร่าง', 'მონახაზები', 'ረቂቆች', 'សារព្រាង', '下書き', '草稿', '草稿', '草稿', '임시 보관함'] 16 | }; 17 | const SPECIAL_USE_BOX_FLAGS = Object.keys(SPECIAL_USE_BOXES); 18 | /** 19 | * Checks if a mailbox is for special use 20 | * 21 | * @param {Object} mailbox 22 | * @return {String} Special use flag (if detected) 23 | */ 24 | 25 | function checkSpecialUse(mailbox) { 26 | if (mailbox.flags) { 27 | for (let i = 0; i < SPECIAL_USE_FLAGS.length; i++) { 28 | const type = SPECIAL_USE_FLAGS[i]; 29 | 30 | if ((mailbox.flags || []).indexOf(type) >= 0) { 31 | mailbox.specialUse = type; 32 | mailbox.specialUseFlag = type; 33 | return type; 34 | } 35 | } 36 | } 37 | 38 | return checkSpecialUseByName(mailbox); 39 | } 40 | 41 | function checkSpecialUseByName(mailbox) { 42 | const name = (0, _ramda.propOr)('', 'name', mailbox).toLowerCase().trim(); 43 | 44 | for (let i = 0; i < SPECIAL_USE_BOX_FLAGS.length; i++) { 45 | const type = SPECIAL_USE_BOX_FLAGS[i]; 46 | 47 | if (SPECIAL_USE_BOXES[type].indexOf(name) >= 0) { 48 | mailbox.specialUse = type; 49 | return type; 50 | } 51 | } 52 | 53 | return false; 54 | } 55 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9zcGVjaWFsLXVzZS5qcyJdLCJuYW1lcyI6WyJTUEVDSUFMX1VTRV9GTEFHUyIsIlNQRUNJQUxfVVNFX0JPWEVTIiwiU1BFQ0lBTF9VU0VfQk9YX0ZMQUdTIiwiT2JqZWN0Iiwia2V5cyIsImNoZWNrU3BlY2lhbFVzZSIsIm1haWxib3giLCJmbGFncyIsImkiLCJsZW5ndGgiLCJ0eXBlIiwiaW5kZXhPZiIsInNwZWNpYWxVc2UiLCJzcGVjaWFsVXNlRmxhZyIsImNoZWNrU3BlY2lhbFVzZUJ5TmFtZSIsIm5hbWUiLCJ0b0xvd2VyQ2FzZSIsInRyaW0iXSwibWFwcGluZ3MiOiI7Ozs7Ozs7QUFBQTs7QUFFQSxNQUFNQSxpQkFBaUIsR0FBRyxDQUFDLE9BQUQsRUFBVSxXQUFWLEVBQXVCLFVBQXZCLEVBQW1DLFdBQW5DLEVBQWdELFFBQWhELEVBQTBELFFBQTFELEVBQW9FLFNBQXBFLENBQTFCO0FBQ0EsTUFBTUMsaUJBQWlCLEdBQUc7QUFDeEIsWUFBVSxDQUNSLE1BRFEsRUFDQSxVQURBLEVBQ1ksVUFEWixFQUN3QixVQUR4QixFQUNvQyxjQURwQyxFQUNvRCxXQURwRCxFQUNpRSxXQURqRSxFQUM4RSxhQUQ5RSxFQUM2RixVQUQ3RixFQUVSLFVBRlEsRUFFSSxVQUZKLEVBRWdCLFNBRmhCLEVBRTJCLFNBRjNCLEVBRXNDLGNBRnRDLEVBRXNELFdBRnRELEVBRW1FLFNBRm5FLEVBRThFLFdBRjlFLEVBRTJGLFNBRjNGLEVBR1Isb0JBSFEsRUFHYyxlQUhkLEVBRytCLFVBSC9CLEVBRzJDLFNBSDNDLEVBR3NELGFBSHRELEVBR3FFLGFBSHJFLEVBR29GLE1BSHBGLEVBRzRGLFdBSDVGLEVBSVIsa0JBSlEsRUFJWSxXQUpaLEVBSXlCLFFBSnpCLEVBSW1DLFVBSm5DLEVBSStDLGlCQUovQyxFQUlrRSxVQUpsRSxFQUk4RSxRQUo5RSxFQUl3RixTQUp4RixFQUtSLFNBTFEsRUFLRyxTQUxILEVBS2MsU0FMZCxFQUt5QixTQUx6QixFQUtvQyxVQUxw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Q0FBQ1MsTUFBdEMsRUFBOENELENBQUMsRUFBL0MsRUFBbUQ7QUFDakQsWUFBTUUsSUFBSSxHQUFHVixpQkFBaUIsQ0FBQ1EsQ0FBRCxDQUE5Qjs7QUFDQSxVQUFJLENBQUNGLE9BQU8sQ0FBQ0MsS0FBUixJQUFpQixFQUFsQixFQUFzQkksT0FBdEIsQ0FBOEJELElBQTlCLEtBQXVDLENBQTNDLEVBQThDO0FBQzVDSixRQUFBQSxPQUFPLENBQUNNLFVBQVIsR0FBcUJGLElBQXJCO0FBQ0FKLFFBQUFBLE9BQU8sQ0FBQ08sY0FBUixHQUF5QkgsSUFBekI7QUFDQSxlQUFPQSxJQUFQO0FBQ0Q7QUFDRjtBQUNGOztBQUVELFNBQU9JLHFCQUFxQixDQUFDUixPQUFELENBQTVCO0FBQ0Q7O0FBRUQsU0FBU1EscUJBQVQsQ0FBZ0NSLE9BQWhDLEVBQXlDO0FBQ3ZDLFFBQU1TLElBQUksR0FBRyxtQkFBTyxFQUFQLEVBQVcsTUFBWCxFQUFtQlQsT0FBbkIsRUFBNEJVLFdBQTVCLEdBQTBDQyxJQUExQyxFQUFiOztBQUVBLE9BQUssSUFBSVQsQ0FBQyxHQUFHLENBQWIsRUFBZ0JBLENBQUMsR0FBR04scUJBQXFCLENBQUNPLE1BQTFDLEVBQWtERCxDQUFDLEVBQW5ELEVBQXVEO0FBQ3JELFVBQU1FLElBQUksR0FBR1IscUJBQXFCLENBQUNNLENBQUQsQ0FBbEM7O0FBQ0EsUUFBSVAsaUJBQWlCLENBQUNTLElBQUQsQ0FBakIsQ0FBd0JDLE9BQXhCLENBQWdDSSxJQUFoQyxLQUF5QyxDQUE3QyxFQUFnRDtBQUM5Q1QsTUFBQUEsT0FBTyxDQUFDTSxVQUFSLEdBQXFCRixJQUFyQjtBQUNBLGFBQU9BLElBQVA7QUFDRDtBQUNGOztBQUVELFNBQU8sS0FBUDtBQUNEIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgcHJvcE9yIH0gZnJvbSAncmFtZGEnXG5cbmNvbnN0IFNQRUNJQUxfVVNFX0ZMQUdTID0gWydcXFxcQWxsJywgJ1xcXFxBcmNoaXZlJywgJ1xcXFxEcmFmdHMnLCAnXFxcXEZsYWdnZWQnLCAnXFxcXEp1bmsnLCAnXFxcXFNlbnQnLCAnXFxcXFRyYXNoJ11cbmNvbnN0IFNQRUNJQUxfVVNFX0JPWEVTID0ge1xuICAnXFxcXFNlbnQnOiBbXG4gICAgJ2Fpa2EnLCAnYmlkYWxpYWsnLCAnYmlkYWxpdGEnLCAnZGloYW50YXInLCAnZSByb21ldHN3ZW5nJywgJ2UgdGluZGFtaScsICdlbGvDvGxkw7Z0dCcsICdlbGvDvGxkw7Z0dGVrJywgJ2VudmlhZGFzJyxcbiAgICAnZW52aWFkYXMnLCAnZW52aWFkb3MnLCAnZW52aWF0cycsICdlbnZvecOpcycsICdldGh1bnllbHdleW8nLCAnZXhwZWRpYXRlJywgJ2V6aXB1cnUnLCAnZ2VzZW5kZXRlJywgJ2dlc3R1dXInLFxuICAgICdnw7ZuZGVyaWxtacWfIMO2xJ9lbGVyJywgJ2fDtm5kyZlyaWzJmW5syZlyJywgJ2liZXJpbGVuJywgJ2ludmlhdGknLCAnacWhc2nFs3N0aWVqaScsICdrdXRodW55ZWx3ZScsICdsYXNhJywgJ2zDpGhldGV0eXQnLFxuICAgICdtZXNzYWdlcyBlbnZvecOpcycsICduYWlwYWRhbGEnLCAnbmFsZWZhJywgJ25hcGFkYWxhJywgJ25vc8WrdMSrdMSBcyB6acWGYXMnLCAnb2Rlc2xhbsOpJywgJ3BhZGFsYScsICdwb3NsYW5lJyxcbiAgICAncG9zbGFubycsICdwb3NsYW5vJywgJ3Bvc2xhbsOpJywgJ3Bvc2xhdG8nLCAnc2FhZGV0dWQnLCAnc2FhZGV0dWQga2lyamFkJywgJ3NlbmR0JywgJ3NlbmR0JywgJ3NlbnQnLCAnc2VudCBpdGVtcycsXG4gICAgJ3NlbnQgbWVzc2FnZXMnLCAnc8OkbmRhIHBvc3RlcicsICdzw6RudCcsICd0ZXJraXJpbScsICd0aSBmaSByYW7huaPhurknLCAndMOrIGTDq3JndWFyYScsICd2ZXJ6b25kZW4nLCAndmlsaXZ5b3R1bXdhJyxcbiAgICAnd3lzxYJhbmUnLCAnxJHDoyBn4butaScsICfPg8+EzrHOu864zq3Ovc+EzrEnLCAn0LbQuNCx0LXRgNC40LvQs9C10L0nLCAn0LbRltCx0LXRgNGW0LvQs9C10L3QtNC10YAnLCAn0LjQt9C/0YDQsNGC0LXQvdC4JywgJ9C40LvQs9GN0Y3RgdGN0L0nLCAn0LjRgNGB0L7QuyDRiNGD0LQnLCAn0LjRgdC/0YDQsNGC0LXQvdC+JyxcbiAgICAn0L3QsNC00ZbRgdC70LDQvdGWJywgJ9C+0YLQv9GA0LDQstC70LXQvdC90YvQtScsICfQv9Cw0YHQu9Cw0L3Ri9GPJywgJ9GO0LHQvtGA0LjQu9Cz0LDQvScsICfVuNaC1bLVodaA1a/VvtWh1a4nLCAn16DXqdec15fXlScsICfXpNeo15nXmNeZ150g16nXoNep15zXl9eVJywgJ9in2YTZhdix2LPZhNipJywgJ9io2r7bjNis25Ig2q/YptuSJyxcbiAgICAn2LPZiNiy2YXamNuBJywgJ9mE25Daq9mEINi02YjbjCcsICfZhdmI2KfYsdivINin2LHYs9in2YQg2LTYr9mHJywgJ+CkquCkvuCkoOCkteCkv+CksuClhycsICfgpKrgpL7gpKDgpLXgpL/gpLLgpYfgpLLgpYcnLCAn4KSq4KWN4KSw4KWH4KS34KS/4KSkJywgJ+CkreClh+CknOCkviDgpJfgpK/gpL4nLCAn4Kaq4KeN4Kaw4KeH4Kaw4Ka/4KakJywgJ+CmquCnjeCmsOCnh+CmsOCmv+CmpCcsICfgpqrgp43gp7Dgp4fgp7Dgpr/gpqQnLCAn4Kit4KmH4Kic4KmHJywgJ+CqruCri+CqleCqsuCrh+CqsuCqvicsXG4gICAgJ+CsquCsoOCsvuCsl+CssuCsvicsICfgroXgrqngr4Hgrqrgr43grqrgrr/grq/grrXgr4gnLCAn4LCq4LCC4LCq4LC/4LCC4LCa4LCs4LCh4LC/4LCC4LCm4LC/JywgJ+CyleCys+CzgeCyueCyv+CyuOCysuCyvuCypicsICfgtIXgtK/gtJrgtY3gtJrgtYEnLCAn4La64LeQ4LeA4LeUIOC2tOC2q+C3kuC3gOC3lOC2qScsICfguKrguYjguIfguYHguKXguYnguKcnLCAn4YOS4YOQ4YOS4YOW4YOQ4YOV4YOc4YOY4YOa4YOYJywgJ+GLqOGJsOGIi+GKqScsICfhnpThnrbhnpPigIvhnpXhn5Lhnonhnr4nLFxuICAgICflr4Tku7blgpnku70nLCAn5a+E5Lu25YKZ5Lu9JywgJ+W3suWPkeS/oeaBrycsICfpgIHkv6HmuIjjgb/vvpLvvbDvvpknLCAn67Cc7IugIOuplOyLnOyngCcsICfrs7Trgrgg7Y647KeA7ZWoJ1xuICBdLFxuICAnXFxcXFRyYXNoJzogW1xuICAgICdhcnRpY29sZSDImXRlcnNlJywgJ2JpbicsICdib3J0dGFnbmEgb2JqZWt0JywgJ2RlbGV0ZWQnLCAnZGVsZXRlZCBpdGVtcycsICdkZWxldGVkIG1lc3NhZ2VzJywgJ2VsZW1lbnRpIGVsaW1pbmF0aScsXG4gICAgJ2VsZW1lbnRvcyBib3JyYWRvcycsICdlbGVtZW50b3MgZWxpbWluYWRvcycsICdnZWzDtnNjaHRlIG9iamVrdGUnLCAnaXRlbSBkaXBhZGFtJywgJ2l0ZW5zIGFwYWdhZG9zJywgJ2l0ZW5zIGV4Y2x1w61kb3MnLFxuICAgICdt4bulYyDEkcOjIHjDs2EnLCAnb2RzdHJhbsSbbsOpIHBvbG/Fvmt5JywgJ3Blc2FuIHRlcmhhcHVzJywgJ3BvaXN0ZXR1dCcsICdwcmFodCcsICdwcsO8Z2lrYXN0JywgJ3NpbGlubWnFnyDDtsSfZWxlcicsXG4gICAgJ3NsZXR0ZWRlIGJlc2tlZGVyJywgJ3NsZXR0ZWRlIGVsZW1lbnRlcicsICd0cmFzaCcsICd0w7Zyw7ZsdCBlbGVtZWsnLCAndXN1bmnEmXRlIHdpYWRvbW/Fm2NpJywgJ3ZlcndpamRlcmRlIGl0ZW1zJyxcbiAgICAndnltYXphbsOpIHNwcsOhdnknLCAnw6lsw6ltZW50cyBzdXBwcmltw6lzJywgJ9Cy0LjQtNCw0LvQtdC90ZYnLCAn0LbQvtC50YvQu9KT0LDQvdC00LDRgCcsICfRg9C00LDQu9C10L3QvdGL0LUnLCAn16TXqNeZ15jXmdedINep16DXnteX16fXlScsICfYp9mE2LnZhtin2LXYsSDYp9mE2YXYrdiw2YjZgdipJyxcbiAgICAn2YXZiNin2LHYryDYrdiw2YEg2LTYr9mHJywgJ+C4o+C4suC4ouC4geC4suC4o+C4l+C4teC5iOC4peC4micsICflt7LliKDpmaTpgq7ku7YnLCAn5bey5Yiq6Zmk6aCF55uuJywgJ+W3suWIqumZpOmgheebridcbiAgXSxcbiAgJ1xcXFxKdW5rJzogW1xuICAgICdidWxrIG1haWwnLCAnY29ycmVvIG5vIGRlc2VhZG8nLCAnY291cnJpZXIgaW5kw6lzaXJhYmxlJywgJ2lzdGVubWV5ZW4nLCAnaXN0ZW5tZXllbiBlLXBvc3RhJywgJ2p1bmsnLCAnbGV2w6lsc3plbcOpdCcsXG4gICAgJ25ldnnFvmlhZGFuw6EgcG/FoXRhJywgJ25ldnnFvsOhZGFuw6EgcG/FoXRhJywgJ25vIGRlc2VhZG8nLCAncG9zdGEgaW5kZXNpZGVyYXRhJywgJ3BvdXJyaWVsJywgJ3Jvc2thcG9zdGknLCAnc2tyw6RwcG9zdCcsXG4gICAgJ3NwYW0nLCAnc3BhbScsICdzcGFtb3dhbmllJywgJ3PDuHBwZWxwb3N0JywgJ3RoxrAgcsOhYycsICfRgdC/0LDQvCcsICfXk9eV15DXqCDXlteR15wnLCAn2KfZhNix2LPYp9im2YQg2KfZhNi52LTZiNin2KbZitipJywgJ9mH2LHYstmG2KfZhdmHJywgJ+C4quC5geC4m+C4oScsXG4gICAgJ+KAjuWeg+WcvumDteS7ticsICflnoPlnL7pgq7ku7YnLCAn5Z6D5Zy+6Zu76YO1J1xuICBdLFxuICAnXFxcXERyYWZ0cyc6IFtcbiAgICAnYmEgYnJvdWlsbG9uJywgJ2JvcnJhZG9yJywgJ2JvcnJhZG9yJywgJ2JvcnJhZG9yZXMnLCAnYm96emUnLCAnYnJvdWlsbG9ucycsICdi4bqjbiB0aOG6o28nLCAnY2lvcm5lJywgJ2NvbmNlcHRlbicsICdkcmFmJyxcbiAgICAnZHJhZnRzJywgJ2Ryw7ZnJywgJ2VudHfDvHJmZScsICdlc2JvcnJhbnlzJywgJ2dhcmFsYW1hbGFyJywgJ2loZSBlZGV0dXJ1JywgJ2lpZHJhZnRpJywgJ2l6aW5obGFrYScsICdqdW9kcmHFocSNaWFpJywgJ2tsYWRkJyxcbiAgICAna2xhZGRlcicsICdrb25jZXB0eScsICdrb25jZXB0eScsICdrb25zZXAnLCAna29uc2VwdGUnLCAna29waWUgcm9ib2N6ZScsICdsYXlpaMmZbMmZcicsICdsdW9ubm9rc2V0JywgJ21lbG5yYWtzdGknLCAnbWVyYWxvJyxcbiAgICAnbWVzYXpoZSB0w6sgcGFkw6tyZ3VhcmEnLCAnbWdhIGRyYWZ0JywgJ211c3RhbmRpZCcsICduYWNydGknLCAnbmFjcnRpJywgJ29zbnV0a2knLCAncGlzemtvemF0b2snLCAncmFzY3VuaG9zJywgJ3Jhc2ltdScsXG4gICAgJ3NraWNlJywgJ3Rhc2xha2xhcicsICd0c2FyYXJydW4gc2HGmW9ubmknLCAndXRrYXN0JywgJ3Zha2lyYW9rYScsICd2w6F6bGF0b2snLCAnemlycmlib3Jyb2FrJywgJ8Ogd+G7jW4gw6Br4buNcGFt4buNzIEnLCAnz4DPgc+Mz4fOtc65z4HOsScsXG4gICAgJ9C20L7QsdCw0LvQsNGAJywgJ9C90LDRhtGA0YLQuCcsICfQvdC+0L7RgNCz0YPRg9C0JywgJ9GB0LjRkdKz0L3QsNCy0LjRgScsICfRhdC+0LzQsNC60Lgg0YXQsNGC0LvQsNGAJywgJ9GH0LDRgNC90LDQstGW0LrRlicsICfRh9C10YDQvdC10YLQutC4JywgJ9GH0LXRgNC90L7QstC4JywgJ9GH0LXRgNC90L7QstC40LrQuCcsICfRh9C10YDQvdC+0LLQuNC60YLQtdGAJyxcbiAgICAn1b3Wh9Wh1aPWgNWl1oAnLCAn15jXmdeV15jXldeqJywgJ9mF2LPZiNiv2KfYqicsICfZhdiz2YjYr9in2KonLCAn2YXZiNiz2YjYr9uQJywgJ9m+24zYtCDZhtmI24zYs9mH2KcnLCAn2ojYsdin2YHZuS8nLCAn4KSh4KWN4KSw4KS+4KWe4KWN4KSfJywgJ+CkquCljeCksOCkvuCksOClguCkqicsICfgppbgprjgp5zgpr4nLCAn4KaW4Ka44Kec4Ka+JywgJ+CmoeCnjeCnsOCmvuCmq+CnjeCmnycsICfgqKHgqY3gqLDgqL7gqKvgqJ8nLCAn4Kqh4KuN4Kqw4Kq+4Kqr4KuN4Kqf4Kq4JyxcbiAgICAn4Kyh4K2N4Kyw4Ky+4Kyr4K2N4KyfJywgJ+CuteCusOCviOCuteCvgeCuleCus+CvjScsICfgsJrgsL/gsKTgsY3gsKTgsYEg4LCq4LGN4LCw4LCk4LGB4LCy4LGBJywgJ+CyleCysOCyoeCzgeCyl+Cys+CzgScsICfgtJXgtLDgtJ/gtYHgtJXgtLPgtY3igI0nLCAn4Laa4LeZ4Lan4LeU4La44LeKIOC2tOC2reC3iicsICfguInguJrguLHguJrguKPguYjguLLguIcnLCAn4YOb4YOd4YOc4YOQ4YOu4YOQ4YOW4YOU4YOR4YOYJywgJ+GIqOGJguGJhuGJvScsICfhnp/hnrbhnprhnpbhn5LhnprhnrbhnoQnLCAn5LiL5pu444GNJywgJ+iNieeovycsXG4gICAgJ+iNieeovycsICfojYnnqL8nLCAn7J6E7IucIOuztOq0gO2VqCdcbiAgXVxufVxuY29uc3QgU1BFQ0lBTF9VU0VfQk9YX0ZMQUdTID0gT2JqZWN0LmtleXMoU1BFQ0lBTF9VU0VfQk9YRVMpXG5cbi8qKlxuICogQ2hlY2tzIGlmIGEgbWFpbGJveCBpcyBmb3Igc3BlY2lhbCB1c2VcbiAqXG4gKiBAcGFyYW0ge09iamVjdH0gbWFpbGJveFxuICogQHJldHVybiB7U3RyaW5nfSBTcGVjaWFsIHVzZSBmbGFnIChpZiBkZXRlY3RlZClcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGNoZWNrU3BlY2lhbFVzZSAobWFpbGJveCkge1xuICBpZiAobWFpbGJveC5mbGFncykge1xuICAgIGZvciAobGV0IGkgPSAwOyBpIDwgU1BFQ0lBTF9VU0VfRkxBR1MubGVuZ3RoOyBpKyspIHtcbiAgICAgIGNvbnN0IHR5cGUgPSBTUEVDSUFMX1VTRV9GTEFHU1tpXVxuICAgICAgaWYgKChtYWlsYm94LmZsYWdzIHx8IFtdKS5pbmRleE9mKHR5cGUpID49IDApIHtcbiAgICAgICAgbWFpbGJveC5zcGVjaWFsVXNlID0gdHlwZVxuICAgICAgICBtYWlsYm94LnNwZWNpYWxVc2VGbGFnID0gdHlwZVxuICAgICAgICByZXR1cm4gdHlwZVxuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIHJldHVybiBjaGVja1NwZWNpYWxVc2VCeU5hbWUobWFpbGJveClcbn1cblxuZnVuY3Rpb24gY2hlY2tTcGVjaWFsVXNlQnlOYW1lIChtYWlsYm94KSB7XG4gIGNvbnN0IG5hbWUgPSBwcm9wT3IoJycsICduYW1lJywgbWFpbGJveCkudG9Mb3dlckNhc2UoKS50cmltKClcblxuICBmb3IgKGxldCBpID0gMDsgaSA8IFNQRUNJQUxfVVNFX0JPWF9GTEFHUy5sZW5ndGg7IGkrKykge1xuICAgIGNvbnN0IHR5cGUgPSBTUEVDSUFMX1VTRV9CT1hfRkxBR1NbaV1cbiAgICBpZiAoU1BFQ0lBTF9VU0VfQk9YRVNbdHlwZV0uaW5kZXhPZihuYW1lKSA+PSAwKSB7XG4gICAgICBtYWlsYm94LnNwZWNpYWxVc2UgPSB0eXBlXG4gICAgICByZXR1cm4gdHlwZVxuICAgIH1cbiAgfVxuXG4gIHJldHVybiBmYWxzZVxufVxuIl19 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emailjs-imap-client", 3 | "version": "3.1.0", 4 | "homepage": "https://github.com/emailjs/emailjs-imap-client", 5 | "description": "JavaScript IMAP client", 6 | "author": "Andris Reinman ", 7 | "keywords": [ 8 | "IMAP" 9 | ], 10 | "license": "MIT", 11 | "scripts": { 12 | "build": "./scripts/build.sh", 13 | "lint": "npx standard", 14 | "preversion": "npm run build", 15 | "test": "npm run lint && npm run unit && npm run integration", 16 | "unit": "npx mocha './src/*-unit.js' --reporter spec --require @babel/register testutils.js", 17 | "integration": "npx mocha './src/*-integration.js' --reporter spec --require @babel/register testutils.js", 18 | "build-worker": "./scripts/worker.sh" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git://github.com/emailjs/emailjs-imap-client.git" 23 | }, 24 | "main": "dist/index", 25 | "dependencies": { 26 | "emailjs-addressparser": "^2.0.2", 27 | "emailjs-base64": "^1.1.4", 28 | "emailjs-imap-handler": "^3.0.2", 29 | "emailjs-mime-codec": "^2.0.8", 30 | "emailjs-tcp-socket": "^2.0.2", 31 | "emailjs-utf7": "^4.0.1", 32 | "pako": "^1.0.10", 33 | "ramda": "^0.26.1" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.8.4", 37 | "@babel/preset-env": "^7.8.4", 38 | "@babel/register": "^7.8.3", 39 | "babel-loader": "^8.0.6", 40 | "babel-plugin-inline-import": "^3.0.0", 41 | "chai": "^4.2.0", 42 | "hoodiecrow-imap": "^2.1.0", 43 | "mocha": "^7.0.1", 44 | "pre-commit": "^1.2.2", 45 | "sinon": "^8.0.0", 46 | "standard": "^13.0.1", 47 | "webpack": "^4.33.0", 48 | "webpack-cli": "^3.3.3" 49 | }, 50 | "standard": { 51 | "globals": [ 52 | "describe", 53 | "it", 54 | "before", 55 | "beforeEach", 56 | "afterEach", 57 | "after", 58 | "expect", 59 | "sinon", 60 | "self", 61 | "Worker", 62 | "URL", 63 | "Blob" 64 | ], 65 | "ignore": [ 66 | "dist", 67 | "res" 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /res/fixtures/envelope.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | source: [{ 5 | value: '17-Jul-1996 02:44:25 -0700' 6 | }, { 7 | value: '=?utf-8?b?w7XDpMO2w7w=?=' 8 | }, 9 | [ 10 | [{ 11 | value: '=?utf-8?b?w7XDpMO2w7w=?= 1' 12 | }, 13 | null, { 14 | value: 'from.1' 15 | }, { 16 | value: 'host' 17 | } 18 | ], 19 | [{ 20 | value: '=?utf-8?b?w7XDpMO2w7w=?= 2' 21 | }, 22 | null, { 23 | value: 'from.2' 24 | }, { 25 | value: 'host' 26 | } 27 | ] 28 | ], 29 | [ 30 | [{ 31 | value: '=?utf-8?b?w7XDpMO2w7w=?= 1' 32 | }, 33 | null, { 34 | value: 'sender.1' 35 | }, { 36 | value: 'host' 37 | } 38 | ], 39 | [{ 40 | value: '=?utf-8?b?w7XDpMO2w7w=?= 2' 41 | }, 42 | null, { 43 | value: 'sender.2' 44 | }, { 45 | value: 'host' 46 | } 47 | ] 48 | ], 49 | [ 50 | [{ 51 | value: '=?utf-8?b?w7XDpMO2w7w=?= 1' 52 | }, 53 | null, { 54 | value: 'reply.to.1' 55 | }, { 56 | value: 'host' 57 | } 58 | ], 59 | [{ 60 | value: '=?utf-8?b?w7XDpMO2w7w=?= 2' 61 | }, 62 | null, { 63 | value: 'reply.to.2' 64 | }, { 65 | value: 'host' 66 | } 67 | ], 68 | [{ 69 | value: '' 70 | }, 71 | null, { 72 | value: '"evil@attacker.com"' 73 | }, { 74 | value: 'victim.com' 75 | } 76 | ], 77 | [{ 78 | value: 'Last, First' 79 | }, 80 | null, { 81 | value: 'first.last' 82 | }, { 83 | value: 'example.com' 84 | } 85 | ] 86 | ], 87 | [ 88 | [{ 89 | value: '=?utf-8?b?w7XDpMO2w7w=?= 1' 90 | }, 91 | null, { 92 | value: 'to.1' 93 | }, { 94 | value: 'host' 95 | } 96 | ], 97 | [{ 98 | value: '=?utf-8?b?w7XDpMO2w7w=?= 2' 99 | }, 100 | null, { 101 | value: 'to.2' 102 | }, { 103 | value: 'host' 104 | } 105 | ] 106 | ], 107 | [ 108 | [{ 109 | value: '=?utf-8?b?w7XDpMO2w7w=?= 1' 110 | }, 111 | null, { 112 | value: 'cc.1' 113 | }, { 114 | value: 'host' 115 | } 116 | ], 117 | [{ 118 | value: '=?utf-8?b?w7XDpMO2w7w=?= 2' 119 | }, 120 | null, { 121 | value: 'cc.2' 122 | }, { 123 | value: 'host' 124 | } 125 | ] 126 | ], 127 | [ 128 | [{ 129 | value: '=?utf-8?b?w7XDpMO2w7w=?= 1' 130 | }, 131 | null, { 132 | value: 'bcc.1' 133 | }, { 134 | value: 'host' 135 | } 136 | ], 137 | [{ 138 | value: '=?utf-8?b?w7XDpMO2w7w=?= 2' 139 | }, 140 | null, { 141 | value: 'bcc.2' 142 | }, { 143 | value: 'host' 144 | } 145 | ] 146 | ], { 147 | value: 'replyid' 148 | }, { 149 | value: 'msgid' 150 | } 151 | ], 152 | parsed: { 153 | date: '17-Jul-1996 02:44:25 -0700', 154 | subject: 'õäöü', 155 | from: [{ 156 | name: 'õäöü 1', 157 | address: 'from.1@host' 158 | }, { 159 | name: 'õäöü 2', 160 | address: 'from.2@host' 161 | }], 162 | sender: [{ 163 | name: 'õäöü 1', 164 | address: 'sender.1@host' 165 | }, { 166 | name: 'õäöü 2', 167 | address: 'sender.2@host' 168 | }], 169 | 'reply-to': [{ 170 | name: 'õäöü 1', 171 | address: 'reply.to.1@host' 172 | }, { 173 | name: 'õäöü 2', 174 | address: 'reply.to.2@host' 175 | }, { 176 | name: '@victim.com', 177 | address: 'evil@attacker.com' 178 | }, { 179 | name: 'Last, First', 180 | address: 'first.last@example.com' 181 | }], 182 | to: [{ 183 | name: 'õäöü 1', 184 | address: 'to.1@host' 185 | }, { 186 | name: 'õäöü 2', 187 | address: 'to.2@host' 188 | }], 189 | cc: [{ 190 | name: 'õäöü 1', 191 | address: 'cc.1@host' 192 | }, { 193 | name: 'õäöü 2', 194 | address: 'cc.2@host' 195 | }], 196 | bcc: [{ 197 | name: 'õäöü 1', 198 | address: 'bcc.1@host' 199 | }, { 200 | name: 'õäöü 2', 201 | address: 'bcc.2@host' 202 | }], 203 | 'in-reply-to': 'replyid', 204 | 'message-id': 'msgid' 205 | } 206 | }; 207 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm run build-worker 4 | rm -rf $PWD/dist 5 | babel src --out-dir dist --ignore '**/*-unit.js','**/*-integration.js','**/*-worker.js' --source-maps inline 6 | -------------------------------------------------------------------------------- /scripts/worker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -f $PWD/res/compression.worker.blob 4 | webpack -p 5 | mv $PWD/res/compression.worker.js $PWD/res/compression.worker.blob 6 | -------------------------------------------------------------------------------- /src/client-integration.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | 3 | import hoodiecrow from 'hoodiecrow-imap' 4 | import ImapClient, { LOG_LEVEL_NONE as logLevel } from '../src/index' 5 | import { parseSEARCH } from './command-parser' 6 | import { buildSEARCHCommand } from './command-builder' 7 | 8 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' 9 | 10 | describe('browserbox integration tests', () => { 11 | let imap 12 | const port = 10000 13 | let server 14 | 15 | beforeEach((done) => { 16 | // start imap test server 17 | var options = { 18 | // debug: true, 19 | plugins: ['STARTTLS', 'X-GM-EXT-1'], 20 | secureConnection: false, 21 | storage: { 22 | INBOX: { 23 | messages: [ 24 | { raw: 'Subject: hello 1\r\n\r\nWorld 1!' }, 25 | { raw: 'Subject: hello 2\r\n\r\nWorld 2!', flags: ['\\Seen'] }, 26 | { raw: 'Subject: hello 3\r\n\r\nWorld 3!', uid: 555 }, 27 | { raw: 'From: sender name \r\nTo: Receiver name \r\nSubject: hello 4\r\nMessage-Id: \r\nDate: Fri, 13 Sep 2013 15:01:00 +0300\r\n\r\nWorld 4!' }, 28 | { raw: 'Subject: hello 5\r\n\r\nWorld 5!', flags: ['$MyFlag', '\\Deleted'], uid: 557 }, 29 | { raw: 'Subject: hello 6\r\n\r\nWorld 6!' }, 30 | { raw: 'Subject: hello 7\r\n\r\nWorld 7!', uid: 600 } 31 | ] 32 | }, 33 | '': { 34 | separator: '/', 35 | folders: { 36 | '[Gmail]': { 37 | flags: ['\\Noselect'], 38 | folders: { 39 | 'All Mail': { 'special-use': '\\All' }, 40 | Drafts: { 'special-use': '\\Drafts' }, 41 | Important: { 'special-use': '\\Important' }, 42 | 'Sent Mail': { 'special-use': '\\Sent' }, 43 | Spam: { 'special-use': '\\Junk' }, 44 | Starred: { 'special-use': '\\Flagged' }, 45 | Trash: { 'special-use': '\\Trash' }, 46 | A: { messages: [{}] }, 47 | B: { messages: [{}] } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | server = hoodiecrow(options) 56 | server.listen(port, done) 57 | }) 58 | 59 | afterEach((done) => { 60 | server.close(done) 61 | }) 62 | 63 | describe('Connection tests', () => { 64 | var insecureServer 65 | 66 | beforeEach((done) => { 67 | // start imap test server 68 | var options = { 69 | // debug: true, 70 | plugins: [], 71 | secureConnection: false 72 | } 73 | 74 | insecureServer = hoodiecrow(options) 75 | insecureServer.listen(port + 2, done) 76 | }) 77 | 78 | afterEach((done) => { 79 | insecureServer.close(done) 80 | }) 81 | 82 | it('should use STARTTLS by default', () => { 83 | imap = new ImapClient('127.0.0.1', port, { 84 | logLevel, 85 | auth: { 86 | user: 'testuser', 87 | pass: 'testpass' 88 | }, 89 | useSecureTransport: false 90 | }) 91 | 92 | return imap.connect().then(() => { 93 | expect(imap.client.secureMode).to.be.true 94 | }).then(() => { 95 | return imap.close() 96 | }) 97 | }) 98 | 99 | it('should ignore STARTTLS', () => { 100 | imap = new ImapClient('127.0.0.1', port, { 101 | logLevel, 102 | auth: { 103 | user: 'testuser', 104 | pass: 'testpass' 105 | }, 106 | useSecureTransport: false, 107 | ignoreTLS: true 108 | }) 109 | 110 | return imap.connect().then(() => { 111 | expect(imap.client.secureMode).to.be.false 112 | }).then(() => { 113 | return imap.close() 114 | }) 115 | }) 116 | 117 | it('should fail connecting to non-STARTTLS host', () => { 118 | imap = new ImapClient('127.0.0.1', port + 2, { 119 | logLevel, 120 | auth: { 121 | user: 'testuser', 122 | pass: 'testpass' 123 | }, 124 | useSecureTransport: false, 125 | requireTLS: true 126 | }) 127 | 128 | return imap.connect().catch((err) => { 129 | expect(err).to.exist 130 | }) 131 | }) 132 | 133 | it('should connect to non secure host', () => { 134 | imap = new ImapClient('127.0.0.1', port + 2, { 135 | logLevel, 136 | auth: { 137 | user: 'testuser', 138 | pass: 'testpass' 139 | }, 140 | useSecureTransport: false 141 | }) 142 | 143 | return imap.connect().then(() => { 144 | expect(imap.client.secureMode).to.be.false 145 | }).then(() => { 146 | return imap.close() 147 | }) 148 | }) 149 | 150 | it('should fail authentication', (done) => { 151 | imap = new ImapClient('127.0.0.1', port + 2, { 152 | logLevel, 153 | auth: { 154 | user: 'invalid', 155 | pass: 'invalid' 156 | }, 157 | useSecureTransport: false 158 | }) 159 | 160 | imap.connect().then(() => { 161 | expect(imap.client.secureMode).to.be.false 162 | }).catch(() => { done() }) 163 | }) 164 | }) 165 | 166 | describe('Post login tests', () => { 167 | beforeEach(() => { 168 | imap = new ImapClient('127.0.0.1', port, { 169 | logLevel, 170 | auth: { 171 | user: 'testuser', 172 | pass: 'testpass' 173 | }, 174 | useSecureTransport: false 175 | }) 176 | 177 | return imap.connect().then(() => { 178 | return imap.selectMailbox('[Gmail]/Spam') 179 | }) 180 | }) 181 | 182 | afterEach(() => { 183 | return imap.close() 184 | }) 185 | 186 | describe('#listMailboxes', () => { 187 | it('should succeed', () => { 188 | return imap.listMailboxes().then((mailboxes) => { 189 | expect(mailboxes).to.exist 190 | }) 191 | }) 192 | }) 193 | 194 | describe('#listMessages', () => { 195 | it('should succeed', () => { 196 | return imap.listMessages('inbox', '1:*', ['uid', 'flags', 'envelope', 'bodystructure', 'body.peek[]']).then((messages) => { 197 | expect(messages).to.not.be.empty 198 | }) 199 | }) 200 | }) 201 | 202 | describe('#subscribe', () => { 203 | it('should succeed', () => { 204 | return imap.subscribeMailbox('inbox').then(response => { 205 | expect(response.command).to.equal('OK') 206 | }) 207 | }) 208 | }) 209 | 210 | describe('#unsubscribe', () => { 211 | it('should succeed', () => { 212 | return imap.unsubscribeMailbox('inbox').then(response => { 213 | expect(response.command).to.equal('OK') 214 | }) 215 | }) 216 | }) 217 | 218 | describe('#upload', () => { 219 | it('should succeed', () => { 220 | var msgCount 221 | 222 | return imap.listMessages('inbox', '1:*', ['uid', 'flags', 'envelope', 'bodystructure']).then((messages) => { 223 | expect(messages).to.not.be.empty 224 | msgCount = messages.length 225 | }).then(() => { 226 | return imap.upload('inbox', 'MIME-Version: 1.0\r\nDate: Wed, 9 Jul 2014 15:07:47 +0200\r\nDelivered-To: test@test.com\r\nMessage-ID: \r\nSubject: test\r\nFrom: Test Test \r\nTo: Test Test \r\nContent-Type: text/plain; charset=UTF-8\r\n\r\ntest', { 227 | flags: ['\\Seen', '\\Answered', '\\$MyFlag'] 228 | }) 229 | }).then(() => { 230 | return imap.listMessages('inbox', '1:*', ['uid', 'flags', 'envelope', 'bodystructure']) 231 | }).then((messages) => { 232 | expect(messages.length).to.equal(msgCount + 1) 233 | }) 234 | }) 235 | }) 236 | 237 | describe('#search', () => { 238 | it('should return a sequence number', () => { 239 | return imap.search('inbox', { 240 | header: ['subject', 'hello 3'] 241 | }).then((result) => { 242 | expect(result).to.deep.equal([3]) 243 | }) 244 | }) 245 | 246 | it('should return an uid', () => { 247 | return imap.search('inbox', { 248 | header: ['subject', 'hello 3'] 249 | }, { 250 | byUid: true 251 | }).then((result) => { 252 | expect(result).to.deep.equal([555]) 253 | }) 254 | }) 255 | 256 | it('should work with complex queries', () => { 257 | return imap.search('inbox', { 258 | header: ['subject', 'hello'], 259 | seen: true 260 | }).then((result) => { 261 | expect(result).to.deep.equal([2]) 262 | }) 263 | }) 264 | }) 265 | 266 | describe('#setFlags', () => { 267 | it('should set flags for a message', () => { 268 | return imap.setFlags('inbox', '1', ['\\Seen', '$MyFlag']).then((result) => { 269 | expect(result).to.deep.equal([{ 270 | '#': 1, 271 | flags: ['\\Seen', '$MyFlag'] 272 | }]) 273 | }) 274 | }) 275 | 276 | it('should add flags to a message', () => { 277 | return imap.setFlags('inbox', '2', { 278 | add: ['$MyFlag'] 279 | }).then((result) => { 280 | expect(result).to.deep.equal([{ 281 | '#': 2, 282 | flags: ['\\Seen', '$MyFlag'] 283 | }]) 284 | }) 285 | }) 286 | 287 | it('should remove flags from a message', () => { 288 | return imap.setFlags('inbox', '557', { 289 | remove: ['\\Deleted'] 290 | }, { 291 | byUid: true 292 | }).then((result) => { 293 | expect(result).to.deep.equal([{ 294 | '#': 5, 295 | flags: ['$MyFlag'], 296 | uid: 557 297 | }]) 298 | }) 299 | }) 300 | 301 | it('should not return anything on silent mode', () => { 302 | return imap.setFlags('inbox', '1', ['$MyFlag2'], { 303 | silent: true 304 | }).then((result) => { 305 | expect(result).to.deep.equal([]) 306 | }) 307 | }) 308 | }) 309 | 310 | describe('#store', () => { 311 | it('should add labels for a message', () => { 312 | return imap.store('inbox', '1', '+X-GM-LABELS', ['\\Sent', '\\Junk']).then((result) => { 313 | expect(result).to.deep.equal([{ 314 | '#': 1, 315 | 'x-gm-labels': ['\\Inbox', '\\Sent', '\\Junk'] 316 | }]) 317 | }) 318 | }) 319 | 320 | it('should set labels for a message', () => { 321 | return imap.store('inbox', '1', 'X-GM-LABELS', ['\\Sent', '\\Junk']).then((result) => { 322 | expect(result).to.deep.equal([{ 323 | '#': 1, 324 | 'x-gm-labels': ['\\Sent', '\\Junk'] 325 | }]) 326 | }) 327 | }) 328 | 329 | it('should remove labels from a message', () => { 330 | return imap.store('inbox', '1', '-X-GM-LABELS', ['\\Sent', '\\Inbox']).then((result) => { 331 | expect(result).to.deep.equal([{ 332 | '#': 1, 333 | 'x-gm-labels': [] 334 | }]) 335 | }) 336 | }) 337 | }) 338 | 339 | describe('#deleteMessages', () => { 340 | it('should delete a message', () => { 341 | var initialInfo 342 | 343 | var expungeNotified = new Promise((resolve, reject) => { 344 | imap.onupdate = function (mb, type /*, data */) { 345 | try { 346 | expect(mb).to.equal('inbox') 347 | expect(type).to.equal('expunge') 348 | resolve() 349 | } catch (err) { 350 | reject(err) 351 | } 352 | } 353 | }) 354 | 355 | return imap.selectMailbox('inbox').then((info) => { 356 | initialInfo = info 357 | return imap.deleteMessages('inbox', 557, { 358 | byUid: true 359 | }) 360 | }).then(() => { 361 | return imap.selectMailbox('inbox') 362 | }).then((resultInfo) => { 363 | expect(initialInfo.exists - 1 === resultInfo.exists).to.be.true 364 | }).then(() => expungeNotified) 365 | }) 366 | }) 367 | 368 | describe('#copyMessages', () => { 369 | it('should copy a message', () => { 370 | return imap.copyMessages('inbox', 555, '[Gmail]/Trash', { 371 | byUid: true 372 | }).then(() => { 373 | return imap.selectMailbox('[Gmail]/Trash') 374 | }).then((info) => { 375 | expect(info.exists).to.equal(1) 376 | }) 377 | }) 378 | }) 379 | 380 | describe('#moveMessages', () => { 381 | it('should move a message', () => { 382 | var initialInfo 383 | return imap.selectMailbox('inbox').then((info) => { 384 | initialInfo = info 385 | return imap.moveMessages('inbox', 555, '[Gmail]/Spam', { 386 | byUid: true 387 | }) 388 | }).then(() => { 389 | return imap.selectMailbox('[Gmail]/Spam') 390 | }).then((info) => { 391 | expect(info.exists).to.equal(1) 392 | return imap.selectMailbox('inbox') 393 | }).then((resultInfo) => { 394 | expect(initialInfo.exists).to.not.equal(resultInfo.exists) 395 | }) 396 | }) 397 | }) 398 | 399 | describe('precheck', () => { 400 | it('should handle precheck error correctly', () => { 401 | // simulates a broken search command 402 | var search = (query, options = {}) => { 403 | var command = buildSEARCHCommand(query, options) 404 | return imap.exec(command, 'SEARCH', { 405 | precheck: () => Promise.reject(new Error('FOO')) 406 | }).then((response) => parseSEARCH(response)) 407 | } 408 | 409 | return imap.selectMailbox('inbox') 410 | .then(() => search({ header: ['subject', 'hello 3'] })) 411 | .catch((err) => { 412 | expect(err.message).to.equal('FOO') 413 | return imap.selectMailbox('[Gmail]/Spam') 414 | }) 415 | }) 416 | 417 | it('should select correct mailboxes in prechecks on concurrent calls', () => { 418 | return imap.selectMailbox('[Gmail]/A').then(() => { 419 | return Promise.all([ 420 | imap.selectMailbox('[Gmail]/B'), 421 | imap.setFlags('[Gmail]/A', '1', ['\\Seen']) 422 | ]) 423 | }).then(() => { 424 | return imap.listMessages('[Gmail]/A', '1:1', ['flags']) 425 | }).then((messages) => { 426 | expect(messages.length).to.equal(1) 427 | expect(messages[0].flags).to.deep.equal(['\\Seen']) 428 | }) 429 | }) 430 | 431 | it('should send precheck commands in correct order on concurrent calls', () => { 432 | return Promise.all([ 433 | imap.setFlags('[Gmail]/A', '1', ['\\Seen']), 434 | imap.setFlags('[Gmail]/B', '1', ['\\Seen']) 435 | ]).then(() => { 436 | return imap.listMessages('[Gmail]/A', '1:1', ['flags']) 437 | }).then((messages) => { 438 | expect(messages.length).to.equal(1) 439 | expect(messages[0].flags).to.deep.equal(['\\Seen']) 440 | }).then(() => { 441 | return imap.listMessages('[Gmail]/B', '1:1', ['flags']) 442 | }).then((messages) => { 443 | expect(messages.length).to.equal(1) 444 | expect(messages[0].flags).to.deep.equal(['\\Seen']) 445 | }) 446 | }) 447 | }) 448 | }) 449 | 450 | describe('Timeout', () => { 451 | beforeEach(() => { 452 | imap = new ImapClient('127.0.0.1', port, { 453 | logLevel, 454 | auth: { 455 | user: 'testuser', 456 | pass: 'testpass' 457 | }, 458 | useSecureTransport: false 459 | }) 460 | 461 | return imap.connect() 462 | .then(() => { 463 | // remove the ondata event to simulate 100% packet loss and make the socket time out after 10ms 464 | imap.client.timeoutSocketLowerBound = 10 465 | imap.client.timeoutSocketMultiplier = 0 466 | imap.client.socket.ondata = () => { } 467 | }) 468 | }) 469 | 470 | it('should timeout', (done) => { 471 | imap.onerror = () => { done() } 472 | imap.selectMailbox('inbox').catch(() => {}) 473 | }) 474 | 475 | it('should reject all pending commands on timeout', () => { 476 | let rejectionCount = 0 477 | return Promise.all([ 478 | imap.selectMailbox('INBOX') 479 | .catch(err => { 480 | expect(err).to.exist 481 | rejectionCount++ 482 | }), 483 | imap.listMessages('INBOX', '1:*', ['body.peek[]']) 484 | .catch(err => { 485 | expect(err).to.exist 486 | rejectionCount++ 487 | }) 488 | 489 | ]).then(() => { 490 | expect(rejectionCount).to.equal(2) 491 | }) 492 | }) 493 | }) 494 | }) 495 | -------------------------------------------------------------------------------- /src/command-builder-unit.js: -------------------------------------------------------------------------------- 1 | import { 2 | buildSTORECommand, 3 | buildFETCHCommand, 4 | buildXOAuth2Token, 5 | buildSEARCHCommand 6 | } from './command-builder' 7 | 8 | describe('buildFETCHCommand', () => { 9 | it('should build single ALL', () => { 10 | expect(buildFETCHCommand('1:*', ['all'], {})).to.deep.equal({ 11 | command: 'FETCH', 12 | attributes: [{ 13 | type: 'SEQUENCE', 14 | value: '1:*' 15 | }, { 16 | type: 'ATOM', 17 | value: 'ALL' 18 | }] 19 | }) 20 | }) 21 | 22 | it('should build FETCH with uid', () => { 23 | expect(buildFETCHCommand('1:*', ['all'], { 24 | byUid: true 25 | })).to.deep.equal({ 26 | command: 'UID FETCH', 27 | attributes: [{ 28 | type: 'SEQUENCE', 29 | value: '1:*' 30 | }, { 31 | type: 'ATOM', 32 | value: 'ALL' 33 | }] 34 | }) 35 | }) 36 | 37 | it('should build FETCH with uid, envelope', () => { 38 | expect(buildFETCHCommand('1:*', ['uid', 'envelope'], {})).to.deep.equal({ 39 | command: 'FETCH', 40 | attributes: [{ 41 | type: 'SEQUENCE', 42 | value: '1:*' 43 | }, 44 | [{ 45 | type: 'ATOM', 46 | value: 'UID' 47 | }, { 48 | type: 'ATOM', 49 | value: 'ENVELOPE' 50 | }] 51 | ] 52 | }) 53 | }) 54 | 55 | it('should build FETCH with modseq', () => { 56 | expect(buildFETCHCommand('1:*', ['modseq (1234567)'], {})).to.deep.equal({ 57 | command: 'FETCH', 58 | attributes: [{ 59 | type: 'SEQUENCE', 60 | value: '1:*' 61 | }, 62 | [{ 63 | type: 'ATOM', 64 | value: 'MODSEQ' 65 | }, 66 | [{ 67 | type: 'ATOM', 68 | value: '1234567' 69 | }] 70 | ] 71 | ] 72 | }) 73 | }) 74 | 75 | it('should build FETCH with section', () => { 76 | expect(buildFETCHCommand('1:*', ['body[text]'], {})).to.deep.equal({ 77 | command: 'FETCH', 78 | attributes: [{ 79 | type: 'SEQUENCE', 80 | value: '1:*' 81 | }, { 82 | type: 'ATOM', 83 | value: 'BODY', 84 | section: [{ 85 | type: 'ATOM', 86 | value: 'TEXT' 87 | }] 88 | }] 89 | }) 90 | }) 91 | 92 | it('should build FETCH with section and list', () => { 93 | expect(buildFETCHCommand('1:*', ['body[header.fields (date in-reply-to)]'], {})).to.deep.equal({ 94 | command: 'FETCH', 95 | attributes: [{ 96 | type: 'SEQUENCE', 97 | value: '1:*' 98 | }, { 99 | type: 'ATOM', 100 | value: 'BODY', 101 | section: [{ 102 | type: 'ATOM', 103 | value: 'HEADER.FIELDS' 104 | }, 105 | [{ 106 | type: 'ATOM', 107 | value: 'DATE' 108 | }, { 109 | type: 'ATOM', 110 | value: 'IN-REPLY-TO' 111 | }] 112 | ] 113 | }] 114 | }) 115 | }) 116 | 117 | it('should build FETCH with ', () => { 118 | expect(buildFETCHCommand('1:*', ['all'], { 119 | changedSince: '123456' 120 | })).to.deep.equal({ 121 | command: 'FETCH', 122 | attributes: [{ 123 | type: 'SEQUENCE', 124 | value: '1:*' 125 | }, { 126 | type: 'ATOM', 127 | value: 'ALL' 128 | }, 129 | [{ 130 | type: 'ATOM', 131 | value: 'CHANGEDSINCE' 132 | }, { 133 | type: 'ATOM', 134 | value: '123456' 135 | }] 136 | ] 137 | }) 138 | }) 139 | 140 | it('should build FETCH with partial', () => { 141 | expect(buildFETCHCommand('1:*', ['body[]'], {})).to.deep.equal({ 142 | command: 'FETCH', 143 | attributes: [{ 144 | type: 'SEQUENCE', 145 | value: '1:*' 146 | }, { 147 | type: 'ATOM', 148 | value: 'BODY', 149 | section: [] 150 | }] 151 | }) 152 | }) 153 | 154 | it('should build FETCH with the valueAsString option', () => { 155 | expect(buildFETCHCommand('1:*', ['body[]'], { valueAsString: false })).to.deep.equal({ 156 | command: 'FETCH', 157 | attributes: [{ 158 | type: 'SEQUENCE', 159 | value: '1:*' 160 | }, { 161 | type: 'ATOM', 162 | value: 'BODY', 163 | section: [] 164 | }], 165 | valueAsString: false 166 | }) 167 | }) 168 | }) 169 | 170 | describe('#_buildXOAuth2Token', () => { 171 | it('should return base64 encoded XOAUTH2 token', () => { 172 | expect(buildXOAuth2Token('user@host', 'abcde')).to.equal('dXNlcj11c2VyQGhvc3QBYXV0aD1CZWFyZXIgYWJjZGUBAQ==') 173 | }) 174 | }) 175 | 176 | describe('buildSEARCHCommand', () => { 177 | it('should compose a search command', () => { 178 | expect(buildSEARCHCommand({ 179 | unseen: true, 180 | header: ['subject', 'hello world'], 181 | or: { 182 | unseen: true, 183 | seen: true 184 | }, 185 | not: { 186 | seen: true 187 | }, 188 | sentbefore: new Date(2011, 1, 3, 12, 0, 0), 189 | since: new Date(2011, 11, 23, 12, 0, 0), 190 | uid: '1:*', 191 | 'X-GM-MSGID': '1499257647490662970', 192 | 'X-GM-THRID': '1499257647490662971' 193 | }, {})).to.deep.equal({ 194 | command: 'SEARCH', 195 | attributes: [{ 196 | type: 'atom', 197 | value: 'UNSEEN' 198 | }, { 199 | type: 'atom', 200 | value: 'HEADER' 201 | }, { 202 | type: 'string', 203 | value: 'subject' 204 | }, { 205 | type: 'string', 206 | value: 'hello world' 207 | }, { 208 | type: 'atom', 209 | value: 'OR' 210 | }, { 211 | type: 'atom', 212 | value: 'UNSEEN' 213 | }, { 214 | type: 'atom', 215 | value: 'SEEN' 216 | }, { 217 | type: 'atom', 218 | value: 'NOT' 219 | }, { 220 | type: 'atom', 221 | value: 'SEEN' 222 | }, { 223 | type: 'atom', 224 | value: 'SENTBEFORE' 225 | }, { 226 | type: 'atom', 227 | value: '3-Feb-2011' 228 | }, { 229 | type: 'atom', 230 | value: 'SINCE' 231 | }, { 232 | type: 'atom', 233 | value: '23-Dec-2011' 234 | }, { 235 | type: 'atom', 236 | value: 'UID' 237 | }, { 238 | type: 'sequence', 239 | value: '1:*' 240 | }, { 241 | type: 'atom', 242 | value: 'X-GM-MSGID' 243 | }, { 244 | type: 'number', 245 | value: '1499257647490662970' 246 | }, { 247 | type: 'atom', 248 | value: 'X-GM-THRID' 249 | }, { 250 | type: 'number', 251 | value: '1499257647490662971' 252 | }] 253 | }) 254 | }) 255 | 256 | it('should compose an unicode search command', () => { 257 | expect(buildSEARCHCommand({ 258 | body: 'jõgeva' 259 | }, {})).to.deep.equal({ 260 | command: 'SEARCH', 261 | attributes: [{ 262 | type: 'atom', 263 | value: 'CHARSET' 264 | }, { 265 | type: 'atom', 266 | value: 'UTF-8' 267 | }, { 268 | type: 'atom', 269 | value: 'BODY' 270 | }, { 271 | type: 'literal', 272 | value: 'jõgeva' 273 | }] 274 | }) 275 | }) 276 | }) 277 | 278 | describe('#_buildSTORECommand', () => { 279 | it('should compose a store command from an array', () => { 280 | expect(buildSTORECommand('1,2,3', 'FLAGS', ['a', 'b'], {})).to.deep.equal({ 281 | command: 'STORE', 282 | attributes: [{ 283 | type: 'sequence', 284 | value: '1,2,3' 285 | }, { 286 | type: 'atom', 287 | value: 'FLAGS' 288 | }, 289 | [{ 290 | type: 'atom', 291 | value: 'a' 292 | }, { 293 | type: 'atom', 294 | value: 'b' 295 | }] 296 | ] 297 | }) 298 | }) 299 | 300 | it('should compose a store set flags command', () => { 301 | expect(buildSTORECommand('1,2,3', 'FLAGS', ['a', 'b'], {})).to.deep.equal({ 302 | command: 'STORE', 303 | attributes: [{ 304 | type: 'sequence', 305 | value: '1,2,3' 306 | }, { 307 | type: 'atom', 308 | value: 'FLAGS' 309 | }, 310 | [{ 311 | type: 'atom', 312 | value: 'a' 313 | }, { 314 | type: 'atom', 315 | value: 'b' 316 | }] 317 | ] 318 | }) 319 | }) 320 | 321 | it('should compose a store add flags command', () => { 322 | expect(buildSTORECommand('1,2,3', '+FLAGS', ['a', 'b'], {})).to.deep.equal({ 323 | command: 'STORE', 324 | attributes: [{ 325 | type: 'sequence', 326 | value: '1,2,3' 327 | }, { 328 | type: 'atom', 329 | value: '+FLAGS' 330 | }, 331 | [{ 332 | type: 'atom', 333 | value: 'a' 334 | }, { 335 | type: 'atom', 336 | value: 'b' 337 | }] 338 | ] 339 | }) 340 | }) 341 | 342 | it('should compose a store remove flags command', () => { 343 | expect(buildSTORECommand('1,2,3', '-FLAGS', ['a', 'b'], {})).to.deep.equal({ 344 | command: 'STORE', 345 | attributes: [{ 346 | type: 'sequence', 347 | value: '1,2,3' 348 | }, { 349 | type: 'atom', 350 | value: '-FLAGS' 351 | }, 352 | [{ 353 | type: 'atom', 354 | value: 'a' 355 | }, { 356 | type: 'atom', 357 | value: 'b' 358 | }] 359 | ] 360 | }) 361 | }) 362 | 363 | it('should compose a store remove silent flags command', () => { 364 | expect(buildSTORECommand('1,2,3', '-FLAGS', ['a', 'b'], { 365 | silent: true 366 | })).to.deep.equal({ 367 | command: 'STORE', 368 | attributes: [{ 369 | type: 'sequence', 370 | value: '1,2,3' 371 | }, { 372 | type: 'atom', 373 | value: '-FLAGS.SILENT' 374 | }, 375 | [{ 376 | type: 'atom', 377 | value: 'a' 378 | }, { 379 | type: 'atom', 380 | value: 'b' 381 | }] 382 | ] 383 | }) 384 | }) 385 | 386 | it('should compose a uid store flags command', () => { 387 | expect(buildSTORECommand('1,2,3', 'FLAGS', ['a', 'b'], { 388 | byUid: true 389 | })).to.deep.equal({ 390 | command: 'UID STORE', 391 | attributes: [{ 392 | type: 'sequence', 393 | value: '1,2,3' 394 | }, { 395 | type: 'atom', 396 | value: 'FLAGS' 397 | }, 398 | [{ 399 | type: 'atom', 400 | value: 'a' 401 | }, { 402 | type: 'atom', 403 | value: 'b' 404 | }] 405 | ] 406 | }) 407 | }) 408 | }) 409 | -------------------------------------------------------------------------------- /src/command-builder.js: -------------------------------------------------------------------------------- 1 | import { parser } from 'emailjs-imap-handler' 2 | import { encode } from 'emailjs-mime-codec' 3 | import { encode as encodeBase64 } from 'emailjs-base64' 4 | import { 5 | fromTypedArray, 6 | toTypedArray 7 | } from './common' 8 | 9 | /** 10 | * Builds a FETCH command 11 | * 12 | * @param {String} sequence Message range selector 13 | * @param {Array} items List of elements to fetch (eg. `['uid', 'envelope']`). 14 | * @param {Object} [options] Optional options object. Use `{byUid:true}` for `UID FETCH` 15 | * @returns {Object} Structured IMAP command 16 | */ 17 | export function buildFETCHCommand (sequence, items, options) { 18 | const command = { 19 | command: options.byUid ? 'UID FETCH' : 'FETCH', 20 | attributes: [{ 21 | type: 'SEQUENCE', 22 | value: sequence 23 | }] 24 | } 25 | 26 | if (options.valueAsString !== undefined) { 27 | command.valueAsString = options.valueAsString 28 | } 29 | 30 | let query = [] 31 | 32 | items.forEach((item) => { 33 | item = item.toUpperCase().trim() 34 | 35 | if (/^\w+$/.test(item)) { 36 | // alphanum strings can be used directly 37 | query.push({ 38 | type: 'ATOM', 39 | value: item 40 | }) 41 | } else if (item) { 42 | try { 43 | // parse the value as a fake command, use only the attributes block 44 | const cmd = parser(toTypedArray('* Z ' + item)) 45 | query = query.concat(cmd.attributes || []) 46 | } catch (e) { 47 | // if parse failed, use the original string as one entity 48 | query.push({ 49 | type: 'ATOM', 50 | value: item 51 | }) 52 | } 53 | } 54 | }) 55 | 56 | if (query.length === 1) { 57 | query = query.pop() 58 | } 59 | 60 | command.attributes.push(query) 61 | 62 | if (options.changedSince) { 63 | command.attributes.push([{ 64 | type: 'ATOM', 65 | value: 'CHANGEDSINCE' 66 | }, { 67 | type: 'ATOM', 68 | value: options.changedSince 69 | }]) 70 | } 71 | 72 | return command 73 | } 74 | 75 | /** 76 | * Builds a login token for XOAUTH2 authentication command 77 | * 78 | * @param {String} user E-mail address of the user 79 | * @param {String} token Valid access token for the user 80 | * @return {String} Base64 formatted login token 81 | */ 82 | export function buildXOAuth2Token (user = '', token) { 83 | const authData = [ 84 | `user=${user}`, 85 | `auth=Bearer ${token}`, 86 | '', 87 | '' 88 | ] 89 | return encodeBase64(authData.join('\x01')) 90 | } 91 | 92 | /** 93 | * Compiles a search query into an IMAP command. Queries are composed as objects 94 | * where keys are search terms and values are term arguments. Only strings, 95 | * numbers and Dates are used. If the value is an array, the members of it 96 | * are processed separately (use this for terms that require multiple params). 97 | * If the value is a Date, it is converted to the form of "01-Jan-1970". 98 | * Subqueries (OR, NOT) are made up of objects 99 | * 100 | * {unseen: true, header: ["subject", "hello world"]}; 101 | * SEARCH UNSEEN HEADER "subject" "hello world" 102 | * 103 | * @param {Object} query Search query 104 | * @param {Object} [options] Option object 105 | * @param {Boolean} [options.byUid] If ture, use UID SEARCH instead of SEARCH 106 | * @return {Object} IMAP command object 107 | */ 108 | export function buildSEARCHCommand (query = {}, options = {}) { 109 | const command = { 110 | command: options.byUid ? 'UID SEARCH' : 'SEARCH' 111 | } 112 | 113 | let isAscii = true 114 | 115 | const buildTerm = (query) => { 116 | let list = [] 117 | 118 | Object.keys(query).forEach((key) => { 119 | let params = [] 120 | const formatDate = (date) => date.toUTCString().replace(/^\w+, 0?(\d+) (\w+) (\d+).*/, '$1-$2-$3') 121 | const escapeParam = (param) => { 122 | if (typeof param === 'number') { 123 | return { 124 | type: 'number', 125 | value: param 126 | } 127 | } else if (typeof param === 'string') { 128 | if (/[\u0080-\uFFFF]/.test(param)) { 129 | isAscii = false 130 | return { 131 | type: 'literal', 132 | value: fromTypedArray(encode(param)) // cast unicode string to pseudo-binary as imap-handler compiles strings as octets 133 | } 134 | } 135 | return { 136 | type: 'string', 137 | value: param 138 | } 139 | } else if (Object.prototype.toString.call(param) === '[object Date]') { 140 | // RFC 3501 allows for dates to be placed in 141 | // double-quotes or left without quotes. Some 142 | // servers (Yandex), do not like the double quotes, 143 | // so we treat the date as an atom. 144 | return { 145 | type: 'atom', 146 | value: formatDate(param) 147 | } 148 | } else if (Array.isArray(param)) { 149 | return param.map(escapeParam) 150 | } else if (typeof param === 'object') { 151 | return buildTerm(param) 152 | } 153 | } 154 | 155 | params.push({ 156 | type: 'atom', 157 | value: key.toUpperCase() 158 | }); 159 | 160 | [].concat(query[key] || []).forEach((param) => { 161 | switch (key.toLowerCase()) { 162 | case 'uid': 163 | param = { 164 | type: 'sequence', 165 | value: param 166 | } 167 | break 168 | // The Gmail extension values of X-GM-THRID and 169 | // X-GM-MSGID are defined to be unsigned 64-bit integers 170 | // and they must not be quoted strings or the server 171 | // will report a parse error. 172 | case 'x-gm-thrid': 173 | case 'x-gm-msgid': 174 | param = { 175 | type: 'number', 176 | value: param 177 | } 178 | break 179 | default: 180 | param = escapeParam(param) 181 | } 182 | if (param) { 183 | params = params.concat(param || []) 184 | } 185 | }) 186 | list = list.concat(params || []) 187 | }) 188 | 189 | return list 190 | } 191 | 192 | command.attributes = buildTerm(query) 193 | 194 | // If any string input is using 8bit bytes, prepend the optional CHARSET argument 195 | if (!isAscii) { 196 | command.attributes.unshift({ 197 | type: 'atom', 198 | value: 'UTF-8' 199 | }) 200 | command.attributes.unshift({ 201 | type: 'atom', 202 | value: 'CHARSET' 203 | }) 204 | } 205 | 206 | return command 207 | } 208 | 209 | /** 210 | * Creates an IMAP STORE command from the selected arguments 211 | */ 212 | export function buildSTORECommand (sequence, action = '', flags = [], options = {}) { 213 | const command = { 214 | command: options.byUid ? 'UID STORE' : 'STORE', 215 | attributes: [{ 216 | type: 'sequence', 217 | value: sequence 218 | }] 219 | } 220 | 221 | command.attributes.push({ 222 | type: 'atom', 223 | value: action.toUpperCase() + (options.silent ? '.SILENT' : '') 224 | }) 225 | 226 | command.attributes.push(flags.map((flag) => { 227 | return { 228 | type: 'atom', 229 | value: flag 230 | } 231 | })) 232 | 233 | return command 234 | } 235 | -------------------------------------------------------------------------------- /src/command-parser-unit.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | /* eslint-disable no-useless-escape */ 3 | 4 | import { parser } from 'emailjs-imap-handler' 5 | import { 6 | parseAPPEND, 7 | parseCOPY, 8 | parseSEARCH, 9 | parseNAMESPACE, 10 | parseENVELOPE, 11 | parseSELECT, 12 | parseBODYSTRUCTURE, 13 | parseFETCH 14 | } from './command-parser' 15 | import { toTypedArray } from './common' 16 | import testEnvelope from '../res/fixtures/envelope' 17 | import mimeTorture from '../res/fixtures/mime-torture-bodystructure' 18 | 19 | describe('parseNAMESPACE', () => { 20 | it('should not succeed for no namespace response', () => { 21 | expect(parseNAMESPACE({ 22 | payload: { 23 | NAMESPACE: [] 24 | } 25 | })).to.be.false 26 | }) 27 | 28 | it('should return single personal namespace', () => { 29 | expect(parseNAMESPACE({ 30 | payload: { 31 | NAMESPACE: [{ 32 | attributes: [ 33 | [ 34 | [{ 35 | type: 'STRING', 36 | value: 'INBOX.' 37 | }, { 38 | type: 'STRING', 39 | value: '.' 40 | }] 41 | ], null, null 42 | ] 43 | }] 44 | } 45 | })).to.deep.equal({ 46 | personal: [{ 47 | prefix: 'INBOX.', 48 | delimiter: '.' 49 | }], 50 | users: false, 51 | shared: false 52 | }) 53 | }) 54 | 55 | it('should return single personal, single users, multiple shared', () => { 56 | expect(parseNAMESPACE({ 57 | payload: { 58 | NAMESPACE: [{ 59 | attributes: [ 60 | // personal 61 | [ 62 | [{ 63 | type: 'STRING', 64 | value: '' 65 | }, { 66 | type: 'STRING', 67 | value: '/' 68 | }] 69 | ], 70 | // users 71 | [ 72 | [{ 73 | type: 'STRING', 74 | value: '~' 75 | }, { 76 | type: 'STRING', 77 | value: '/' 78 | }] 79 | ], 80 | // shared 81 | [ 82 | [{ 83 | type: 'STRING', 84 | value: '#shared/' 85 | }, { 86 | type: 'STRING', 87 | value: '/' 88 | }], 89 | [{ 90 | type: 'STRING', 91 | value: '#public/' 92 | }, { 93 | type: 'STRING', 94 | value: '/' 95 | }] 96 | ] 97 | ] 98 | }] 99 | } 100 | })).to.deep.equal({ 101 | personal: [{ 102 | prefix: '', 103 | delimiter: '/' 104 | }], 105 | users: [{ 106 | prefix: '~', 107 | delimiter: '/' 108 | }], 109 | shared: [{ 110 | prefix: '#shared/', 111 | delimiter: '/' 112 | }, { 113 | prefix: '#public/', 114 | delimiter: '/' 115 | }] 116 | }) 117 | }) 118 | 119 | it('should handle NIL namespace hierarchy delim', () => { 120 | expect(parseNAMESPACE({ 121 | payload: { 122 | NAMESPACE: [ 123 | // This specific value is returned by yahoo.co.jp's 124 | // imapgate version 0.7.68_11_1.61475 IMAP server 125 | parser(toTypedArray('* NAMESPACE (("" NIL)) NIL NIL')) 126 | ] 127 | } 128 | })).to.deep.equal({ 129 | personal: [{ 130 | prefix: '', 131 | delimiter: null 132 | }], 133 | users: false, 134 | shared: false 135 | }) 136 | }) 137 | }) 138 | 139 | describe('parseSELECT', () => { 140 | it('should parse a complete response', () => { 141 | expect(parseSELECT({ 142 | code: 'READ-WRITE', 143 | payload: { 144 | EXISTS: [{ 145 | nr: 123 146 | }], 147 | FLAGS: [{ 148 | attributes: [ 149 | [{ 150 | type: 'ATOM', 151 | value: '\\Answered' 152 | }, { 153 | type: 'ATOM', 154 | value: '\\Flagged' 155 | }] 156 | ] 157 | }], 158 | OK: [{ 159 | code: 'PERMANENTFLAGS', 160 | permanentflags: ['\\Answered', '\\Flagged'] 161 | }, { 162 | code: 'UIDVALIDITY', 163 | uidvalidity: '2' 164 | }, { 165 | code: 'UIDNEXT', 166 | uidnext: '38361' 167 | }, { 168 | code: 'HIGHESTMODSEQ', 169 | highestmodseq: '3682918' 170 | }] 171 | } 172 | })).to.deep.equal({ 173 | exists: 123, 174 | flags: ['\\Answered', '\\Flagged'], 175 | highestModseq: '3682918', 176 | permanentFlags: ['\\Answered', '\\Flagged'], 177 | readOnly: false, 178 | uidNext: 38361, 179 | uidValidity: 2 180 | }) 181 | }) 182 | 183 | it('should parse response with no modseq', () => { 184 | expect(parseSELECT({ 185 | code: 'READ-WRITE', 186 | payload: { 187 | EXISTS: [{ 188 | nr: 123 189 | }], 190 | FLAGS: [{ 191 | attributes: [ 192 | [{ 193 | type: 'ATOM', 194 | value: '\\Answered' 195 | }, { 196 | type: 'ATOM', 197 | value: '\\Flagged' 198 | }] 199 | ] 200 | }], 201 | OK: [{ 202 | code: 'PERMANENTFLAGS', 203 | permanentflags: ['\\Answered', '\\Flagged'] 204 | }, { 205 | code: 'UIDVALIDITY', 206 | uidvalidity: '2' 207 | }, { 208 | code: 'UIDNEXT', 209 | uidnext: '38361' 210 | }] 211 | } 212 | })).to.deep.equal({ 213 | exists: 123, 214 | flags: ['\\Answered', '\\Flagged'], 215 | permanentFlags: ['\\Answered', '\\Flagged'], 216 | readOnly: false, 217 | uidNext: 38361, 218 | uidValidity: 2 219 | }) 220 | }) 221 | 222 | it('should parse response with read-only', () => { 223 | expect(parseSELECT({ 224 | code: 'READ-ONLY', 225 | payload: { 226 | EXISTS: [{ 227 | nr: 123 228 | }], 229 | FLAGS: [{ 230 | attributes: [ 231 | [{ 232 | type: 'ATOM', 233 | value: '\\Answered' 234 | }, { 235 | type: 'ATOM', 236 | value: '\\Flagged' 237 | }] 238 | ] 239 | }], 240 | OK: [{ 241 | code: 'PERMANENTFLAGS', 242 | permanentflags: ['\\Answered', '\\Flagged'] 243 | }, { 244 | code: 'UIDVALIDITY', 245 | uidvalidity: '2' 246 | }, { 247 | code: 'UIDNEXT', 248 | uidnext: '38361' 249 | }] 250 | } 251 | })).to.deep.equal({ 252 | exists: 123, 253 | flags: ['\\Answered', '\\Flagged'], 254 | permanentFlags: ['\\Answered', '\\Flagged'], 255 | readOnly: true, 256 | uidNext: 38361, 257 | uidValidity: 2 258 | }) 259 | }) 260 | 261 | it('should parse response with NOMODSEQ flag', () => { 262 | expect(parseSELECT({ 263 | code: 'READ-WRITE', 264 | payload: { 265 | EXISTS: [{ 266 | nr: 123 267 | }], 268 | FLAGS: [{ 269 | attributes: [ 270 | [{ 271 | type: 'ATOM', 272 | value: '\\Answered' 273 | }, { 274 | type: 'ATOM', 275 | value: '\\Flagged' 276 | }] 277 | ] 278 | }], 279 | OK: [{ 280 | code: 'PERMANENTFLAGS', 281 | permanentflags: ['\\Answered', '\\Flagged'] 282 | }, { 283 | code: 'UIDVALIDITY', 284 | uidvalidity: '2' 285 | }, { 286 | code: 'UIDNEXT', 287 | uidnext: '38361' 288 | }, { 289 | code: 'NOMODSEQ' 290 | }] 291 | } 292 | })).to.deep.equal({ 293 | exists: 123, 294 | flags: ['\\Answered', '\\Flagged'], 295 | permanentFlags: ['\\Answered', '\\Flagged'], 296 | readOnly: false, 297 | uidNext: 38361, 298 | uidValidity: 2, 299 | noModseq: true 300 | }) 301 | }) 302 | }) 303 | 304 | describe('parseENVELOPE', () => { 305 | it('should parsed envelope object', () => { 306 | expect(parseENVELOPE(testEnvelope.source)).to.deep.equal(testEnvelope.parsed) 307 | }) 308 | }) 309 | 310 | describe('parseBODYSTRUCTURE', () => { 311 | it('should parse bodystructure object', () => { 312 | expect(parseBODYSTRUCTURE(mimeTorture.source)).to.deep.equal(mimeTorture.parsed) 313 | }) 314 | 315 | it('should parse bodystructure with unicode filename', () => { 316 | var input = [ 317 | [ 318 | { type: 'STRING', value: 'APPLICATION' }, 319 | { type: 'STRING', value: 'OCTET-STREAM' }, 320 | null, 321 | null, 322 | null, 323 | { type: 'STRING', value: 'BASE64' }, 324 | { type: 'ATOM', value: '40' }, 325 | null, 326 | [ 327 | { type: 'STRING', value: 'ATTACHMENT' }, 328 | [ 329 | { type: 'STRING', value: 'FILENAME' }, 330 | { type: 'STRING', value: '=?ISO-8859-1?Q?BBR_Handel,_Gewerbe,_B=FCrobetriebe,?= =?ISO-8859-1?Q?_private_Bildungseinrichtungen.txt?=' } 331 | ] 332 | ], 333 | null 334 | ], 335 | { type: 'STRING', value: 'MIXED' }, 336 | [ 337 | { type: 'STRING', value: 'BOUNDARY' }, 338 | { type: 'STRING', value: '----sinikael-?=_1-14105085265110.49903922458179295' } 339 | ], 340 | null, 341 | null 342 | ] 343 | 344 | var expected = { 345 | childNodes: [{ 346 | part: '1', 347 | type: 'application/octet-stream', 348 | encoding: 'base64', 349 | size: 40, 350 | disposition: 'attachment', 351 | dispositionParameters: { 352 | filename: 'BBR Handel, Gewerbe, Bürobetriebe, private Bildungseinrichtungen.txt' 353 | } 354 | }], 355 | type: 'multipart/mixed', 356 | parameters: { 357 | boundary: '----sinikael-?=_1-14105085265110.49903922458179295' 358 | } 359 | } 360 | 361 | expect(parseBODYSTRUCTURE(input)).to.deep.equal(expected) 362 | }) 363 | }) 364 | 365 | describe('parseFETCH', () => { 366 | it('should return values lowercase keys', () => { 367 | expect(parseFETCH({ 368 | payload: { 369 | FETCH: [{ 370 | nr: 123, 371 | attributes: [ 372 | [{ 373 | type: 'ATOM', 374 | value: 'BODY', 375 | section: [{ 376 | type: 'ATOM', 377 | value: 'HEADER' 378 | }, 379 | [{ 380 | type: 'ATOM', 381 | value: 'DATE' 382 | }, { 383 | type: 'ATOM', 384 | value: 'SUBJECT' 385 | }] 386 | ], 387 | partial: [0, 123] 388 | }, { 389 | type: 'ATOM', 390 | value: 'abc' 391 | }] 392 | ] 393 | }] 394 | } 395 | })).to.deep.equal([{ 396 | '#': 123, 397 | 'body[header (date subject)]<0.123>': 'abc' 398 | }]) 399 | }) 400 | 401 | it('should merge multiple responses based on sequence number', () => { 402 | expect(parseFETCH({ 403 | payload: { 404 | FETCH: [{ 405 | nr: 123, 406 | attributes: [ 407 | [{ 408 | type: 'ATOM', 409 | value: 'UID' 410 | }, { 411 | type: 'ATOM', 412 | value: 789 413 | }] 414 | ] 415 | }, { 416 | nr: 124, 417 | attributes: [ 418 | [{ 419 | type: 'ATOM', 420 | value: 'UID' 421 | }, { 422 | type: 'ATOM', 423 | value: 790 424 | }] 425 | ] 426 | }, { 427 | nr: 123, 428 | attributes: [ 429 | [{ 430 | type: 'ATOM', 431 | value: 'MODSEQ' 432 | }, { 433 | type: 'ATOM', 434 | value: '127' 435 | }] 436 | ] 437 | }] 438 | } 439 | })).to.deep.equal([{ 440 | '#': 123, 441 | uid: 789, 442 | modseq: '127' 443 | }, { 444 | '#': 124, 445 | uid: 790 446 | }]) 447 | }) 448 | }) 449 | 450 | describe('parseSEARCH', () => { 451 | it('should parse SEARCH response', () => { 452 | expect(parseSEARCH({ 453 | payload: { 454 | SEARCH: [{ 455 | attributes: [{ 456 | value: 5 457 | }, { 458 | value: 7 459 | }] 460 | }, { 461 | attributes: [{ 462 | value: 6 463 | }] 464 | }] 465 | } 466 | })).to.deep.equal([5, 6, 7]) 467 | }) 468 | 469 | it('should parse empty SEARCH response', () => { 470 | expect(parseSEARCH({ 471 | payload: { 472 | SEARCH: [{ 473 | command: 'SEARCH', 474 | tag: '*' 475 | }] 476 | } 477 | })).to.deep.equal([]) 478 | }) 479 | 480 | it('should throw error when response is not defined', () => { 481 | expect(() => parseSEARCH()).throws(Error, 'parseSEARCH') 482 | }) 483 | }) 484 | 485 | describe('parseCOPY', () => { 486 | it('should parse COPY response', () => { 487 | expect(parseCOPY({ 488 | copyuid: ['1', '1:3', '3,4,2'] 489 | })).to.deep.equal({ 490 | srcSeqSet: '1:3', 491 | destSeqSet: '3,4,2' 492 | }) 493 | }) 494 | 495 | it('should return undefined when response does not contain copyuid', () => { 496 | expect(parseCOPY({})).to.equal(undefined) 497 | }) 498 | 499 | it('should return undefined when response is not defined', () => { 500 | expect(parseCOPY()).to.equal(undefined) 501 | }) 502 | }) 503 | 504 | describe('parseAPPEND', () => { 505 | it('should parse APPEND response', () => { 506 | expect(parseAPPEND({ 507 | appenduid: ['1', '3'] 508 | })).to.equal('3') 509 | }) 510 | 511 | it('should return undefined when response does not contain copyuid', () => { 512 | expect(parseAPPEND({})).to.equal(undefined) 513 | }) 514 | 515 | it('should return undefined when response is not defined', () => { 516 | expect(parseAPPEND()).to.equal(undefined) 517 | }) 518 | }) 519 | -------------------------------------------------------------------------------- /src/command-parser.js: -------------------------------------------------------------------------------- 1 | import parseAddress from 'emailjs-addressparser' 2 | import { compiler } from 'emailjs-imap-handler' 3 | import { zip, fromPairs, prop, pathOr, propOr, toLower } from 'ramda' 4 | import { mimeWordEncode, mimeWordsDecode } from 'emailjs-mime-codec' 5 | 6 | /** 7 | * Parses NAMESPACE response 8 | * 9 | * @param {Object} response 10 | * @return {Object} Namespaces object 11 | */ 12 | export function parseNAMESPACE (response) { 13 | if (!response.payload || !response.payload.NAMESPACE || !response.payload.NAMESPACE.length) { 14 | return false 15 | } 16 | 17 | const attributes = [].concat(response.payload.NAMESPACE.pop().attributes || []) 18 | if (!attributes.length) { 19 | return false 20 | } 21 | 22 | return { 23 | personal: parseNAMESPACEElement(attributes[0]), 24 | users: parseNAMESPACEElement(attributes[1]), 25 | shared: parseNAMESPACEElement(attributes[2]) 26 | } 27 | } 28 | 29 | /** 30 | * Parses a NAMESPACE element 31 | * 32 | * @param {Object} element 33 | * @return {Object} Namespaces element object 34 | */ 35 | export function parseNAMESPACEElement (element) { 36 | if (!element) { 37 | return false 38 | } 39 | 40 | element = [].concat(element || []) 41 | return element.map((ns) => { 42 | if (!ns || !ns.length) { 43 | return false 44 | } 45 | 46 | return { 47 | prefix: ns[0].value, 48 | delimiter: ns[1] && ns[1].value // The delimiter can legally be NIL which maps to null 49 | } 50 | }) 51 | } 52 | 53 | /** 54 | * Parses SELECT response 55 | * 56 | * @param {Object} response 57 | * @return {Object} Mailbox information object 58 | */ 59 | export function parseSELECT (response) { 60 | if (!response || !response.payload) { 61 | return 62 | } 63 | 64 | const mailbox = { 65 | readOnly: response.code === 'READ-ONLY' 66 | } 67 | const existsResponse = response.payload.EXISTS && response.payload.EXISTS.pop() 68 | const flagsResponse = response.payload.FLAGS && response.payload.FLAGS.pop() 69 | const okResponse = response.payload.OK 70 | 71 | if (existsResponse) { 72 | mailbox.exists = existsResponse.nr || 0 73 | } 74 | 75 | if (flagsResponse && flagsResponse.attributes && flagsResponse.attributes.length) { 76 | mailbox.flags = flagsResponse.attributes[0].map((flag) => (flag.value || '').toString().trim()) 77 | } 78 | 79 | [].concat(okResponse || []).forEach((ok) => { 80 | switch (ok && ok.code) { 81 | case 'PERMANENTFLAGS': 82 | mailbox.permanentFlags = [].concat(ok.permanentflags || []) 83 | break 84 | case 'UIDVALIDITY': 85 | mailbox.uidValidity = Number(ok.uidvalidity) || 0 86 | break 87 | case 'UIDNEXT': 88 | mailbox.uidNext = Number(ok.uidnext) || 0 89 | break 90 | case 'HIGHESTMODSEQ': 91 | mailbox.highestModseq = ok.highestmodseq || '0' // keep 64bit uint as a string 92 | break 93 | case 'NOMODSEQ': 94 | mailbox.noModseq = true 95 | break 96 | } 97 | }) 98 | 99 | return mailbox 100 | } 101 | 102 | /** 103 | * Parses message envelope from FETCH response. All keys in the resulting 104 | * object are lowercase. Address fields are all arrays with {name:, address:} 105 | * structured values. Unicode strings are automatically decoded. 106 | * 107 | * @param {Array} value Envelope array 108 | * @param {Object} Envelope object 109 | */ 110 | export function parseENVELOPE (value) { 111 | const envelope = {} 112 | 113 | if (value[0] && value[0].value) { 114 | envelope.date = value[0].value 115 | } 116 | 117 | if (value[1] && value[1].value) { 118 | envelope.subject = mimeWordsDecode(value[1] && value[1].value) 119 | } 120 | 121 | if (value[2] && value[2].length) { 122 | envelope.from = processAddresses(value[2]) 123 | } 124 | 125 | if (value[3] && value[3].length) { 126 | envelope.sender = processAddresses(value[3]) 127 | } 128 | 129 | if (value[4] && value[4].length) { 130 | envelope['reply-to'] = processAddresses(value[4]) 131 | } 132 | 133 | if (value[5] && value[5].length) { 134 | envelope.to = processAddresses(value[5]) 135 | } 136 | 137 | if (value[6] && value[6].length) { 138 | envelope.cc = processAddresses(value[6]) 139 | } 140 | 141 | if (value[7] && value[7].length) { 142 | envelope.bcc = processAddresses(value[7]) 143 | } 144 | 145 | if (value[8] && value[8].value) { 146 | envelope['in-reply-to'] = value[8].value 147 | } 148 | 149 | if (value[9] && value[9].value) { 150 | envelope['message-id'] = value[9].value 151 | } 152 | 153 | return envelope 154 | } 155 | 156 | /* 157 | * ENVELOPE lists addresses as [name-part, source-route, username, hostname] 158 | * where source-route is not used anymore and can be ignored. 159 | * To get comparable results with other parts of the email.js stack 160 | * browserbox feeds the parsed address values from ENVELOPE 161 | * to addressparser and uses resulting values instead of the 162 | * pre-parsed addresses 163 | */ 164 | function processAddresses (list = []) { 165 | return list.map((addr) => { 166 | const name = (pathOr('', ['0', 'value'], addr)).trim() 167 | const address = (pathOr('', ['2', 'value'], addr)) + '@' + (pathOr('', ['3', 'value'], addr)) 168 | const formatted = name ? (encodeAddressName(name) + ' <' + address + '>') : address 169 | const parsed = parseAddress(formatted).shift() // there should be just a single address 170 | parsed.name = mimeWordsDecode(parsed.name) 171 | return parsed 172 | }) 173 | } 174 | 175 | /** 176 | * If needed, encloses with quotes or mime encodes the name part of an e-mail address 177 | * 178 | * @param {String} name Name part of an address 179 | * @returns {String} Mime word encoded or quoted string 180 | */ 181 | function encodeAddressName (name) { 182 | if (!/^[\w ']*$/.test(name)) { 183 | if (/^[\x20-\x7e]*$/.test(name)) { 184 | return JSON.stringify(name) 185 | } else { 186 | return mimeWordEncode(name, 'Q', 52) 187 | } 188 | } 189 | return name 190 | } 191 | 192 | /** 193 | * Parses message body structure from FETCH response. 194 | * 195 | * @param {Array} value BODYSTRUCTURE array 196 | * @param {Object} Envelope object 197 | */ 198 | export function parseBODYSTRUCTURE (node, path = []) { 199 | const curNode = {} 200 | let i = 0 201 | let part = 0 202 | 203 | if (path.length) { 204 | curNode.part = path.join('.') 205 | } 206 | 207 | // multipart 208 | if (Array.isArray(node[0])) { 209 | curNode.childNodes = [] 210 | while (Array.isArray(node[i])) { 211 | curNode.childNodes.push(parseBODYSTRUCTURE(node[i], path.concat(++part))) 212 | i++ 213 | } 214 | 215 | // multipart type 216 | curNode.type = 'multipart/' + ((node[i++] || {}).value || '').toString().toLowerCase() 217 | 218 | // extension data (not available for BODY requests) 219 | 220 | // body parameter parenthesized list 221 | if (i < node.length - 1) { 222 | if (node[i]) { 223 | curNode.parameters = attributesToObject(node[i]) 224 | } 225 | i++ 226 | } 227 | } else { 228 | // content type 229 | curNode.type = [ 230 | ((node[i++] || {}).value || '').toString().toLowerCase(), ((node[i++] || {}).value || '').toString().toLowerCase() 231 | ].join('/') 232 | 233 | // body parameter parenthesized list 234 | if (node[i]) { 235 | curNode.parameters = attributesToObject(node[i]) 236 | } 237 | i++ 238 | 239 | // id 240 | if (node[i]) { 241 | curNode.id = ((node[i] || {}).value || '').toString() 242 | } 243 | i++ 244 | 245 | // description 246 | if (node[i]) { 247 | curNode.description = ((node[i] || {}).value || '').toString() 248 | } 249 | i++ 250 | 251 | // encoding 252 | if (node[i]) { 253 | curNode.encoding = ((node[i] || {}).value || '').toString().toLowerCase() 254 | } 255 | i++ 256 | 257 | // size 258 | if (node[i]) { 259 | curNode.size = Number((node[i] || {}).value || 0) || 0 260 | } 261 | i++ 262 | 263 | if (curNode.type === 'message/rfc822') { 264 | // message/rfc adds additional envelope, bodystructure and line count values 265 | 266 | // envelope 267 | if (node[i]) { 268 | curNode.envelope = parseENVELOPE([].concat(node[i] || [])) 269 | } 270 | i++ 271 | 272 | if (node[i]) { 273 | curNode.childNodes = [ 274 | // rfc822 bodyparts share the same path, difference is between MIME and HEADER 275 | // path.MIME returns message/rfc822 header 276 | // path.HEADER returns inlined message header 277 | parseBODYSTRUCTURE(node[i], path) 278 | ] 279 | } 280 | i++ 281 | 282 | // line count 283 | if (node[i]) { 284 | curNode.lineCount = Number((node[i] || {}).value || 0) || 0 285 | } 286 | i++ 287 | } else if (/^text\//.test(curNode.type)) { 288 | // text/* adds additional line count values 289 | 290 | // line count 291 | if (node[i]) { 292 | curNode.lineCount = Number((node[i] || {}).value || 0) || 0 293 | } 294 | i++ 295 | } 296 | 297 | // extension data (not available for BODY requests) 298 | 299 | // md5 300 | if (i < node.length - 1) { 301 | if (node[i]) { 302 | curNode.md5 = ((node[i] || {}).value || '').toString().toLowerCase() 303 | } 304 | i++ 305 | } 306 | } 307 | 308 | // the following are shared extension values (for both multipart and non-multipart parts) 309 | // not available for BODY requests 310 | 311 | // body disposition 312 | if (i < node.length - 1) { 313 | if (Array.isArray(node[i]) && node[i].length) { 314 | curNode.disposition = ((node[i][0] || {}).value || '').toString().toLowerCase() 315 | if (Array.isArray(node[i][1])) { 316 | curNode.dispositionParameters = attributesToObject(node[i][1]) 317 | } 318 | } 319 | i++ 320 | } 321 | 322 | // body language 323 | if (i < node.length - 1) { 324 | if (node[i]) { 325 | curNode.language = [].concat(node[i]).map((val) => propOr('', 'value', val).toLowerCase()) 326 | } 327 | i++ 328 | } 329 | 330 | // body location 331 | // NB! defined as a "string list" in RFC3501 but replaced in errata document with "string" 332 | // Errata: http://www.rfc-editor.org/errata_search.php?rfc=3501 333 | if (i < node.length - 1) { 334 | if (node[i]) { 335 | curNode.location = ((node[i] || {}).value || '').toString() 336 | } 337 | i++ 338 | } 339 | 340 | return curNode 341 | } 342 | 343 | function attributesToObject (attrs = [], keyTransform = toLower, valueTransform = mimeWordsDecode) { 344 | const vals = attrs.map(prop('value')) 345 | const keys = vals.filter((_, i) => i % 2 === 0).map(keyTransform) 346 | const values = vals.filter((_, i) => i % 2 === 1).map(valueTransform) 347 | return fromPairs(zip(keys, values)) 348 | } 349 | 350 | /** 351 | * Parses FETCH response 352 | * 353 | * @param {Object} response 354 | * @return {Object} Message object 355 | */ 356 | export function parseFETCH (response) { 357 | if (!response || !response.payload || !response.payload.FETCH || !response.payload.FETCH.length) { 358 | return [] 359 | } 360 | 361 | const list = [] 362 | const messages = {} 363 | 364 | response.payload.FETCH.forEach((item) => { 365 | const params = [].concat([].concat(item.attributes || [])[0] || []) // ensure the first value is an array 366 | let message 367 | let i, len, key 368 | 369 | if (messages[item.nr]) { 370 | // same sequence number is already used, so merge values instead of creating a new message object 371 | message = messages[item.nr] 372 | } else { 373 | messages[item.nr] = message = { 374 | '#': item.nr 375 | } 376 | list.push(message) 377 | } 378 | 379 | for (i = 0, len = params.length; i < len; i++) { 380 | if (i % 2 === 0) { 381 | key = compiler({ 382 | attributes: [params[i]] 383 | }).toLowerCase().replace(/<\d+>$/, '') 384 | continue 385 | } 386 | message[key] = parseFetchValue(key, params[i]) 387 | } 388 | }) 389 | 390 | return list 391 | } 392 | 393 | /** 394 | * Parses a single value from the FETCH response object 395 | * 396 | * @param {String} key Key name (uppercase) 397 | * @param {Mized} value Value for the key 398 | * @return {Mixed} Processed value 399 | */ 400 | function parseFetchValue (key, value) { 401 | if (!value) { 402 | return null 403 | } 404 | 405 | if (!Array.isArray(value)) { 406 | switch (key) { 407 | case 'uid': 408 | case 'rfc822.size': 409 | return Number(value.value) || 0 410 | case 'modseq': // do not cast 64 bit uint to a number 411 | return value.value || '0' 412 | } 413 | return value.value 414 | } 415 | 416 | switch (key) { 417 | case 'flags': 418 | case 'x-gm-labels': 419 | value = [].concat(value).map((flag) => (flag.value || '')) 420 | break 421 | case 'envelope': 422 | value = parseENVELOPE([].concat(value || [])) 423 | break 424 | case 'bodystructure': 425 | value = parseBODYSTRUCTURE([].concat(value || [])) 426 | break 427 | case 'modseq': 428 | value = (value.shift() || {}).value || '0' 429 | break 430 | } 431 | 432 | return value 433 | } 434 | 435 | /** 436 | * Binary Search - from npm module binary-search, license CC0 437 | * 438 | * @param {Array} haystack Ordered array 439 | * @param {any} needle Item to search for in haystack 440 | * @param {Function} comparator Function that defines the sort order 441 | * @return {Number} Index of needle in haystack or if not found, 442 | * -Index-1 is the position where needle could be inserted while still 443 | * keeping haystack ordered. 444 | */ 445 | function binSearch (haystack, needle, comparator = (a, b) => a - b) { 446 | var mid, cmp 447 | var low = 0 448 | var high = haystack.length - 1 449 | 450 | while (low <= high) { 451 | // Note that "(low + high) >>> 1" may overflow, and results in 452 | // a typecast to double (which gives the wrong results). 453 | mid = low + (high - low >> 1) 454 | cmp = +comparator(haystack[mid], needle) 455 | 456 | if (cmp < 0.0) { 457 | // too low 458 | low = mid + 1 459 | } else if (cmp > 0.0) { 460 | // too high 461 | high = mid - 1 462 | } else { 463 | // key found 464 | return mid 465 | } 466 | } 467 | 468 | // key not found 469 | return ~low 470 | }; 471 | 472 | /** 473 | * Parses SEARCH response. Gathers all untagged SEARCH responses, fetched seq./uid numbers 474 | * and compiles these into a sorted array. 475 | * 476 | * @param {Object} response 477 | * @return {Array} Sorted Seq./UID number list 478 | */ 479 | export function parseSEARCH (response) { 480 | const list = [] 481 | 482 | if (!response) { 483 | throw new Error('parseSEARCH can not parse undefined response') 484 | } 485 | 486 | if (!response.payload || !response.payload.SEARCH || !response.payload.SEARCH.length) { 487 | return list 488 | } 489 | 490 | response.payload.SEARCH.forEach(result => 491 | (result.attributes || []).forEach(nr => { 492 | nr = Number((nr && nr.value) || nr) || 0 493 | const idx = binSearch(list, nr) 494 | if (idx < 0) { 495 | list.splice(-idx - 1, 0, nr) 496 | } 497 | }) 498 | ) 499 | 500 | return list 501 | }; 502 | 503 | /** 504 | * Parses COPY and UID COPY response. 505 | * https://tools.ietf.org/html/rfc4315 506 | * @param {Object} response 507 | * @returns {{destSeqSet: string, srcSeqSet: string}} Source and 508 | * destination uid sets if available, undefined if not. 509 | */ 510 | export function parseCOPY (response) { 511 | const copyuid = response && response.copyuid 512 | if (copyuid) { 513 | return { 514 | srcSeqSet: copyuid[1], 515 | destSeqSet: copyuid[2] 516 | } 517 | } 518 | } 519 | 520 | /** 521 | * Parses APPEND (upload) response. 522 | * https://tools.ietf.org/html/rfc4315 523 | * @param {Object} response 524 | * @returns {String} The uid assigned to the uploaded message if available. 525 | */ 526 | export function parseAPPEND (response) { 527 | return response && response.appenduid && response.appenduid[1] 528 | } 529 | -------------------------------------------------------------------------------- /src/common.js: -------------------------------------------------------------------------------- 1 | export const LOG_LEVEL_NONE = 1000 2 | export const LOG_LEVEL_ERROR = 40 3 | export const LOG_LEVEL_WARN = 30 4 | export const LOG_LEVEL_INFO = 20 5 | export const LOG_LEVEL_DEBUG = 10 6 | export const LOG_LEVEL_ALL = 0 7 | 8 | export const toTypedArray = str => new Uint8Array(str.split('').map(char => char.charCodeAt(0))) 9 | export const fromTypedArray = arr => String.fromCharCode.apply(null, arr) 10 | -------------------------------------------------------------------------------- /src/compression-worker.js: -------------------------------------------------------------------------------- 1 | import Compressor from './compression' 2 | 3 | const MESSAGE_INITIALIZE_WORKER = 'start' 4 | const MESSAGE_INFLATE = 'inflate' 5 | const MESSAGE_INFLATED_DATA_READY = 'inflated_ready' 6 | const MESSAGE_DEFLATE = 'deflate' 7 | const MESSAGE_DEFLATED_DATA_READY = 'deflated_ready' 8 | 9 | const createMessage = (message, buffer) => ({ message, buffer }) 10 | 11 | const inflatedReady = buffer => self.postMessage(createMessage(MESSAGE_INFLATED_DATA_READY, buffer), [buffer]) 12 | const deflatedReady = buffer => self.postMessage(createMessage(MESSAGE_DEFLATED_DATA_READY, buffer), [buffer]) 13 | const compressor = new Compressor(inflatedReady, deflatedReady) 14 | 15 | self.onmessage = function (e) { 16 | const message = e.data.message 17 | const buffer = e.data.buffer 18 | 19 | switch (message) { 20 | case MESSAGE_INITIALIZE_WORKER: 21 | break 22 | 23 | case MESSAGE_INFLATE: 24 | compressor.inflate(buffer) 25 | break 26 | 27 | case MESSAGE_DEFLATE: 28 | compressor.deflate(buffer) 29 | break 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/compression.js: -------------------------------------------------------------------------------- 1 | import ZStream from 'pako/lib/zlib/zstream' 2 | import { deflateInit2, deflate } from 'pako/lib/zlib/deflate' 3 | import { inflate, inflateInit2 } from 'pako/lib/zlib/inflate' 4 | import messages from 'pako/lib/zlib/messages.js' 5 | import { 6 | Z_NO_FLUSH, Z_SYNC_FLUSH, Z_OK, 7 | Z_STREAM_END, Z_DEFAULT_COMPRESSION, 8 | Z_DEFAULT_STRATEGY, Z_DEFLATED 9 | } from 'pako/lib/zlib/constants' 10 | 11 | const CHUNK_SIZE = 16384 12 | const WINDOW_BITS = 15 13 | 14 | /** 15 | * Handles de-/compression via #inflate() and #deflate(), calls you back via #deflatedReady() and #inflatedReady(). 16 | * The chunk we get from deflater is actually a view of a 16kB arraybuffer, so we need to copy the relevant parts 17 | * memory to a new arraybuffer. 18 | */ 19 | export default function Compressor (inflatedReady, deflatedReady) { 20 | this.inflatedReady = inflatedReady 21 | this.deflatedReady = deflatedReady 22 | this._inflate = inflater(chunk => this.inflatedReady(chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.length))) 23 | this._deflate = deflater(chunk => this.deflatedReady(chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.length))) 24 | } 25 | 26 | Compressor.prototype.inflate = function (buffer) { 27 | this._inflate(new Uint8Array(buffer)) 28 | } 29 | 30 | Compressor.prototype.deflate = function (buffer) { 31 | this._deflate(new Uint8Array(buffer)) 32 | } 33 | 34 | function deflater (emit) { 35 | const stream = new ZStream() 36 | const status = deflateInit2(stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, WINDOW_BITS, 8, Z_DEFAULT_STRATEGY) 37 | if (status !== Z_OK) { 38 | throw new Error('Problem initializing deflate stream: ' + messages[status]) 39 | } 40 | 41 | return function (data) { 42 | if (data === undefined) return emit() 43 | 44 | // Attach the input data 45 | stream.input = data 46 | stream.next_in = 0 47 | stream.avail_in = stream.input.length 48 | 49 | let status 50 | let output 51 | let start 52 | let ret = true 53 | 54 | do { 55 | // When the stream gets full, we need to create new space. 56 | if (stream.avail_out === 0) { 57 | stream.output = new Uint8Array(CHUNK_SIZE) 58 | start = stream.next_out = 0 59 | stream.avail_out = CHUNK_SIZE 60 | } 61 | 62 | // Perform the deflate 63 | status = deflate(stream, Z_SYNC_FLUSH) 64 | if (status !== Z_STREAM_END && status !== Z_OK) { 65 | throw new Error('Deflate problem: ' + messages[status]) 66 | } 67 | 68 | // If the output buffer got full, flush the data. 69 | if (stream.avail_out === 0 && stream.next_out > start) { 70 | output = stream.output.subarray(start, start = stream.next_out) 71 | ret = emit(output) 72 | } 73 | } while ((stream.avail_in > 0 || stream.avail_out === 0) && status !== Z_STREAM_END) 74 | 75 | // Emit whatever is left in output. 76 | if (stream.next_out > start) { 77 | output = stream.output.subarray(start, start = stream.next_out) 78 | ret = emit(output) 79 | } 80 | return ret 81 | } 82 | } 83 | 84 | function inflater (emit) { 85 | const stream = new ZStream() 86 | 87 | const status = inflateInit2(stream, WINDOW_BITS) 88 | if (status !== Z_OK) { 89 | throw new Error('Problem initializing inflate stream: ' + messages[status]) 90 | } 91 | 92 | return function (data) { 93 | if (data === undefined) return emit() 94 | 95 | let start 96 | stream.input = data 97 | stream.next_in = 0 98 | stream.avail_in = stream.input.length 99 | 100 | let status, output 101 | let ret = true 102 | 103 | do { 104 | if (stream.avail_out === 0) { 105 | stream.output = new Uint8Array(CHUNK_SIZE) 106 | start = stream.next_out = 0 107 | stream.avail_out = CHUNK_SIZE 108 | } 109 | 110 | status = inflate(stream, Z_NO_FLUSH) 111 | if (status !== Z_STREAM_END && status !== Z_OK) { 112 | throw new Error('inflate problem: ' + messages[status]) 113 | } 114 | 115 | if (stream.next_out) { 116 | if (stream.avail_out === 0 || status === Z_STREAM_END) { 117 | output = stream.output.subarray(start, start = stream.next_out) 118 | ret = emit(output) 119 | } 120 | } 121 | } while ((stream.avail_in > 0) && status !== Z_STREAM_END) 122 | 123 | if (stream.next_out > start) { 124 | output = stream.output.subarray(start, start = stream.next_out) 125 | ret = emit(output) 126 | } 127 | 128 | return ret 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/imap-unit.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | 3 | import ImapClient from './imap' 4 | import { toTypedArray } from './common' 5 | 6 | const host = 'localhost' 7 | const port = 10000 8 | 9 | describe('browserbox imap unit tests', () => { 10 | var client, socketStub 11 | 12 | /* jshint indent:false */ 13 | 14 | beforeEach(() => { 15 | client = new ImapClient(host, port) 16 | expect(client).to.exist 17 | 18 | client.logger = { 19 | debug: () => { }, 20 | error: () => { } 21 | } 22 | 23 | var Socket = function () { } 24 | Socket.open = () => { } 25 | Socket.prototype.close = () => { } 26 | Socket.prototype.send = () => { } 27 | Socket.prototype.suspend = () => { } 28 | Socket.prototype.resume = () => { } 29 | Socket.prototype.upgradeToSecure = () => { } 30 | 31 | socketStub = sinon.createStubInstance(Socket) 32 | sinon.stub(Socket, 'open').withArgs(host, port).returns(socketStub) 33 | 34 | var promise = client.connect(Socket).then(() => { 35 | expect(Socket.open.callCount).to.equal(1) 36 | 37 | expect(socketStub.onerror).to.exist 38 | expect(socketStub.onopen).to.exist 39 | expect(socketStub.onclose).to.exist 40 | expect(socketStub.ondata).to.exist 41 | }) 42 | 43 | setTimeout(() => socketStub.onopen(), 10) 44 | 45 | return promise 46 | }) 47 | 48 | describe.skip('#close', () => { 49 | it('should call socket.close', () => { 50 | client.socket.readyState = 'open' 51 | 52 | setTimeout(() => socketStub.onclose(), 10) 53 | return client.close().then(() => { 54 | expect(socketStub.close.callCount).to.equal(1) 55 | }) 56 | }) 57 | 58 | it('should not call socket.close', () => { 59 | client.socket.readyState = 'not open. duh.' 60 | 61 | setTimeout(() => socketStub.onclose(), 10) 62 | return client.close().then(() => { 63 | expect(socketStub.close.called).to.be.false 64 | }) 65 | }) 66 | }) 67 | 68 | describe('#upgrade', () => { 69 | it('should upgrade socket', () => { 70 | client.secureMode = false 71 | client.upgrade() 72 | }) 73 | 74 | it('should not upgrade socket', () => { 75 | client.secureMode = true 76 | client.upgrade() 77 | }) 78 | }) 79 | 80 | describe('#setHandler', () => { 81 | it('should set global handler for keyword', () => { 82 | var handler = () => { } 83 | client.setHandler('fetch', handler) 84 | 85 | expect(client._globalAcceptUntagged.FETCH).to.equal(handler) 86 | }) 87 | }) 88 | 89 | describe('#socket.onerror', () => { 90 | it('should emit error and close connection', (done) => { 91 | client.socket.onerror({ 92 | data: new Error('err') 93 | }) 94 | 95 | client.onerror = () => { 96 | done() 97 | } 98 | }) 99 | }) 100 | 101 | describe('#socket.onclose', () => { 102 | it('should emit error ', (done) => { 103 | client.socket.onclose() 104 | 105 | client.onerror = () => { 106 | done() 107 | } 108 | }) 109 | }) 110 | 111 | describe('#_onData', () => { 112 | it('should process input', () => { 113 | sinon.stub(client, '_parseIncomingCommands') 114 | sinon.stub(client, '_iterateIncomingBuffer') 115 | 116 | client._onData({ 117 | data: toTypedArray('foobar').buffer 118 | }) 119 | 120 | expect(client._parseIncomingCommands.calledOnce).to.be.true 121 | expect(client._iterateIncomingBuffer.calledOnce).to.be.true 122 | }) 123 | }) 124 | 125 | describe('rateIncomingBuffer', () => { 126 | it('should iterate chunked input', () => { 127 | appendIncomingBuffer('* 1 FETCH (UID 1)\r\n* 2 FETCH (UID 2)\r\n* 3 FETCH (UID 3)\r\n') 128 | var iterator = client._iterateIncomingBuffer() 129 | 130 | expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 1 FETCH (UID 1)') 131 | expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 2 FETCH (UID 2)') 132 | expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 3 FETCH (UID 3)') 133 | expect(iterator.next().value).to.be.undefined 134 | }) 135 | 136 | it('should process chunked literals', () => { 137 | appendIncomingBuffer('* 1 FETCH (UID {1}\r\n1)\r\n* 2 FETCH (UID {4}\r\n2345)\r\n* 3 FETCH (UID {4}\r\n3789)\r\n') 138 | var iterator = client._iterateIncomingBuffer() 139 | 140 | expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 1 FETCH (UID {1}\r\n1)') 141 | expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 2 FETCH (UID {4}\r\n2345)') 142 | expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 3 FETCH (UID {4}\r\n3789)') 143 | expect(iterator.next().value).to.be.undefined 144 | }) 145 | 146 | it('should process chunked literals 2', () => { 147 | appendIncomingBuffer('* 1 FETCH (UID 1)\r\n* 2 FETCH (UID {4}\r\n2345)\r\n') 148 | var iterator = client._iterateIncomingBuffer() 149 | 150 | expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 1 FETCH (UID 1)') 151 | expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 2 FETCH (UID {4}\r\n2345)') 152 | expect(iterator.next().value).to.be.undefined 153 | }) 154 | 155 | it('should process chunked literals 3', () => { 156 | appendIncomingBuffer('* 1 FETCH (UID {1}\r\n1)\r\n* 2 FETCH (UID 4)\r\n') 157 | var iterator = client._iterateIncomingBuffer() 158 | 159 | expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 1 FETCH (UID {1}\r\n1)') 160 | expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 2 FETCH (UID 4)') 161 | expect(iterator.next().value).to.be.undefined 162 | }) 163 | 164 | it('should process chunked literals 4', () => { 165 | appendIncomingBuffer('* SEARCH {1}\r\n1 {1}\r\n2\r\n') 166 | var iterator = client._iterateIncomingBuffer() 167 | expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* SEARCH {1}\r\n1 {1}\r\n2') 168 | }) 169 | 170 | it('should process CRLF literal', () => { 171 | appendIncomingBuffer('* 1 FETCH (UID 20 BODY[HEADER.FIELDS (REFERENCES LIST-ID)] {2}\r\n\r\n)\r\n') 172 | var iterator = client._iterateIncomingBuffer() 173 | expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 1 FETCH (UID 20 BODY[HEADER.FIELDS (REFERENCES LIST-ID)] {2}\r\n\r\n)') 174 | }) 175 | 176 | it('should process CRLF literal 2', () => { 177 | appendIncomingBuffer('* 1 FETCH (UID 1 ENVELOPE ("string with {parenthesis}") BODY[HEADER.FIELDS (REFERENCES LIST-ID)] {2}\r\n\r\n)\r\n') 178 | var iterator = client._iterateIncomingBuffer() 179 | expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 1 FETCH (UID 1 ENVELOPE ("string with {parenthesis}") BODY[HEADER.FIELDS (REFERENCES LIST-ID)] {2}\r\n\r\n)') 180 | }) 181 | 182 | it('should parse multiple zero-length literals', () => { 183 | appendIncomingBuffer('* 126015 FETCH (UID 585599 BODY[1.2] {0}\r\n BODY[1.1] {0}\r\n)\r\n') 184 | var iterator = client._iterateIncomingBuffer() 185 | expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 126015 FETCH (UID 585599 BODY[1.2] {0}\r\n BODY[1.1] {0}\r\n)') 186 | }) 187 | 188 | it('should process two commands when CRLF arrives in 2 parts', () => { 189 | appendIncomingBuffer('* 1 FETCH (UID 1)\r') 190 | var iterator1 = client._iterateIncomingBuffer() 191 | expect(iterator1.next().value).to.be.undefined 192 | 193 | appendIncomingBuffer('\n* 2 FETCH (UID 2)\r\n') 194 | var iterator2 = client._iterateIncomingBuffer() 195 | expect(String.fromCharCode.apply(null, iterator2.next().value)).to.equal('* 1 FETCH (UID 1)') 196 | expect(String.fromCharCode.apply(null, iterator2.next().value)).to.equal('* 2 FETCH (UID 2)') 197 | expect(iterator2.next().value).to.be.undefined 198 | }) 199 | 200 | it('should process literal when literal count arrives in 2 parts', () => { 201 | appendIncomingBuffer('* 1 FETCH (UID {') 202 | var iterator1 = client._iterateIncomingBuffer() 203 | expect(iterator1.next().value).to.be.undefined 204 | 205 | appendIncomingBuffer('2}\r\n12)\r\n') 206 | var iterator2 = client._iterateIncomingBuffer() 207 | expect(String.fromCharCode.apply(null, iterator2.next().value)).to.equal('* 1 FETCH (UID {2}\r\n12)') 208 | expect(iterator2.next().value).to.be.undefined 209 | }) 210 | 211 | it('should process literal when literal count arrives in 2 parts 2', () => { 212 | appendIncomingBuffer('* 1 FETCH (UID {1') 213 | var iterator1 = client._iterateIncomingBuffer() 214 | expect(iterator1.next().value).to.be.undefined 215 | 216 | appendIncomingBuffer('0}\r\n0123456789)\r\n') 217 | var iterator2 = client._iterateIncomingBuffer() 218 | expect(String.fromCharCode.apply(null, iterator2.next().value)).to.equal('* 1 FETCH (UID {10}\r\n0123456789)') 219 | expect(iterator2.next().value).to.be.undefined 220 | }) 221 | 222 | it('should process literal when literal count arrives in 2 parts 3', () => { 223 | appendIncomingBuffer('* 1 FETCH (UID {') 224 | var iterator1 = client._iterateIncomingBuffer() 225 | expect(iterator1.next().value).to.be.undefined 226 | 227 | appendIncomingBuffer('10}\r\n1234567890)\r\n') 228 | var iterator2 = client._iterateIncomingBuffer() 229 | expect(String.fromCharCode.apply(null, iterator2.next().value)).to.equal('* 1 FETCH (UID {10}\r\n1234567890)') 230 | expect(iterator2.next().value).to.be.undefined 231 | }) 232 | 233 | it('should process literal when literal count arrives in 2 parts 4', () => { 234 | appendIncomingBuffer('* 1 FETCH (UID 1 BODY[HEADER.FIELDS (REFERENCES LIST-ID)] {2}\r') 235 | var iterator1 = client._iterateIncomingBuffer() 236 | expect(iterator1.next().value).to.be.undefined 237 | appendIncomingBuffer('\nXX)\r\n') 238 | var iterator2 = client._iterateIncomingBuffer() 239 | expect(String.fromCharCode.apply(null, iterator2.next().value)).to.equal('* 1 FETCH (UID 1 BODY[HEADER.FIELDS (REFERENCES LIST-ID)] {2}\r\nXX)') 240 | }) 241 | 242 | it('should process literal when literal count arrives in 3 parts', () => { 243 | appendIncomingBuffer('* 1 FETCH (UID {') 244 | var iterator1 = client._iterateIncomingBuffer() 245 | expect(iterator1.next().value).to.be.undefined 246 | 247 | appendIncomingBuffer('1') 248 | var iterator2 = client._iterateIncomingBuffer() 249 | expect(iterator2.next().value).to.be.undefined 250 | 251 | appendIncomingBuffer('}\r\n1)\r\n') 252 | var iterator3 = client._iterateIncomingBuffer() 253 | expect(String.fromCharCode.apply(null, iterator3.next().value)).to.equal('* 1 FETCH (UID {1}\r\n1)') 254 | expect(iterator3.next().value).to.be.undefined 255 | }) 256 | 257 | it('should process SEARCH response when it arrives in 2 parts', () => { 258 | appendIncomingBuffer('* SEARCH 1 2') 259 | var iterator1 = client._iterateIncomingBuffer() 260 | expect(iterator1.next().value).to.be.undefined 261 | 262 | appendIncomingBuffer(' 3 4\r\n') 263 | var iterator2 = client._iterateIncomingBuffer() 264 | expect(String.fromCharCode.apply(null, iterator2.next().value)).to.equal('* SEARCH 1 2 3 4') 265 | expect(iterator2.next().value).to.be.undefined 266 | }) 267 | 268 | it('should not process {} in string as literal 1', () => { 269 | appendIncomingBuffer('* 1 FETCH (UID 1 ENVELOPE ("string with {parenthesis}"))\r\n') 270 | var iterator = client._iterateIncomingBuffer() 271 | expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 1 FETCH (UID 1 ENVELOPE ("string with {parenthesis}"))') 272 | }) 273 | 274 | it('should not process {} in string as literal 2', () => { 275 | appendIncomingBuffer('* 1 FETCH (UID 1 ENVELOPE ("string with number in parenthesis {123}"))\r\n') 276 | var iterator = client._iterateIncomingBuffer() 277 | expect(String.fromCharCode.apply(null, iterator.next().value)).to.equal('* 1 FETCH (UID 1 ENVELOPE ("string with number in parenthesis {123}"))') 278 | }) 279 | 280 | function appendIncomingBuffer (content) { 281 | client._incomingBuffers.push(toTypedArray(content)) 282 | } 283 | }) 284 | 285 | describe('#_parseIncomingCommands', () => { 286 | it('should process a tagged item from the queue', () => { 287 | client.onready = sinon.stub() 288 | sinon.stub(client, '_handleResponse') 289 | 290 | function * gen () { yield toTypedArray('OK Hello world!') } 291 | 292 | client._parseIncomingCommands(gen()) 293 | 294 | expect(client.onready.callCount).to.equal(1) 295 | expect(client._handleResponse.withArgs({ 296 | tag: 'OK', 297 | command: 'Hello', 298 | attributes: [{ 299 | type: 'ATOM', 300 | value: 'world!' 301 | }] 302 | }).calledOnce).to.be.true 303 | }) 304 | 305 | it('should process an untagged item from the queue', () => { 306 | sinon.stub(client, '_handleResponse') 307 | 308 | function * gen () { yield toTypedArray('* 1 EXISTS') } 309 | 310 | client._parseIncomingCommands(gen()) 311 | 312 | expect(client._handleResponse.withArgs({ 313 | tag: '*', 314 | command: 'EXISTS', 315 | attributes: [], 316 | nr: 1 317 | }).calledOnce).to.be.true 318 | }) 319 | 320 | it('should process a plus tagged item from the queue', () => { 321 | sinon.stub(client, 'send') 322 | 323 | function * gen () { yield toTypedArray('+ Please continue') } 324 | client._currentCommand = { 325 | data: ['literal data'] 326 | } 327 | 328 | client._parseIncomingCommands(gen()) 329 | 330 | expect(client.send.withArgs('literal data\r\n').callCount).to.equal(1) 331 | }) 332 | 333 | it('should process an XOAUTH2 error challenge', () => { 334 | sinon.stub(client, 'send') 335 | 336 | function * gen () { yield toTypedArray('+ FOOBAR') } 337 | client._currentCommand = { 338 | data: [], 339 | errorResponseExpectsEmptyLine: true 340 | } 341 | 342 | client._parseIncomingCommands(gen()) 343 | 344 | expect(client.send.withArgs('\r\n').callCount).to.equal(1) 345 | }) 346 | }) 347 | 348 | describe('#_handleResponse', () => { 349 | it('should invoke global handler by default', () => { 350 | sinon.stub(client, '_processResponse') 351 | sinon.stub(client, '_sendRequest') 352 | 353 | client._globalAcceptUntagged.TEST = () => { } 354 | sinon.stub(client._globalAcceptUntagged, 'TEST') 355 | 356 | client._currentCommand = false 357 | client._handleResponse({ 358 | tag: '*', 359 | command: 'test' 360 | }) 361 | 362 | expect(client._sendRequest.callCount).to.equal(1) 363 | expect(client._globalAcceptUntagged.TEST.withArgs({ 364 | tag: '*', 365 | command: 'test' 366 | }).callCount).to.equal(1) 367 | }) 368 | 369 | it('should invoke global handler if needed', () => { 370 | sinon.stub(client, '_processResponse') 371 | client._globalAcceptUntagged.TEST = () => { } 372 | sinon.stub(client._globalAcceptUntagged, 'TEST') 373 | sinon.stub(client, '_sendRequest') 374 | 375 | client._currentCommand = { 376 | payload: {} 377 | } 378 | client._handleResponse({ 379 | tag: '*', 380 | command: 'test' 381 | }) 382 | 383 | expect(client._sendRequest.callCount).to.equal(0) 384 | expect(client._globalAcceptUntagged.TEST.withArgs({ 385 | tag: '*', 386 | command: 'test' 387 | }).callCount).to.equal(1) 388 | }) 389 | 390 | it('should push to payload', () => { 391 | sinon.stub(client, '_processResponse') 392 | client._globalAcceptUntagged.TEST = () => { } 393 | sinon.stub(client._globalAcceptUntagged, 'TEST') 394 | 395 | client._currentCommand = { 396 | payload: { 397 | TEST: [] 398 | } 399 | } 400 | client._handleResponse({ 401 | tag: '*', 402 | command: 'test' 403 | }) 404 | 405 | expect(client._globalAcceptUntagged.TEST.callCount).to.equal(0) 406 | expect(client._currentCommand.payload.TEST).to.deep.equal([{ 407 | tag: '*', 408 | command: 'test' 409 | }]) 410 | }) 411 | 412 | it('should invoke command callback', () => { 413 | sinon.stub(client, '_processResponse') 414 | sinon.stub(client, '_sendRequest') 415 | client._globalAcceptUntagged.TEST = () => { } 416 | sinon.stub(client._globalAcceptUntagged, 'TEST') 417 | 418 | client._currentCommand = { 419 | tag: 'A', 420 | callback: (response) => { 421 | expect(response).to.deep.equal({ 422 | tag: 'A', 423 | command: 'test', 424 | payload: { 425 | TEST: 'abc' 426 | } 427 | }) 428 | }, 429 | payload: { 430 | TEST: 'abc' 431 | } 432 | } 433 | client._handleResponse({ 434 | tag: 'A', 435 | command: 'test' 436 | }) 437 | 438 | expect(client._sendRequest.callCount).to.equal(1) 439 | expect(client._globalAcceptUntagged.TEST.callCount).to.equal(0) 440 | }) 441 | }) 442 | 443 | describe('#enqueueCommand', () => { 444 | it('should reject on NO/BAD', () => { 445 | sinon.stub(client, '_sendRequest').callsFake(() => { 446 | client._clientQueue[0].callback({ command: 'NO' }) 447 | }) 448 | 449 | client._tagCounter = 100 450 | client._clientQueue = [] 451 | client._canSend = true 452 | 453 | return client.enqueueCommand({ 454 | command: 'abc' 455 | }, ['def'], { 456 | t: 1 457 | }).catch((err) => { 458 | expect(err).to.exist 459 | expect(err.command).to.equal('NO') 460 | }) 461 | }) 462 | 463 | it('should invoke sending', () => { 464 | sinon.stub(client, '_sendRequest').callsFake(() => { 465 | client._clientQueue[0].callback({}) 466 | }) 467 | 468 | client._tagCounter = 100 469 | client._clientQueue = [] 470 | client._canSend = true 471 | 472 | return client.enqueueCommand({ 473 | command: 'abc' 474 | }, ['def'], { 475 | t: 1 476 | }).then(() => { 477 | expect(client._sendRequest.callCount).to.equal(1) 478 | expect(client._clientQueue.length).to.equal(1) 479 | expect(client._clientQueue[0].tag).to.equal('W101') 480 | expect(client._clientQueue[0].request).to.deep.equal({ 481 | command: 'abc', 482 | tag: 'W101' 483 | }) 484 | expect(client._clientQueue[0].t).to.equal(1) 485 | }) 486 | }) 487 | 488 | it('should only queue', () => { 489 | sinon.stub(client, '_sendRequest') 490 | 491 | client._tagCounter = 100 492 | client._clientQueue = [] 493 | client._canSend = false 494 | 495 | setTimeout(() => { client._clientQueue[0].callback({}) }, 0) 496 | 497 | return client.enqueueCommand({ 498 | command: 'abc' 499 | }, ['def'], { 500 | t: 1 501 | }).then(() => { 502 | expect(client._sendRequest.callCount).to.equal(0) 503 | expect(client._clientQueue.length).to.equal(1) 504 | expect(client._clientQueue[0].tag).to.equal('W101') 505 | }) 506 | }) 507 | 508 | it('should store valueAsString option in the command', () => { 509 | sinon.stub(client, '_sendRequest') 510 | 511 | client._tagCounter = 100 512 | client._clientQueue = [] 513 | client._canSend = false 514 | 515 | setTimeout(() => { client._clientQueue[0].callback({}) }, 0) 516 | return client.enqueueCommand({ 517 | command: 'abc', 518 | valueAsString: false 519 | }, ['def'], { 520 | t: 1 521 | }).then(() => { 522 | expect(client._clientQueue[0].request.valueAsString).to.equal(false) 523 | }) 524 | }) 525 | }) 526 | 527 | describe('#_sendRequest', () => { 528 | it('should enter idle if nothing is to process', () => { 529 | sinon.stub(client, '_enterIdle') 530 | 531 | client._clientQueue = [] 532 | client._sendRequest() 533 | 534 | expect(client._enterIdle.callCount).to.equal(1) 535 | }) 536 | 537 | it('should send data', () => { 538 | sinon.stub(client, '_clearIdle') 539 | sinon.stub(client, 'send') 540 | 541 | client._clientQueue = [{ 542 | request: { 543 | tag: 'W101', 544 | command: 'TEST' 545 | } 546 | }] 547 | client._sendRequest() 548 | 549 | expect(client._clearIdle.callCount).to.equal(1) 550 | expect(client.send.args[0][0]).to.equal('W101 TEST\r\n') 551 | }) 552 | 553 | it('should send partial data', () => { 554 | sinon.stub(client, '_clearIdle') 555 | sinon.stub(client, 'send') 556 | 557 | client._clientQueue = [{ 558 | request: { 559 | tag: 'W101', 560 | command: 'TEST', 561 | attributes: [{ 562 | type: 'LITERAL', 563 | value: 'abc' 564 | }] 565 | } 566 | }] 567 | client._sendRequest() 568 | 569 | expect(client._clearIdle.callCount).to.equal(1) 570 | expect(client.send.args[0][0]).to.equal('W101 TEST {3}\r\n') 571 | expect(client._currentCommand.data).to.deep.equal(['abc']) 572 | }) 573 | 574 | it('should run precheck', (done) => { 575 | sinon.stub(client, '_clearIdle') 576 | 577 | client._canSend = true 578 | client._clientQueue = [{ 579 | request: { 580 | tag: 'W101', 581 | command: 'TEST', 582 | attributes: [{ 583 | type: 'LITERAL', 584 | value: 'abc' 585 | }] 586 | }, 587 | precheck: (ctx) => { 588 | expect(ctx).to.exist 589 | expect(client._canSend).to.be.true 590 | client._sendRequest = () => { 591 | expect(client._clientQueue.length).to.equal(2) 592 | expect(client._clientQueue[0].tag).to.include('.p') 593 | expect(client._clientQueue[0].request.tag).to.include('.p') 594 | client._clearIdle.restore() 595 | done() 596 | } 597 | client.enqueueCommand({}, undefined, { 598 | ctx: ctx 599 | }) 600 | return Promise.resolve() 601 | } 602 | }] 603 | client._sendRequest() 604 | }) 605 | }) 606 | 607 | describe('#_enterIdle', () => { 608 | it('should set idle timer', (done) => { 609 | client.onidle = () => { 610 | done() 611 | } 612 | client.timeoutEnterIdle = 1 613 | 614 | client._enterIdle() 615 | }) 616 | }) 617 | 618 | describe('#_processResponse', () => { 619 | it('should set humanReadable', () => { 620 | var response = { 621 | tag: '*', 622 | command: 'OK', 623 | attributes: [{ 624 | type: 'TEXT', 625 | value: 'Some random text' 626 | }] 627 | } 628 | client._processResponse(response) 629 | 630 | expect(response.humanReadable).to.equal('Some random text') 631 | }) 632 | 633 | it('should set response code', () => { 634 | var response = { 635 | tag: '*', 636 | command: 'OK', 637 | attributes: [{ 638 | type: 'ATOM', 639 | section: [{ 640 | type: 'ATOM', 641 | value: 'CAPABILITY' 642 | }, { 643 | type: 'ATOM', 644 | value: 'IMAP4REV1' 645 | }, { 646 | type: 'ATOM', 647 | value: 'UIDPLUS' 648 | }] 649 | }, { 650 | type: 'TEXT', 651 | value: 'Some random text' 652 | }] 653 | } 654 | client._processResponse(response) 655 | expect(response.code).to.equal('CAPABILITY') 656 | expect(response.capability).to.deep.equal(['IMAP4REV1', 'UIDPLUS']) 657 | }) 658 | }) 659 | 660 | describe('#isError', () => { 661 | it('should detect if an object is an error', () => { 662 | expect(client.isError(new RangeError('abc'))).to.be.true 663 | expect(client.isError('abc')).to.be.false 664 | }) 665 | }) 666 | 667 | describe('#enableCompression', () => { 668 | it('should create inflater and deflater streams', () => { 669 | client.socket.ondata = () => { } 670 | sinon.stub(client.socket, 'ondata') 671 | 672 | expect(client.compressed).to.be.false 673 | client.enableCompression() 674 | expect(client.compressed).to.be.true 675 | 676 | const payload = 'asdasd' 677 | const expected = payload.split('').map(char => char.charCodeAt(0)) 678 | 679 | client.send(payload) 680 | const actualOut = socketStub.send.args[0][0] 681 | client.socket.ondata({ data: actualOut }) 682 | expect(Buffer.from(client._socketOnData.args[0][0].data)).to.deep.equal(Buffer.from(expected)) 683 | }) 684 | }) 685 | 686 | describe('#getPreviouslyQueued', () => { 687 | const ctx = {} 688 | 689 | it('should return undefined with empty queue and no current command', () => { 690 | client._currentCommand = undefined 691 | client._clientQueue = [] 692 | 693 | expect(testAndGetAttribute()).to.be.undefined 694 | }) 695 | 696 | it('should return undefined with empty queue and non-SELECT current command', () => { 697 | client._currentCommand = createCommand('TEST') 698 | client._clientQueue = [] 699 | 700 | expect(testAndGetAttribute()).to.be.undefined 701 | }) 702 | 703 | it('should return current command with empty queue and SELECT current command', () => { 704 | client._currentCommand = createCommand('SELECT', 'ATTR') 705 | client._clientQueue = [] 706 | 707 | expect(testAndGetAttribute()).to.equal('ATTR') 708 | }) 709 | 710 | it('should return current command with non-SELECT commands in queue and SELECT current command', () => { 711 | client._currentCommand = createCommand('SELECT', 'ATTR') 712 | client._clientQueue = [ 713 | createCommand('TEST01'), 714 | createCommand('TEST02') 715 | ] 716 | 717 | expect(testAndGetAttribute()).to.equal('ATTR') 718 | }) 719 | 720 | it('should return last SELECT before ctx with multiple SELECT commands in queue (1)', () => { 721 | client._currentCommand = createCommand('SELECT', 'ATTR01') 722 | client._clientQueue = [ 723 | createCommand('SELECT', 'ATTR'), 724 | createCommand('TEST'), 725 | ctx, 726 | createCommand('SELECT', 'ATTR03') 727 | ] 728 | 729 | expect(testAndGetAttribute()).to.equal('ATTR') 730 | }) 731 | 732 | it('should return last SELECT before ctx with multiple SELECT commands in queue (2)', () => { 733 | client._clientQueue = [ 734 | createCommand('SELECT', 'ATTR02'), 735 | createCommand('SELECT', 'ATTR'), 736 | ctx, 737 | createCommand('SELECT', 'ATTR03') 738 | ] 739 | 740 | expect(testAndGetAttribute()).to.equal('ATTR') 741 | }) 742 | 743 | it('should return last SELECT before ctx with multiple SELECT commands in queue (3)', () => { 744 | client._clientQueue = [ 745 | createCommand('SELECT', 'ATTR02'), 746 | createCommand('SELECT', 'ATTR'), 747 | createCommand('TEST'), 748 | ctx, 749 | createCommand('SELECT', 'ATTR03') 750 | ] 751 | 752 | expect(testAndGetAttribute()).to.equal('ATTR') 753 | }) 754 | 755 | function testAndGetAttribute () { 756 | const data = client.getPreviouslyQueued(['SELECT'], ctx) 757 | if (data) { 758 | return data.request.attributes[0].value 759 | } 760 | } 761 | 762 | function createCommand (command, attribute) { 763 | const attributes = [] 764 | const data = { 765 | request: { command, attributes } 766 | } 767 | 768 | if (attribute) { 769 | data.request.attributes.push({ 770 | type: 'STRING', 771 | value: attribute 772 | }) 773 | } 774 | 775 | return data 776 | } 777 | }) 778 | }) 779 | -------------------------------------------------------------------------------- /src/imap.js: -------------------------------------------------------------------------------- 1 | import { propOr } from 'ramda' 2 | import TCPSocket from 'emailjs-tcp-socket' 3 | import { toTypedArray, fromTypedArray } from './common' 4 | import { parser, compiler } from 'emailjs-imap-handler' 5 | import Compression from './compression' 6 | import CompressionBlob from '../res/compression.worker.blob' 7 | 8 | // 9 | // constants used for communication with the worker 10 | // 11 | const MESSAGE_INITIALIZE_WORKER = 'start' 12 | const MESSAGE_INFLATE = 'inflate' 13 | const MESSAGE_INFLATED_DATA_READY = 'inflated_ready' 14 | const MESSAGE_DEFLATE = 'deflate' 15 | const MESSAGE_DEFLATED_DATA_READY = 'deflated_ready' 16 | 17 | const EOL = '\r\n' 18 | const LINE_FEED = 10 19 | const CARRIAGE_RETURN = 13 20 | const LEFT_CURLY_BRACKET = 123 21 | const RIGHT_CURLY_BRACKET = 125 22 | 23 | const ASCII_PLUS = 43 24 | 25 | // State tracking when constructing an IMAP command from buffers. 26 | const BUFFER_STATE_LITERAL = 'literal' 27 | const BUFFER_STATE_POSSIBLY_LITERAL_LENGTH_1 = 'literal_length_1' 28 | const BUFFER_STATE_POSSIBLY_LITERAL_LENGTH_2 = 'literal_length_2' 29 | const BUFFER_STATE_DEFAULT = 'default' 30 | 31 | /** 32 | * How much time to wait since the last response until the connection is considered idling 33 | */ 34 | const TIMEOUT_ENTER_IDLE = 1000 35 | 36 | /** 37 | * Lower Bound for socket timeout to wait since the last data was written to a socket 38 | */ 39 | const TIMEOUT_SOCKET_LOWER_BOUND = 10000 40 | 41 | /** 42 | * Multiplier for socket timeout: 43 | * 44 | * We assume at least a GPRS connection with 115 kb/s = 14,375 kB/s tops, so 10 KB/s to be on 45 | * the safe side. We can timeout after a lower bound of 10s + (n KB / 10 KB/s). A 1 MB message 46 | * upload would be 110 seconds to wait for the timeout. 10 KB/s === 0.1 s/B 47 | */ 48 | const TIMEOUT_SOCKET_MULTIPLIER = 0.1 49 | 50 | /** 51 | * Creates a connection object to an IMAP server. Call `connect` method to inititate 52 | * the actual connection, the constructor only defines the properties but does not actually connect. 53 | * 54 | * @constructor 55 | * 56 | * @param {String} [host='localhost'] Hostname to conenct to 57 | * @param {Number} [port=143] Port number to connect to 58 | * @param {Object} [options] Optional options object 59 | * @param {Boolean} [options.useSecureTransport] Set to true, to use encrypted connection 60 | * @param {String} [options.compressionWorkerPath] offloads de-/compression computation to a web worker, this is the path to the browserified emailjs-compressor-worker.js 61 | */ 62 | export default class Imap { 63 | constructor (host, port, options = {}) { 64 | this.timeoutEnterIdle = TIMEOUT_ENTER_IDLE 65 | this.timeoutSocketLowerBound = TIMEOUT_SOCKET_LOWER_BOUND 66 | this.timeoutSocketMultiplier = TIMEOUT_SOCKET_MULTIPLIER 67 | 68 | this.options = options 69 | 70 | this.port = port || (this.options.useSecureTransport ? 993 : 143) 71 | this.host = host || 'localhost' 72 | 73 | // Use a TLS connection. Port 993 also forces TLS. 74 | this.options.useSecureTransport = 'useSecureTransport' in this.options ? !!this.options.useSecureTransport : this.port === 993 75 | 76 | this.secureMode = !!this.options.useSecureTransport // Does the connection use SSL/TLS 77 | 78 | this._connectionReady = false // Is the conection established and greeting is received from the server 79 | 80 | this._globalAcceptUntagged = {} // Global handlers for unrelated responses (EXPUNGE, EXISTS etc.) 81 | 82 | this._clientQueue = [] // Queue of outgoing commands 83 | this._canSend = false // Is it OK to send something to the server 84 | this._tagCounter = 0 // Counter to allow uniqueue imap tags 85 | this._currentCommand = false // Current command that is waiting for response from the server 86 | 87 | this._idleTimer = false // Timer waiting to enter idle 88 | this._socketTimeoutTimer = false // Timer waiting to declare the socket dead starting from the last write 89 | 90 | this.compressed = false // Is the connection compressed and needs inflating/deflating 91 | 92 | // 93 | // HELPERS 94 | // 95 | 96 | // As the server sends data in chunks, it needs to be split into separate lines. Helps parsing the input. 97 | this._incomingBuffers = [] 98 | this._bufferState = BUFFER_STATE_DEFAULT 99 | this._literalRemaining = 0 100 | 101 | // 102 | // Event placeholders, may be overriden with callback functions 103 | // 104 | this.oncert = null 105 | this.onerror = null // Irrecoverable error occurred. Connection to the server will be closed automatically. 106 | this.onready = null // The connection to the server has been established and greeting is received 107 | this.onidle = null // There are no more commands to process 108 | } 109 | 110 | // PUBLIC METHODS 111 | 112 | /** 113 | * Initiate a connection to the server. Wait for onready event 114 | * 115 | * @param {Object} Socket 116 | * TESTING ONLY! The TCPSocket has a pretty nonsensical convenience constructor, 117 | * which makes it hard to mock. For dependency-injection purposes, we use the 118 | * Socket parameter to pass in a mock Socket implementation. Should be left blank 119 | * in production use! 120 | * @returns {Promise} Resolves when socket is opened 121 | */ 122 | connect (Socket = TCPSocket) { 123 | return new Promise((resolve, reject) => { 124 | this.socket = Socket.open(this.host, this.port, { 125 | binaryType: 'arraybuffer', 126 | useSecureTransport: this.secureMode, 127 | ca: this.options.ca 128 | }) 129 | 130 | // allows certificate handling for platform w/o native tls support 131 | // oncert is non standard so setting it might throw if the socket object is immutable 132 | try { 133 | this.socket.oncert = (cert) => { this.oncert && this.oncert(cert) } 134 | } catch (E) { } 135 | 136 | // Connection closing unexpected is an error 137 | this.socket.onclose = () => this._onError(new Error('Socket closed unexpectedly!')) 138 | this.socket.ondata = (evt) => { 139 | try { 140 | this._onData(evt) 141 | } catch (err) { 142 | this._onError(err) 143 | } 144 | } 145 | 146 | // if an error happens during create time, reject the promise 147 | this.socket.onerror = (e) => { 148 | reject(new Error('Could not open socket: ' + e.data.message)) 149 | } 150 | 151 | this.socket.onopen = () => { 152 | // use proper "irrecoverable error, tear down everything"-handler only after socket is open 153 | this.socket.onerror = (e) => this._onError(e) 154 | resolve() 155 | } 156 | }) 157 | } 158 | 159 | /** 160 | * Closes the connection to the server 161 | * 162 | * @returns {Promise} Resolves when the socket is closed 163 | */ 164 | close (error) { 165 | return new Promise((resolve) => { 166 | var tearDown = () => { 167 | // fulfill pending promises 168 | this._clientQueue.forEach(cmd => cmd.callback(error)) 169 | if (this._currentCommand) { 170 | this._currentCommand.callback(error) 171 | } 172 | 173 | this._clientQueue = [] 174 | this._currentCommand = false 175 | 176 | clearTimeout(this._idleTimer) 177 | this._idleTimer = null 178 | 179 | clearTimeout(this._socketTimeoutTimer) 180 | this._socketTimeoutTimer = null 181 | 182 | if (this.socket) { 183 | // remove all listeners 184 | this.socket.onopen = null 185 | this.socket.onclose = null 186 | this.socket.ondata = null 187 | this.socket.onerror = null 188 | try { 189 | this.socket.oncert = null 190 | } catch (E) { } 191 | 192 | this.socket = null 193 | } 194 | 195 | resolve() 196 | } 197 | 198 | this._disableCompression() 199 | 200 | if (!this.socket || this.socket.readyState !== 'open') { 201 | return tearDown() 202 | } 203 | 204 | this.socket.onclose = this.socket.onerror = tearDown // we don't really care about the error here 205 | this.socket.close() 206 | }) 207 | } 208 | 209 | /** 210 | * Send LOGOUT to the server. 211 | * 212 | * Use is discouraged! 213 | * 214 | * @returns {Promise} Resolves when connection is closed by server. 215 | */ 216 | logout () { 217 | return new Promise((resolve, reject) => { 218 | this.socket.onclose = this.socket.onerror = () => { 219 | this.close('Client logging out').then(resolve).catch(reject) 220 | } 221 | 222 | this.enqueueCommand('LOGOUT') 223 | }) 224 | } 225 | 226 | /** 227 | * Initiates TLS handshake 228 | */ 229 | upgrade () { 230 | this.secureMode = true 231 | this.socket.upgradeToSecure() 232 | } 233 | 234 | /** 235 | * Schedules a command to be sent to the server. 236 | * See https://github.com/emailjs/emailjs-imap-handler for request structure. 237 | * Do not provide a tag property, it will be set by the queue manager. 238 | * 239 | * To catch untagged responses use acceptUntagged property. For example, if 240 | * the value for it is 'FETCH' then the reponse includes 'payload.FETCH' property 241 | * that is an array including all listed * FETCH responses. 242 | * 243 | * @param {Object} request Structured request object 244 | * @param {Array} acceptUntagged a list of untagged responses that will be included in 'payload' property 245 | * @param {Object} [options] Optional data for the command payload 246 | * @returns {Promise} Promise that resolves when the corresponding response was received 247 | */ 248 | enqueueCommand (request, acceptUntagged, options) { 249 | if (typeof request === 'string') { 250 | request = { 251 | command: request 252 | } 253 | } 254 | 255 | acceptUntagged = [].concat(acceptUntagged || []).map((untagged) => (untagged || '').toString().toUpperCase().trim()) 256 | 257 | var tag = 'W' + (++this._tagCounter) 258 | request.tag = tag 259 | 260 | return new Promise((resolve, reject) => { 261 | var data = { 262 | tag: tag, 263 | request: request, 264 | payload: acceptUntagged.length ? {} : undefined, 265 | callback: (response) => { 266 | if (this.isError(response)) { 267 | return reject(response) 268 | } else { 269 | const command = propOr('', 'command', response).toUpperCase().trim() 270 | if (['NO', 'BAD'].includes(command)) { 271 | var error = new Error(response.humanReadable || 'Error') 272 | error.command = command 273 | if (response.code) { 274 | error.code = response.code 275 | } 276 | return reject(error) 277 | } 278 | } 279 | 280 | resolve(response) 281 | } 282 | } 283 | 284 | // apply any additional options to the command 285 | Object.keys(options || {}).forEach((key) => { data[key] = options[key] }) 286 | 287 | acceptUntagged.forEach((command) => { data.payload[command] = [] }) 288 | 289 | // if we're in priority mode (i.e. we ran commands in a precheck), 290 | // queue any commands BEFORE the command that contianed the precheck, 291 | // otherwise just queue command as usual 292 | var index = data.ctx ? this._clientQueue.indexOf(data.ctx) : -1 293 | if (index >= 0) { 294 | data.tag += '.p' 295 | data.request.tag += '.p' 296 | this._clientQueue.splice(index, 0, data) 297 | } else { 298 | this._clientQueue.push(data) 299 | } 300 | 301 | if (this._canSend) { 302 | this._sendRequest() 303 | } 304 | }) 305 | } 306 | 307 | /** 308 | * 309 | * @param commands 310 | * @param ctx 311 | * @returns {*} 312 | */ 313 | getPreviouslyQueued (commands, ctx) { 314 | const startIndex = this._clientQueue.indexOf(ctx) - 1 315 | 316 | // search backwards for the commands and return the first found 317 | for (let i = startIndex; i >= 0; i--) { 318 | if (isMatch(this._clientQueue[i])) { 319 | return this._clientQueue[i] 320 | } 321 | } 322 | 323 | // also check current command if no SELECT is queued 324 | if (isMatch(this._currentCommand)) { 325 | return this._currentCommand 326 | } 327 | 328 | return false 329 | 330 | function isMatch (data) { 331 | return data && data.request && commands.indexOf(data.request.command) >= 0 332 | } 333 | } 334 | 335 | /** 336 | * Send data to the TCP socket 337 | * Arms a timeout waiting for a response from the server. 338 | * 339 | * @param {String} str Payload 340 | */ 341 | send (str) { 342 | const buffer = toTypedArray(str).buffer 343 | this._resetSocketTimeout(buffer.byteLength) 344 | 345 | if (this.compressed) { 346 | this._sendCompressed(buffer) 347 | } else { 348 | this.socket.send(buffer) 349 | } 350 | } 351 | 352 | /** 353 | * Set a global handler for an untagged response. If currently processed command 354 | * has not listed untagged command it is forwarded to the global handler. Useful 355 | * with EXPUNGE, EXISTS etc. 356 | * 357 | * @param {String} command Untagged command name 358 | * @param {Function} callback Callback function with response object and continue callback function 359 | */ 360 | setHandler (command, callback) { 361 | this._globalAcceptUntagged[command.toUpperCase().trim()] = callback 362 | } 363 | 364 | // INTERNAL EVENTS 365 | 366 | /** 367 | * Error handler for the socket 368 | * 369 | * @event 370 | * @param {Event} evt Event object. See evt.data for the error 371 | */ 372 | _onError (evt) { 373 | var error 374 | if (this.isError(evt)) { 375 | error = evt 376 | } else if (evt && this.isError(evt.data)) { 377 | error = evt.data 378 | } else { 379 | error = new Error((evt && evt.data && evt.data.message) || evt.data || evt || 'Error') 380 | } 381 | 382 | // only log here if no onerror handler is supplied 383 | if (!this.onerror) { 384 | this.logger.error(error) 385 | } 386 | 387 | // always call onerror callback, no matter if close() succeeds or fails 388 | this.close(error).then(() => { 389 | this.onerror && this.onerror(error) 390 | }, () => { 391 | this.onerror && this.onerror(error) 392 | }) 393 | } 394 | 395 | /** 396 | * Handler for incoming data from the server. The data is sent in arbitrary 397 | * chunks and can't be used directly so this function makes sure the data 398 | * is split into complete lines before the data is passed to the command 399 | * handler 400 | * 401 | * @param {Event} evt 402 | */ 403 | _onData (evt) { 404 | // reset the timeout on each data packet 405 | this._resetSocketTimeout() 406 | 407 | this._incomingBuffers.push(new Uint8Array(evt.data)) // append to the incoming buffer 408 | this._parseIncomingCommands(this._iterateIncomingBuffer()) // Consume the incoming buffer 409 | } 410 | 411 | * _iterateIncomingBuffer () { 412 | let buf = this._incomingBuffers[this._incomingBuffers.length - 1] || [] 413 | let i = 0 414 | 415 | // loop invariant: 416 | // this._incomingBuffers starts with the beginning of incoming command. 417 | // buf is shorthand for last element of this._incomingBuffers. 418 | // buf[0..i-1] is part of incoming command. 419 | while (i < buf.length) { 420 | switch (this._bufferState) { 421 | case BUFFER_STATE_LITERAL: 422 | const diff = Math.min(buf.length - i, this._literalRemaining) 423 | this._literalRemaining -= diff 424 | i += diff 425 | if (this._literalRemaining === 0) { 426 | this._bufferState = BUFFER_STATE_DEFAULT 427 | } 428 | continue 429 | 430 | case BUFFER_STATE_POSSIBLY_LITERAL_LENGTH_2: 431 | if (i < buf.length) { 432 | if (buf[i] === CARRIAGE_RETURN) { 433 | this._literalRemaining = Number(fromTypedArray(this._lengthBuffer)) + 2 // for CRLF 434 | this._bufferState = BUFFER_STATE_LITERAL 435 | } else { 436 | this._bufferState = BUFFER_STATE_DEFAULT 437 | } 438 | delete this._lengthBuffer 439 | } 440 | continue 441 | 442 | case BUFFER_STATE_POSSIBLY_LITERAL_LENGTH_1: 443 | const start = i 444 | while (i < buf.length && buf[i] >= 48 && buf[i] <= 57) { // digits 445 | i++ 446 | } 447 | if (start !== i) { 448 | const latest = buf.subarray(start, i) 449 | const prevBuf = this._lengthBuffer 450 | this._lengthBuffer = new Uint8Array(prevBuf.length + latest.length) 451 | this._lengthBuffer.set(prevBuf) 452 | this._lengthBuffer.set(latest, prevBuf.length) 453 | } 454 | if (i < buf.length) { 455 | if (this._lengthBuffer.length > 0 && buf[i] === RIGHT_CURLY_BRACKET) { 456 | this._bufferState = BUFFER_STATE_POSSIBLY_LITERAL_LENGTH_2 457 | } else { 458 | delete this._lengthBuffer 459 | this._bufferState = BUFFER_STATE_DEFAULT 460 | } 461 | i++ 462 | } 463 | continue 464 | 465 | default: 466 | // find literal length 467 | const leftIdx = buf.indexOf(LEFT_CURLY_BRACKET, i) 468 | if (leftIdx > -1) { 469 | const leftOfLeftCurly = new Uint8Array(buf.buffer, i, leftIdx - i) 470 | if (leftOfLeftCurly.indexOf(LINE_FEED) === -1) { 471 | i = leftIdx + 1 472 | this._lengthBuffer = new Uint8Array(0) 473 | this._bufferState = BUFFER_STATE_POSSIBLY_LITERAL_LENGTH_1 474 | continue 475 | } 476 | } 477 | 478 | // find end of command 479 | const LFidx = buf.indexOf(LINE_FEED, i) 480 | if (LFidx > -1) { 481 | if (LFidx < buf.length - 1) { 482 | this._incomingBuffers[this._incomingBuffers.length - 1] = new Uint8Array(buf.buffer, 0, LFidx + 1) 483 | } 484 | const commandLength = this._incomingBuffers.reduce((prev, curr) => prev + curr.length, 0) - 2 // 2 for CRLF 485 | const command = new Uint8Array(commandLength) 486 | let index = 0 487 | while (this._incomingBuffers.length > 0) { 488 | let uint8Array = this._incomingBuffers.shift() 489 | 490 | const remainingLength = commandLength - index 491 | if (uint8Array.length > remainingLength) { 492 | const excessLength = uint8Array.length - remainingLength 493 | uint8Array = uint8Array.subarray(0, -excessLength) 494 | 495 | if (this._incomingBuffers.length > 0) { 496 | this._incomingBuffers = [] 497 | } 498 | } 499 | command.set(uint8Array, index) 500 | index += uint8Array.length 501 | } 502 | yield command 503 | if (LFidx < buf.length - 1) { 504 | buf = new Uint8Array(buf.subarray(LFidx + 1)) 505 | this._incomingBuffers.push(buf) 506 | i = 0 507 | } else { 508 | // clear the timeout when an entire command has arrived 509 | // and not waiting on more data for next command 510 | clearTimeout(this._socketTimeoutTimer) 511 | this._socketTimeoutTimer = null 512 | return 513 | } 514 | } else { 515 | return 516 | } 517 | } 518 | } 519 | } 520 | 521 | // PRIVATE METHODS 522 | 523 | /** 524 | * Processes a command from the queue. The command is parsed and feeded to a handler 525 | */ 526 | _parseIncomingCommands (commands) { 527 | for (var command of commands) { 528 | this._clearIdle() 529 | 530 | /* 531 | * The "+"-tagged response is a special case: 532 | * Either the server can asks for the next chunk of data, e.g. for the AUTHENTICATE command. 533 | * 534 | * Or there was an error in the XOAUTH2 authentication, for which SASL initial client response extension 535 | * dictates the client sends an empty EOL response to the challenge containing the error message. 536 | * 537 | * Details on "+"-tagged response: 538 | * https://tools.ietf.org/html/rfc3501#section-2.2.1 539 | */ 540 | // 541 | if (command[0] === ASCII_PLUS) { 542 | if (this._currentCommand.data.length) { 543 | // feed the next chunk of data 544 | var chunk = this._currentCommand.data.shift() 545 | chunk += (!this._currentCommand.data.length ? EOL : '') // EOL if there's nothing more to send 546 | this.send(chunk) 547 | } else if (this._currentCommand.errorResponseExpectsEmptyLine) { 548 | this.send(EOL) // XOAUTH2 empty response, error will be reported when server continues with NO response 549 | } 550 | continue 551 | } 552 | 553 | var response 554 | try { 555 | const valueAsString = this._currentCommand.request && this._currentCommand.request.valueAsString 556 | response = parser(command, { valueAsString }) 557 | this.logger.debug('S:', () => compiler(response, false, true)) 558 | } catch (e) { 559 | this.logger.error('Error parsing imap command!', response) 560 | return this._onError(e) 561 | } 562 | 563 | this._processResponse(response) 564 | this._handleResponse(response) 565 | 566 | // first response from the server, connection is now usable 567 | if (!this._connectionReady) { 568 | this._connectionReady = true 569 | this.onready && this.onready() 570 | } 571 | } 572 | } 573 | 574 | /** 575 | * Feeds a parsed response object to an appropriate handler 576 | * 577 | * @param {Object} response Parsed command object 578 | */ 579 | _handleResponse (response) { 580 | var command = propOr('', 'command', response).toUpperCase().trim() 581 | 582 | if (!this._currentCommand) { 583 | // unsolicited untagged response 584 | if (response.tag === '*' && command in this._globalAcceptUntagged) { 585 | this._globalAcceptUntagged[command](response) 586 | this._canSend = true 587 | this._sendRequest() 588 | } 589 | } else if (this._currentCommand.payload && response.tag === '*' && command in this._currentCommand.payload) { 590 | // expected untagged response 591 | this._currentCommand.payload[command].push(response) 592 | // still expecting more data for the response of the current command 593 | this._resetSocketTimeout() 594 | } else if (response.tag === '*' && command in this._globalAcceptUntagged) { 595 | // unexpected untagged response 596 | this._globalAcceptUntagged[command](response) 597 | // still expecting more data for the response of the current command 598 | this._resetSocketTimeout() 599 | } else if (response.tag === this._currentCommand.tag) { 600 | // tagged response 601 | if (this._currentCommand.payload && Object.keys(this._currentCommand.payload).length) { 602 | response.payload = this._currentCommand.payload 603 | } 604 | this._currentCommand.callback(response) 605 | this._canSend = true 606 | this._sendRequest() 607 | } 608 | } 609 | 610 | /** 611 | * Sends a command from client queue to the server. 612 | */ 613 | _sendRequest () { 614 | if (!this._clientQueue.length) { 615 | this._currentCommand = false 616 | return this._enterIdle() 617 | } 618 | this._clearIdle() 619 | 620 | // an operation was made in the precheck, no need to restart the queue manually 621 | this._restartQueue = false 622 | 623 | var command = this._clientQueue[0] 624 | if (typeof command.precheck === 'function') { 625 | // remember the context 626 | var context = command 627 | var precheck = context.precheck 628 | delete context.precheck 629 | 630 | // we need to restart the queue handling if no operation was made in the precheck 631 | this._restartQueue = true 632 | 633 | // invoke the precheck command and resume normal operation after the promise resolves 634 | precheck(context).then(() => { 635 | // we're done with the precheck 636 | if (this._restartQueue) { 637 | // we need to restart the queue handling 638 | this._sendRequest() 639 | } 640 | }).catch((err) => { 641 | // precheck failed, so we remove the initial command 642 | // from the queue, invoke its callback and resume normal operation 643 | let cmd 644 | const index = this._clientQueue.indexOf(context) 645 | if (index >= 0) { 646 | cmd = this._clientQueue.splice(index, 1)[0] 647 | } 648 | if (cmd && cmd.callback) { 649 | cmd.callback(err) 650 | this._canSend = true 651 | this._parseIncomingCommands(this._iterateIncomingBuffer()) // Consume the rest of the incoming buffer 652 | this._sendRequest() // continue sending 653 | } 654 | }) 655 | return 656 | } 657 | 658 | this._canSend = false 659 | this._currentCommand = this._clientQueue.shift() 660 | 661 | try { 662 | this._currentCommand.data = compiler(this._currentCommand.request, true) 663 | this.logger.debug('C:', () => compiler(this._currentCommand.request, false, true)) // excludes passwords etc. 664 | } catch (e) { 665 | this.logger.error('Error compiling imap command!', this._currentCommand.request) 666 | return this._onError(new Error('Error compiling imap command!')) 667 | } 668 | 669 | var data = this._currentCommand.data.shift() 670 | 671 | this.send(data + (!this._currentCommand.data.length ? EOL : '')) 672 | return this.waitDrain 673 | } 674 | 675 | /** 676 | * Emits onidle, noting to do currently 677 | */ 678 | _enterIdle () { 679 | clearTimeout(this._idleTimer) 680 | this._idleTimer = setTimeout(() => (this.onidle && this.onidle()), this.timeoutEnterIdle) 681 | } 682 | 683 | /** 684 | * Cancel idle timer 685 | */ 686 | _clearIdle () { 687 | clearTimeout(this._idleTimer) 688 | this._idleTimer = null 689 | } 690 | 691 | /** 692 | * Method processes a response into an easier to handle format. 693 | * Add untagged numbered responses (e.g. FETCH) into a nicely feasible form 694 | * Checks if a response includes optional response codes 695 | * and copies these into separate properties. For example the 696 | * following response includes a capability listing and a human 697 | * readable message: 698 | * 699 | * * OK [CAPABILITY ID NAMESPACE] All ready 700 | * 701 | * This method adds a 'capability' property with an array value ['ID', 'NAMESPACE'] 702 | * to the response object. Additionally 'All ready' is added as 'humanReadable' property. 703 | * 704 | * See possiblem IMAP Response Codes at https://tools.ietf.org/html/rfc5530 705 | * 706 | * @param {Object} response Parsed response object 707 | */ 708 | _processResponse (response) { 709 | const command = propOr('', 'command', response).toUpperCase().trim() 710 | 711 | // no attributes 712 | if (!response || !response.attributes || !response.attributes.length) { 713 | return 714 | } 715 | 716 | // untagged responses w/ sequence numbers 717 | if (response.tag === '*' && /^\d+$/.test(response.command) && response.attributes[0].type === 'ATOM') { 718 | response.nr = Number(response.command) 719 | response.command = (response.attributes.shift().value || '').toString().toUpperCase().trim() 720 | } 721 | 722 | // no optional response code 723 | if (['OK', 'NO', 'BAD', 'BYE', 'PREAUTH'].indexOf(command) < 0) { 724 | return 725 | } 726 | 727 | // If last element of the response is TEXT then this is for humans 728 | if (response.attributes[response.attributes.length - 1].type === 'TEXT') { 729 | response.humanReadable = response.attributes[response.attributes.length - 1].value 730 | } 731 | 732 | // Parse and format ATOM values 733 | if (response.attributes[0].type === 'ATOM' && response.attributes[0].section) { 734 | const option = response.attributes[0].section.map((key) => { 735 | if (!key) { 736 | return 737 | } 738 | if (Array.isArray(key)) { 739 | return key.map((key) => (key.value || '').toString().trim()) 740 | } else { 741 | return (key.value || '').toString().toUpperCase().trim() 742 | } 743 | }) 744 | 745 | const key = option.shift() 746 | response.code = key 747 | 748 | if (option.length === 1) { 749 | response[key.toLowerCase()] = option[0] 750 | } else if (option.length > 1) { 751 | response[key.toLowerCase()] = option 752 | } 753 | } 754 | } 755 | 756 | /** 757 | * Checks if a value is an Error object 758 | * 759 | * @param {Mixed} value Value to be checked 760 | * @return {Boolean} returns true if the value is an Error 761 | */ 762 | isError (value) { 763 | return !!Object.prototype.toString.call(value).match(/Error\]$/) 764 | } 765 | 766 | // COMPRESSION RELATED METHODS 767 | 768 | /** 769 | * Sets up deflate/inflate for the IO 770 | */ 771 | enableCompression () { 772 | this._socketOnData = this.socket.ondata 773 | this.compressed = true 774 | 775 | if (typeof window !== 'undefined' && window.Worker) { 776 | this._compressionWorker = new Worker(URL.createObjectURL(new Blob([CompressionBlob]))) 777 | this._compressionWorker.onmessage = (e) => { 778 | var message = e.data.message 779 | var data = e.data.buffer 780 | 781 | switch (message) { 782 | case MESSAGE_INFLATED_DATA_READY: 783 | this._socketOnData({ data }) 784 | break 785 | 786 | case MESSAGE_DEFLATED_DATA_READY: 787 | this.waitDrain = this.socket.send(data) 788 | break 789 | } 790 | } 791 | 792 | this._compressionWorker.onerror = (e) => { 793 | this._onError(new Error('Error handling compression web worker: ' + e.message)) 794 | } 795 | 796 | this._compressionWorker.postMessage(createMessage(MESSAGE_INITIALIZE_WORKER)) 797 | } else { 798 | const inflatedReady = (buffer) => { this._socketOnData({ data: buffer }) } 799 | const deflatedReady = (buffer) => { this.waitDrain = this.socket.send(buffer) } 800 | this._compression = new Compression(inflatedReady, deflatedReady) 801 | } 802 | 803 | // override data handler, decompress incoming data 804 | this.socket.ondata = (evt) => { 805 | if (!this.compressed) { 806 | return 807 | } 808 | 809 | if (this._compressionWorker) { 810 | this._compressionWorker.postMessage(createMessage(MESSAGE_INFLATE, evt.data), [evt.data]) 811 | } else { 812 | this._compression.inflate(evt.data) 813 | } 814 | } 815 | } 816 | 817 | /** 818 | * Undoes any changes related to compression. This only be called when closing the connection 819 | */ 820 | _disableCompression () { 821 | if (!this.compressed) { 822 | return 823 | } 824 | 825 | this.compressed = false 826 | this.socket.ondata = this._socketOnData 827 | this._socketOnData = null 828 | 829 | if (this._compressionWorker) { 830 | // terminate the worker 831 | this._compressionWorker.terminate() 832 | this._compressionWorker = null 833 | } 834 | } 835 | 836 | /** 837 | * Outgoing payload needs to be compressed and sent to socket 838 | * 839 | * @param {ArrayBuffer} buffer Outgoing uncompressed arraybuffer 840 | */ 841 | _sendCompressed (buffer) { 842 | // deflate 843 | if (this._compressionWorker) { 844 | this._compressionWorker.postMessage(createMessage(MESSAGE_DEFLATE, buffer), [buffer]) 845 | } else { 846 | this._compression.deflate(buffer) 847 | } 848 | } 849 | 850 | _resetSocketTimeout (byteLength) { 851 | clearTimeout(this._socketTimeoutTimer) 852 | const timeout = this.timeoutSocketLowerBound + Math.floor((byteLength || 4096) * this.timeoutSocketMultiplier) // max packet size is 4096 bytes 853 | this._socketTimeoutTimer = setTimeout(() => this._onError(new Error(' Socket timed out!')), timeout) 854 | } 855 | } 856 | 857 | const createMessage = (message, buffer) => ({ message, buffer }) 858 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import ImapClient from './client' 2 | 3 | export { 4 | LOG_LEVEL_NONE, 5 | LOG_LEVEL_ERROR, 6 | LOG_LEVEL_WARN, 7 | LOG_LEVEL_INFO, 8 | LOG_LEVEL_DEBUG, 9 | LOG_LEVEL_ALL 10 | } from './common' 11 | 12 | export default ImapClient 13 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOG_LEVEL_ERROR, 3 | LOG_LEVEL_WARN, 4 | LOG_LEVEL_INFO, 5 | LOG_LEVEL_DEBUG 6 | } from './common' 7 | 8 | let SESSIONCOUNTER = 0 9 | 10 | export default function createDefaultLogger (username, hostname) { 11 | const session = ++SESSIONCOUNTER 12 | const log = (level, messages) => { 13 | messages = messages.map(msg => typeof msg === 'function' ? msg() : msg) 14 | const date = new Date().toISOString() 15 | const logMessage = `[${date}][${session}][${username}][${hostname}] ${messages.join(' ')}` 16 | if (level === LOG_LEVEL_DEBUG) { 17 | console.log('[DEBUG]' + logMessage) 18 | } else if (level === LOG_LEVEL_INFO) { 19 | console.info('[INFO]' + logMessage) 20 | } else if (level === LOG_LEVEL_WARN) { 21 | console.warn('[WARN]' + logMessage) 22 | } else if (level === LOG_LEVEL_ERROR) { 23 | console.error('[ERROR]' + logMessage) 24 | } 25 | } 26 | 27 | return { 28 | debug: msgs => log(LOG_LEVEL_DEBUG, msgs), 29 | info: msgs => log(LOG_LEVEL_INFO, msgs), 30 | warn: msgs => log(LOG_LEVEL_WARN, msgs), 31 | error: msgs => log(LOG_LEVEL_ERROR, msgs) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/special-use-unit.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | /* eslint-disable no-useless-escape */ 3 | 4 | import { checkSpecialUse } from './special-use' 5 | 6 | describe('checkSpecialUse', () => { 7 | it('should return a matching special use flag', () => { 8 | expect(checkSpecialUse({ 9 | flags: ['test', '\\All'] 10 | })).to.equal('\\All') 11 | }) 12 | 13 | it('should fail for non-existent flag', () => { 14 | expect(checkSpecialUse({})).to.be.false 15 | }) 16 | 17 | it('should fail for invalid flag', () => { 18 | expect(checkSpecialUse({ 19 | flags: ['test'] 20 | })).to.be.false 21 | }) 22 | 23 | it('should return special use flag if a matching name is found', () => { 24 | expect(checkSpecialUse({ 25 | name: 'test' 26 | })).to.be.false 27 | expect(checkSpecialUse({ 28 | name: 'Praht' 29 | })).to.equal('\\Trash') 30 | expect(checkSpecialUse({ 31 | flags: ['\HasChildren'], // not a special use flag 32 | name: 'Praht' 33 | })).to.equal('\\Trash') 34 | }) 35 | 36 | it('should prefer matching special use flag over a matching name', () => { 37 | expect(checkSpecialUse({ 38 | flags: ['\\All'], 39 | name: 'Praht' 40 | })).to.equal('\\All') 41 | }) 42 | 43 | it('should return an Archive flag on finding box named Archive or Archives', () => { 44 | expect(checkSpecialUse({ 45 | name: 'Archive' 46 | })).to.equal('\\Archive') 47 | expect(checkSpecialUse({ 48 | name: 'Archives' 49 | })).to.equal('\\Archive') 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /src/special-use.js: -------------------------------------------------------------------------------- 1 | import { propOr } from 'ramda' 2 | 3 | const SPECIAL_USE_FLAGS = ['\\All', '\\Archive', '\\Drafts', '\\Flagged', '\\Junk', '\\Sent', '\\Trash'] 4 | const SPECIAL_USE_BOXES = { 5 | '\\Sent': [ 6 | 'aika', 'bidaliak', 'bidalita', 'dihantar', 'e rometsweng', 'e tindami', 'elküldött', 'elküldöttek', 'enviadas', 7 | 'enviadas', 'enviados', 'enviats', 'envoyés', 'ethunyelweyo', 'expediate', 'ezipuru', 'gesendete', 'gestuur', 8 | 'gönderilmiş öğeler', 'göndərilənlər', 'iberilen', 'inviati', 'išsiųstieji', 'kuthunyelwe', 'lasa', 'lähetetyt', 9 | 'messages envoyés', 'naipadala', 'nalefa', 'napadala', 'nosūtītās ziņas', 'odeslané', 'padala', 'poslane', 10 | 'poslano', 'poslano', 'poslané', 'poslato', 'saadetud', 'saadetud kirjad', 'sendt', 'sendt', 'sent', 'sent items', 11 | 'sent messages', 'sända poster', 'sänt', 'terkirim', 'ti fi ranṣẹ', 'të dërguara', 'verzonden', 'vilivyotumwa', 12 | 'wysłane', 'đã gửi', 'σταλθέντα', 'жиберилген', 'жіберілгендер', 'изпратени', 'илгээсэн', 'ирсол шуд', 'испратено', 13 | 'надіслані', 'отправленные', 'пасланыя', 'юборилган', 'ուղարկված', 'נשלחו', 'פריטים שנשלחו', 'المرسلة', 'بھیجے گئے', 14 | 'سوزمژہ', 'لېګل شوی', 'موارد ارسال شده', 'पाठविले', 'पाठविलेले', 'प्रेषित', 'भेजा गया', 'প্রেরিত', 'প্রেরিত', 'প্ৰেৰিত', 'ਭੇਜੇ', 'મોકલેલા', 15 | 'ପଠାଗଲା', 'அனுப்பியவை', 'పంపించబడింది', 'ಕಳುಹಿಸಲಾದ', 'അയച്ചു', 'යැවු පණිවුඩ', 'ส่งแล้ว', 'გაგზავნილი', 'የተላኩ', 'បាន​ផ្ញើ', 16 | '寄件備份', '寄件備份', '已发信息', '送信済みメール', '발신 메시지', '보낸 편지함' 17 | ], 18 | '\\Trash': [ 19 | 'articole șterse', 'bin', 'borttagna objekt', 'deleted', 'deleted items', 'deleted messages', 'elementi eliminati', 20 | 'elementos borrados', 'elementos eliminados', 'gelöschte objekte', 'item dipadam', 'itens apagados', 'itens excluídos', 21 | 'mục đã xóa', 'odstraněné položky', 'pesan terhapus', 'poistetut', 'praht', 'prügikast', 'silinmiş öğeler', 22 | 'slettede beskeder', 'slettede elementer', 'trash', 'törölt elemek', 'usunięte wiadomości', 'verwijderde items', 23 | 'vymazané správy', 'éléments supprimés', 'видалені', 'жойылғандар', 'удаленные', 'פריטים שנמחקו', 'العناصر المحذوفة', 24 | 'موارد حذف شده', 'รายการที่ลบ', '已删除邮件', '已刪除項目', '已刪除項目' 25 | ], 26 | '\\Junk': [ 27 | 'bulk mail', 'correo no deseado', 'courrier indésirable', 'istenmeyen', 'istenmeyen e-posta', 'junk', 'levélszemét', 28 | 'nevyžiadaná pošta', 'nevyžádaná pošta', 'no deseado', 'posta indesiderata', 'pourriel', 'roskaposti', 'skräppost', 29 | 'spam', 'spam', 'spamowanie', 'søppelpost', 'thư rác', 'спам', 'דואר זבל', 'الرسائل العشوائية', 'هرزنامه', 'สแปม', 30 | '‎垃圾郵件', '垃圾邮件', '垃圾電郵' 31 | ], 32 | '\\Drafts': [ 33 | 'ba brouillon', 'borrador', 'borrador', 'borradores', 'bozze', 'brouillons', 'bản thảo', 'ciorne', 'concepten', 'draf', 34 | 'drafts', 'drög', 'entwürfe', 'esborranys', 'garalamalar', 'ihe edeturu', 'iidrafti', 'izinhlaka', 'juodraščiai', 'kladd', 35 | 'kladder', 'koncepty', 'koncepty', 'konsep', 'konsepte', 'kopie robocze', 'layihələr', 'luonnokset', 'melnraksti', 'meralo', 36 | 'mesazhe të padërguara', 'mga draft', 'mustandid', 'nacrti', 'nacrti', 'osnutki', 'piszkozatok', 'rascunhos', 'rasimu', 37 | 'skice', 'taslaklar', 'tsararrun saƙonni', 'utkast', 'vakiraoka', 'vázlatok', 'zirriborroak', 'àwọn àkọpamọ́', 'πρόχειρα', 38 | 'жобалар', 'нацрти', 'нооргууд', 'сиёҳнавис', 'хомаки хатлар', 'чарнавікі', 'чернетки', 'чернови', 'черновики', 'черновиктер', 39 | 'սևագրեր', 'טיוטות', 'مسودات', 'مسودات', 'موسودې', 'پیش نویسها', 'ڈرافٹ/', 'ड्राफ़्ट', 'प्रारूप', 'খসড়া', 'খসড়া', 'ড্ৰাফ্ট', 'ਡ੍ਰਾਫਟ', 'ડ્રાફ્ટસ', 40 | 'ଡ୍ରାଫ୍ଟ', 'வரைவுகள்', 'చిత్తు ప్రతులు', 'ಕರಡುಗಳು', 'കരടുകള്‍', 'කෙටුම් පත්', 'ฉบับร่าง', 'მონახაზები', 'ረቂቆች', 'សារព្រាង', '下書き', '草稿', 41 | '草稿', '草稿', '임시 보관함' 42 | ], 43 | // The \Archive flag is rarely used by major email providers so we also check the path. 44 | // Thunderbird names them Archives instead of Archive. 45 | '\\Archive': ['archive', 'archives'] 46 | } 47 | const SPECIAL_USE_BOX_FLAGS = Object.keys(SPECIAL_USE_BOXES) 48 | 49 | /** 50 | * Checks if a mailbox is for special use 51 | * 52 | * @param {Object} mailbox 53 | * @return {String} Special use flag (if detected) 54 | */ 55 | export function checkSpecialUse (mailbox) { 56 | if (mailbox.flags) { 57 | for (let i = 0; i < SPECIAL_USE_FLAGS.length; i++) { 58 | const type = SPECIAL_USE_FLAGS[i] 59 | if ((mailbox.flags || []).indexOf(type) >= 0) { 60 | mailbox.specialUse = type 61 | mailbox.specialUseFlag = type 62 | return type 63 | } 64 | } 65 | } 66 | 67 | return checkSpecialUseByName(mailbox) 68 | } 69 | 70 | function checkSpecialUseByName (mailbox) { 71 | const name = propOr('', 'name', mailbox).toLowerCase().trim() 72 | 73 | for (let i = 0; i < SPECIAL_USE_BOX_FLAGS.length; i++) { 74 | const type = SPECIAL_USE_BOX_FLAGS[i] 75 | if (SPECIAL_USE_BOXES[type].indexOf(name) >= 0) { 76 | mailbox.specialUse = type 77 | return type 78 | } 79 | } 80 | 81 | return false 82 | } 83 | -------------------------------------------------------------------------------- /testutils.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import sinon from 'sinon' 3 | 4 | global.expect = expect 5 | global.sinon = sinon 6 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: './src/compression-worker.js', 5 | output: { 6 | path: path.resolve(__dirname, 'res'), 7 | filename: 'compression.worker.js' 8 | }, 9 | node: false, 10 | module: { 11 | rules: [{ 12 | exclude: /node_modules/, 13 | use: { 14 | loader: 'babel-loader', 15 | options: { 16 | presets: ['@babel/preset-env'] 17 | } 18 | } 19 | }] 20 | } 21 | } 22 | --------------------------------------------------------------------------------