├── .gitignore ├── imapHandler ├── imapHandler.js ├── imapCompiler.js ├── imapFormalSyntax.js └── imapParser.js ├── README.md ├── package.json ├── config-sample.json ├── index.js ├── template.html └── check-imap.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | config.json -------------------------------------------------------------------------------- /imapHandler/imapHandler.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: require("./imapParser"), 3 | compiler: require("./imapCompiler") 4 | }; 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # capabilitybot 2 | 3 | Simple IMAP CAPABILITY checker. Lists CAPABILITY info for configured hosts. 4 | 5 | ## License 6 | 7 | **MIT** -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "capabilitybot", 3 | "version": "0.1.0", 4 | "description": "Check CAPABILITY info for provided IMAP server", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "IMAP", 11 | "CAPABILITY" 12 | ], 13 | "author": "Andris Reinman", 14 | "license": "MIT", 15 | "dependencies": { 16 | "npmlog": "0.0.6", 17 | "ejs": "~0.8.5" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /config-sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "#": "HTTP port where the server is listening", 3 | "port": 1337, 4 | 5 | "#": "An array of IMAP host configurations", 6 | "hosts": [ 7 | { 8 | "#": "Connection name", 9 | "name": "Mail Host", 10 | 11 | "#": "Hostname (defaults to localhost)", 12 | "host": "localhost", 13 | 14 | "#": "Port, defaults to 143 for non secure and 993 for secure connections", 15 | "port": 143, 16 | 17 | "#": "Start the connection in secure mode if true", 18 | "ssl": false, 19 | 20 | "#": "If the connection is not secure, do not use STARTTLS before authentication", 21 | "ignoreSTARTTLS": false, 22 | 23 | "#": "Username, if not set then authentication is skipped", 24 | "user": "user", 25 | 26 | "#": "Password for the user", 27 | "pass": "pass" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var config = require("./config"), 4 | checkImap = require("./check-imap"), 5 | log = require("npmlog"), 6 | capabilityData = [], 7 | ejs = require("ejs"), 8 | processed = false, 9 | http = require("http"), 10 | template = require("fs").readFileSync(__dirname + "/template.html").toString("utf-8"); 11 | 12 | var i = 0; 13 | 14 | var processHosts = function(){ 15 | if(i >= config.hosts.length){ 16 | log.info("Status", "All checked"); 17 | processed = true; 18 | return; 19 | } 20 | 21 | var host = config.hosts[i++]; 22 | 23 | log.info("host", "Checking %s ...", host.name); 24 | checkImap(host, function(err, capability, transaction){ 25 | capabilityData.push({ 26 | host: host, 27 | error: err, 28 | capability: capability, 29 | transaction: transaction 30 | }); 31 | if(err){ 32 | log.error("imap", "Result: FAIL") 33 | log.error("imap", err); 34 | }else{ 35 | log.info("imap", "Result: SUCCESS"); 36 | } 37 | 38 | if(capability){ 39 | Object.keys(capability).forEach(function(capa){ 40 | log.info(capa, capability[capa]); 41 | }); 42 | } 43 | 44 | processHosts(); 45 | }); 46 | }; 47 | 48 | processHosts(); 49 | 50 | http.createServer(function (req, res) { 51 | res.writeHead(200, {'Content-Type': 'text/html'}); 52 | res.end(ejs.render(template, { 53 | processed: !!processed, 54 | capabilityData: capabilityData.sort(function(a, b){ 55 | return a.host.name.toLowerCase().localeCompare(b.host.name.toLowerCase()); 56 | }), 57 | total: config.hosts.length 58 | })); 59 | }).listen(config.port, function(){ 60 | log.info("http", "Server listening on port %s", config.port); 61 | }); 62 | 63 | -------------------------------------------------------------------------------- /imapHandler/imapCompiler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var imapFormalSyntax = require("./imapFormalSyntax"); 4 | 5 | module.exports = function(response, asArray){ 6 | var respParts = [], 7 | resp = (response.tag || "") + (response.command ? " " + response.command : ""), 8 | val, lastType, 9 | walk = function(node){ 10 | 11 | if(lastType == "LITERAL" || (["(", "<", "["].indexOf(resp.substr(-1)) < 0 && resp.length)){ 12 | resp += " "; 13 | } 14 | 15 | if(Array.isArray(node)){ 16 | lastType = "LIST"; 17 | resp += "("; 18 | node.forEach(walk); 19 | resp += ")"; 20 | return; 21 | } 22 | 23 | if(!node && typeof node != "string" && typeof node != "number"){ 24 | resp += "NIL"; 25 | return; 26 | } 27 | 28 | if(typeof node == "string"){ 29 | resp += JSON.stringify(node); 30 | return; 31 | } 32 | 33 | if(typeof node == "number"){ 34 | resp += Math.round(node) || 0; // Only integers allowed 35 | return; 36 | } 37 | 38 | lastType = node.type; 39 | switch(node.type.toUpperCase()){ 40 | case "LITERAL": 41 | if(!node.value){ 42 | resp += "{0}\r\n"; 43 | }else{ 44 | resp += "{" + node.value.length + "}\r\n"; 45 | } 46 | respParts.push(resp); 47 | resp = node.value || ""; 48 | break; 49 | 50 | case "STRING": 51 | resp += JSON.stringify(node.value || ""); 52 | break; 53 | 54 | case "TEXT": 55 | case "SEQUENCE": 56 | resp += node.value || ""; 57 | break; 58 | 59 | case "NUMBER": 60 | resp += (node.value || 0); 61 | break; 62 | 63 | case "ATOM": 64 | case "SECTION": 65 | val = node.value || ""; 66 | 67 | if(imapFormalSyntax.verify(val.charAt(0) == "\\" ? val.substr(1) : val, imapFormalSyntax["ATOM-CHAR"]()) >= 0){ 68 | val = JSON.stringify(val); 69 | } 70 | 71 | resp += val; 72 | 73 | if(node.section){ 74 | resp+="["; 75 | node.section.forEach(walk); 76 | resp+="]"; 77 | } 78 | if(node.partial){ 79 | resp+="<" + node.partial.join(".") + ">"; 80 | } 81 | break; 82 | } 83 | 84 | }; 85 | 86 | [].concat(response.attributes || []).forEach(walk); 87 | 88 | if(resp.length){ 89 | respParts.push(resp); 90 | } 91 | 92 | return asArray ? respParts : respParts.join(""); 93 | }; 94 | -------------------------------------------------------------------------------- /imapHandler/imapFormalSyntax.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // IMAP Formal Syntax 4 | // http://tools.ietf.org/html/rfc3501#section-9 5 | 6 | function expandRange(start, end){ 7 | var chars = []; 8 | for(var i = start; i <= end; i++){ 9 | chars.push(i); 10 | } 11 | return String.fromCharCode.apply(String, chars); 12 | } 13 | 14 | function excludeChars(source, exclude){ 15 | var sourceArr = Array.prototype.slice.call(source); 16 | for(var i = sourceArr.length - 1; i >= 0; i--){ 17 | if(exclude.indexOf(sourceArr[i]) >= 0){ 18 | sourceArr.splice(i, 1); 19 | } 20 | } 21 | return sourceArr.join(""); 22 | } 23 | 24 | module.exports = { 25 | 26 | "CHAR": function(){ 27 | var value = expandRange(0x01, 0x7F); 28 | this.CHAR = function(){ 29 | return value; 30 | }; 31 | return value; 32 | }, 33 | 34 | "CHAR8": function(){ 35 | var value = expandRange(0x01, 0xFF); 36 | this.CHAR8 = function(){ 37 | return value; 38 | }; 39 | return value; 40 | }, 41 | 42 | "SP": function(){ 43 | return " "; 44 | }, 45 | 46 | "CTL": function(){ 47 | var value = expandRange(0x00, 0x1F) + "\x7F"; 48 | this.CTL = function(){ 49 | return value; 50 | }; 51 | return value; 52 | }, 53 | 54 | "DQUOTE": function(){ 55 | return "\""; 56 | }, 57 | 58 | "ALPHA": function(){ 59 | var value = expandRange(0x41, 0x5A) + expandRange(0x61, 0x7A); 60 | this.ALPHA = function(){ 61 | return value; 62 | }; 63 | return value; 64 | }, 65 | 66 | "DIGIT": function(){ 67 | var value = expandRange(0x30, 0x39) + expandRange(0x61, 0x7A); 68 | this.DIGIT = function(){ 69 | return value; 70 | }; 71 | return value; 72 | }, 73 | 74 | "ATOM-CHAR": function(){ 75 | var value = excludeChars(this.CHAR(), this["atom-specials"]()); 76 | this["ATOM-CHAR"] = function(){ 77 | return value; 78 | }; 79 | return value; 80 | }, 81 | 82 | "ASTRING-CHAR": function(){ 83 | var value = this["ATOM-CHAR"]() + this["resp-specials"](); 84 | this["ASTRING-CHAR"] = function(){ 85 | return value; 86 | }; 87 | return value; 88 | }, 89 | 90 | "TEXT-CHAR": function(){ 91 | var value = excludeChars(this.CHAR(), "\r\n"); 92 | this["TEXT-CHAR"] = function(){ 93 | return value; 94 | }; 95 | return value; 96 | }, 97 | 98 | "atom-specials": function(){ 99 | var value = "(" + ")" + "{" + this.SP() + this.CTL() + this["list-wildcards"]() + 100 | this["quoted-specials"]() + this["resp-specials"](); 101 | this["atom-specials"] = function(){ 102 | return value; 103 | }; 104 | return value; 105 | }, 106 | 107 | "list-wildcards": function(){ 108 | return "%" + "*"; 109 | }, 110 | 111 | "quoted-specials": function(){ 112 | var value = this.DQUOTE() + "\\"; 113 | this["quoted-specials"] = function(){ 114 | return value; 115 | }; 116 | return value; 117 | }, 118 | 119 | "resp-specials": function(){ 120 | return "]"; 121 | }, 122 | 123 | tag: function(){ 124 | var value = excludeChars(this["ASTRING-CHAR"](), "+"); 125 | this.tag = function(){ 126 | return value; 127 | }; 128 | return value; 129 | }, 130 | 131 | command: function(){ 132 | var value = this.ALPHA() + this.DIGIT(); 133 | this.command = function(){ 134 | return value; 135 | }; 136 | return value; 137 | }, 138 | 139 | verify: function(str, allowedChars){ 140 | for(var i=0, len = str.length; i < len; i++){ 141 | if(allowedChars.indexOf(str.charAt(i)) < 0){ 142 | return i; 143 | } 144 | } 145 | return -1; 146 | } 147 | }; 148 | -------------------------------------------------------------------------------- /template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IMAP CAPABILITY 6 | 7 | 8 | 9 | 10 | Fork me on GitHub 11 | 12 |
13 | 14 | 18 | 19 | <% if(processed){ %> 20 | 21 | 26 | 27 | <% capabilityData.forEach(function(capa, i){ %> 28 |

<%= capa.host.name %>

29 | 30 | 35 | 36 | 37 |
38 |
39 | 40 | <% if(capa.error){%> 41 |
42 | <%= capa.error.message %> 43 |
44 | <% } %> 45 | 46 | <% if(capa.capability){ %> 47 | 48 | <% Object.keys(capa.capability).forEach(function(key){ %> 49 | 50 | 51 | 52 | 53 | 54 | 55 | <% }) %> 56 |
<%= key %><%= capa.capability[key] %>
57 | <% } %> 58 | 59 |
60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | <% if(!capa.host.ssl) {%> 76 | 77 | 78 | 79 | 80 | <%}%> 81 | 82 |
Hostname<%= capa.host.host %>
Port<%= capa.host.port %>
SSL connection<%= capa.host.ssl ? "Yes" : "No" %>
Use STARTTLS (if available)<%= capa.host.ignoreSTARTTLS ? "No" : "Yes" %>
83 |
84 |
85 | <% if(capa.transaction){ %> 86 | 87 | <% capa.transaction.forEach(function(item){ %> 88 | 89 | 90 | 91 | 92 | 93 | 94 | <% }) %> 95 |
<%= item.type %>
<%= item.payload %>
96 | <% } %> 97 |
98 |
99 | <% }) %> 100 | 101 | <% } %> 102 | 103 | <% if(!processed){ %> 104 |

Checking hosts ...

105 |
106 |
107 | <%= capabilityData.length %> / <%= total %> Complete 108 |
109 |
110 | 111 | <% } %> 112 | 113 |

© 2014 Andris Reinman andris@kreata.ee

114 | 115 |
116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /check-imap.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var imapHandler = require("./imapHandler/imapHandler"), 4 | net = require("net"), 5 | tls = require("tls"); 6 | 7 | module.exports = function(options, callback){ 8 | new IMAPChecker(options, callback).connect(); 9 | }; 10 | 11 | function IMAPChecker(options, callback){ 12 | options = options || {}; 13 | this.host = options.host || "localhost"; 14 | this.port = options.port || (options.ssl ? 993 : 143); 15 | this.ssl = !!options.ssl; 16 | this.ignoreSTARTTLS = !!options.ignoreSTARTTLS; 17 | 18 | this.callback = callback; 19 | 20 | this.user = options.user || ""; 21 | this.pass = options.pass || ""; 22 | 23 | this.connection = false; 24 | 25 | this.greetingTimeout = false; 26 | 27 | this.capability = {}; 28 | this.logText = []; 29 | 30 | this.remainder = ""; 31 | this.command = ""; 32 | this.literalRemaining = 0; 33 | this.ignoreData = false; 34 | 35 | this.greeting = true; 36 | this.currentAction = -1; 37 | this.actions = [ 38 | { 39 | payload: "A1 CAPABILITY", 40 | 41 | untagged: untaggedCapability.bind(this, "pre-auth"), 42 | 43 | ok: function(self){ 44 | // if no need to run STARTTLS, skip it and post-starttls capability 45 | if(self.ssl || self.ignoreSTARTTLS || (self.capability["pre-auth"] || []).indexOf("STARTTLS") < 0){ 46 | self.currentAction += 2; 47 | } 48 | self.nextAction(); 49 | } 50 | }, 51 | { 52 | payload: "A2 STARTTLS", 53 | ok: function(self){ 54 | self.log({type:"connection", payload:"Upgrading connection ..."}); 55 | self.upgradeConnection(function(err){ 56 | if(err){ 57 | self.onError(err); 58 | return; 59 | } 60 | self.log({type:"connection", payload:"Connection upgraded"}); 61 | self.nextAction(); 62 | }); 63 | } 64 | }, 65 | { 66 | payload: "A3 CAPABILITY", 67 | 68 | untagged: untaggedCapability.bind(this, "post-starttls"), 69 | 70 | ok: function(self){ 71 | self.nextAction(); 72 | } 73 | }, 74 | { 75 | pre: function(self){ 76 | if(!self.user){ 77 | //skip and shift 78 | self.currentAction++; 79 | return false; 80 | } 81 | return true; 82 | }, 83 | 84 | payload: "A4 LOGIN \"" + this.user + "\" \"" + this.pass + "\"", 85 | 86 | logPayload: "A4 LOGIN \"****\" \"****\"", 87 | 88 | ok: function(self){ 89 | self.nextAction(); 90 | } 91 | }, 92 | { 93 | payload: "A5 CAPABILITY", 94 | 95 | untagged: untaggedCapability.bind(this, "post-auth"), 96 | 97 | ok: function(self){ 98 | self.nextAction(); 99 | } 100 | }, 101 | { 102 | payload: "A6 LOGOUT" 103 | } 104 | ]; 105 | } 106 | 107 | IMAPChecker.prototype.GREETING_TIMEOUT = 15 * 1000; 108 | 109 | IMAPChecker.prototype.connect = function(){ 110 | var sslOptions = { 111 | rejectUnauthorized: false 112 | }; 113 | 114 | if(this.ssl){ 115 | this.connection = tls.connect(this.port, this.host, sslOptions, this.onConnect.bind(this)); 116 | }else{ 117 | this.connection = net.connect(this.port, this.host, this.onConnect.bind(this)); 118 | } 119 | 120 | this.connection.on("error", this.onError.bind(this)); 121 | 122 | this.greetingTimeout = setTimeout(this.handleGreetingTimeout.bind(this), this.GREETING_TIMEOUT); 123 | }; 124 | 125 | IMAPChecker.prototype.onConnect = function(){ 126 | this.log({type: "connection", payload: "Connection established to " + this.host + " (" + this.connection.remoteAddress + ")"}); 127 | 128 | this.connection.on("data", this.onData.bind(this)); 129 | this.connection.on("close", this.onClose.bind(this)); 130 | this.connection.on("end", this.onEnd.bind(this)); 131 | }; 132 | 133 | IMAPChecker.prototype.onEnd = function(){ 134 | this.close(); 135 | }; 136 | 137 | IMAPChecker.prototype.onError = function(err){ 138 | this.error = err; 139 | this.log({type: "error", payload: err.message}); 140 | this.close(); 141 | }; 142 | 143 | IMAPChecker.prototype.log = function(data){ 144 | this.logText.push(data); 145 | }; 146 | 147 | IMAPChecker.prototype.close = function(){ 148 | clearTimeout(this.greetingTimeout); 149 | 150 | if(!this.connection){ 151 | if(typeof this.callback == "function" && this.error){ 152 | this.callback(this.error, false, this.logText); 153 | this.callback = false; 154 | } 155 | return; 156 | } 157 | 158 | var socket = this.connection.socket || this.connection; 159 | if(socket && !socket.destroyed){ 160 | if(typeof this.callback == "function" && this.error){ 161 | this.callback(this.error, false, this.logText); 162 | this.callback = false; 163 | } 164 | socket.destroy(); 165 | }else{ 166 | if(typeof this.callback == "function" && this.error){ 167 | this.callback(this.error, false, this.logText); 168 | this.callback = false; 169 | } 170 | } 171 | 172 | this.connection = false; 173 | }; 174 | 175 | IMAPChecker.prototype.onClose = function(){ 176 | this.log({type: "connection", payload: "Connection closed"}); 177 | 178 | clearTimeout(this.greetingTimeout); 179 | this.connection = false; 180 | 181 | if(typeof this.callback == "function"){ 182 | this.callback(this.error || null, this.capability, this.logText); 183 | this.callback = false; 184 | } 185 | }; 186 | 187 | IMAPChecker.prototype.onData = function(chunk){ 188 | clearTimeout(this.greetingTimeout); 189 | 190 | if(this.ignoreData){ 191 | return; 192 | } 193 | 194 | var match, 195 | str = (chunk || "").toString("binary"); 196 | 197 | if(this.literalRemaining){ 198 | if(this.literalRemaining > str.length){ 199 | this.literalRemaining -= str.length; 200 | this.command += str; 201 | return; 202 | } 203 | this.command += str.substr(0, this.literalRemaining); 204 | str = str.substr(this.literalRemaining); 205 | this.literalRemaining = 0; 206 | } 207 | this.remainder = str = this.remainder + str; 208 | while((match = str.match(/(\{(\d+)(\+)?\})?\r?\n/))){ 209 | 210 | if(!match[2]){ 211 | // Now we have a full command line, so lets do something with it 212 | this.processData(this.command + str.substr(0, match.index)); 213 | 214 | this.remainder = str = str.substr(match.index + match[0].length); 215 | this.command = ""; 216 | continue; 217 | } 218 | 219 | this.remainder = ""; 220 | 221 | this.command += str.substr(0, match.index + match[0].length); 222 | 223 | this.literalRemaining = Number(match[2]); 224 | 225 | str = str.substr(match.index + match[0].length); 226 | 227 | if(this.literalRemaining > str.length){ 228 | this.command += str; 229 | this.literalRemaining -= str.length; 230 | return; 231 | }else{ 232 | this.command += str.substr(0, this.literalRemaining); 233 | this.remainder = str = str.substr(this.literalRemaining); 234 | this.literalRemaining = 0; 235 | } 236 | } 237 | }; 238 | 239 | IMAPChecker.prototype.handleGreetingTimeout = function(){ 240 | if(typeof this.callback == "function"){ 241 | this.callback(new Error("Timeout waiting for a greeting"), false, this.logText.length ? this.logText : false); 242 | this.callback = false; 243 | } 244 | this.close(); 245 | }; 246 | 247 | IMAPChecker.prototype.processData = function(data){ 248 | this.log({type: "server", payload: data}); 249 | 250 | var command; 251 | 252 | try{ 253 | command = imapHandler.parser(data, {allowUntagged: true}); 254 | }catch(E){ 255 | return this.onError(E); 256 | } 257 | 258 | // 1st message is a greeting 259 | if(this.greeting && command.tag == "*"){ 260 | if((command.command || "").toString().trim().toUpperCase() != "OK"){ 261 | return this.onError("Invalid greeting"); 262 | } 263 | this.greeting = false; 264 | return this.nextAction(); 265 | } 266 | 267 | var action = this.actions[this.currentAction], 268 | humanReadable = command.attributes && command.attributes.length && command.attributes[command.attributes.length-1].type == "TEXT" && command.attributes[command.attributes.length-1].value || false; 269 | 270 | // handle tagged response 271 | if(command.tag == action.tag){ 272 | switch((command.command || "").toString().trim().toUpperCase()){ 273 | case "OK": 274 | if(typeof action.ok == "function"){ 275 | action.ok(this); 276 | } 277 | break; 278 | case "NO": 279 | if(typeof action.no == "function"){ 280 | action.no(this); 281 | }else{ 282 | this.onError(new Error("Unexpected NO" + (humanReadable ? ": " + humanReadable : ""))); 283 | } 284 | break; 285 | case "BAD": 286 | if(typeof action.bad == "function"){ 287 | action.bad(this); 288 | }else{ 289 | this.onError(new Error("Unexpected BAD" + (humanReadable ? ": " + humanReadable : ""))); 290 | } 291 | break; 292 | default: 293 | return this.nextAction(); 294 | } 295 | } 296 | 297 | // handle untagged responses 298 | if(command.tag == "*" && typeof action.untagged == "function"){ 299 | action.untagged(this, command); 300 | } 301 | }; 302 | 303 | IMAPChecker.prototype.nextAction = function(){ 304 | if(this.currentAction >= this.actions.length - 1){ 305 | return this.close(); 306 | } 307 | 308 | var action = this.actions[++this.currentAction], 309 | command = imapHandler.parser(action.payload); 310 | 311 | // the command can skip itself if needed 312 | if(typeof action.pre == "function" && action.pre(this) === false){ 313 | return this.nextAction(); 314 | } 315 | 316 | action.tag = command.tag; 317 | 318 | this.log({type: "client", payload: action.logPayload || action.payload}); 319 | 320 | this.connection.write(new Buffer(action.payload + "\r\n", "binary")); 321 | }; 322 | 323 | IMAPChecker.prototype.upgradeConnection = function(callback){ 324 | this.ignoreData = true; 325 | this.connection.removeAllListeners("data"); 326 | this.connection.removeAllListeners("error"); 327 | 328 | var opts = { 329 | socket: this.connection, 330 | host: this.host, 331 | rejectUnauthorized: true 332 | }; 333 | 334 | this.connection = tls.connect(opts, (function(){ 335 | this.ignoreData = false; 336 | this.ssl = true; 337 | this.connection.on("data", this.onData.bind(this)); 338 | 339 | return callback(null, true); 340 | }).bind(this)); 341 | this.connection.on("error", this.onError.bind(this)); 342 | }; 343 | 344 | function untaggedCapability(state, self, command){ 345 | if((command.command || "").toString().trim().toUpperCase() == "CAPABILITY"){ 346 | self.capability[state] = [].concat(command.attributes || []).map(function(capa){ 347 | return (capa.value || "").toString().trim().toUpperCase(); 348 | }).join(" "); 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /imapHandler/imapParser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var imapFormalSyntax = require("./imapFormalSyntax"); 4 | 5 | 6 | function ParserInstance(input, options){ 7 | this.input = (input || "").toString(); 8 | this.options = options || {}; 9 | this.remainder = this.input; 10 | this.pos = 0; 11 | } 12 | 13 | ParserInstance.prototype.getTag = function(){ 14 | if(!this.tag){ 15 | this.tag = this.getElement(imapFormalSyntax.tag() + "*+", true); 16 | } 17 | return this.tag; 18 | }; 19 | 20 | ParserInstance.prototype.getCommand = function(){ 21 | var responseCode; 22 | 23 | if(!this.command){ 24 | this.command = this.getElement(imapFormalSyntax.command()); 25 | } 26 | 27 | switch((this.command || "").toString().toUpperCase()){ 28 | case "OK": 29 | case "NO": 30 | case "BAD": 31 | case "PREAUTH": 32 | case "BYE": 33 | responseCode = this.remainder.match(/^ \[[^\]]*\]/); 34 | if(responseCode){ 35 | this.humanReadable = this.remainder.substr(responseCode[0].length).trim(); 36 | this.remainder = responseCode[0]; 37 | } 38 | else{ 39 | this.humanReadable = this.remainder.trim(); 40 | this.remainder = ""; 41 | } 42 | break; 43 | } 44 | 45 | return this.command; 46 | }; 47 | 48 | ParserInstance.prototype.getElement = function(syntax){ 49 | var match, element, errPos; 50 | if(this.remainder.match(/^\s/)){ 51 | throw new Error("Unexpected whitespace at position " + this.pos); 52 | } 53 | 54 | if((match = this.remainder.match(/^[^\s]+(?=\s|$)/))){ 55 | element = match[0]; 56 | 57 | if((errPos = imapFormalSyntax.verify(element, syntax)) >= 0){ 58 | throw new Error("Unexpected char at position " + (this.pos + errPos)); 59 | } 60 | }else{ 61 | throw new Error("Unexpected end of input at position " + this.pos); 62 | } 63 | 64 | this.pos += match[0].length; 65 | this.remainder = this.remainder.substr(match[0].length); 66 | 67 | return element; 68 | }; 69 | 70 | ParserInstance.prototype.getSpace = function(){ 71 | if(!this.remainder.length){ 72 | throw new Error("Unexpected end of input at position " + this.pos); 73 | } 74 | 75 | if(imapFormalSyntax.verify(this.remainder.charAt(0), imapFormalSyntax.SP()) >= 0){ 76 | throw new Error("Unexpected char at position " + this.pos); 77 | } 78 | 79 | this.pos ++; 80 | this.remainder = this.remainder.substr(1); 81 | }; 82 | 83 | ParserInstance.prototype.getAttributes = function(){ 84 | if(!this.remainder.length){ 85 | throw new Error("Unexpected end of input at position " + this.pos); 86 | } 87 | 88 | if(this.remainder.match(/^\s/)){ 89 | throw new Error("Unexpected whitespace at position " + this.pos); 90 | } 91 | 92 | return new TokenParser(this, this.pos, this.remainder, this.options).getAttributes(); 93 | }; 94 | 95 | function TokenParser(parent, startPos, str, options){ 96 | this.str = (str || "").toString(); 97 | this.options = options || {}; 98 | this.parent = parent; 99 | 100 | this.tree = this.currentNode = this.createNode(); 101 | this.pos = startPos || 0; 102 | 103 | this.currentNode.type = "TREE"; 104 | 105 | this.state = "NORMAL"; 106 | 107 | this.processString(); 108 | } 109 | 110 | TokenParser.prototype.getAttributes = function(){ 111 | var attributes = [], 112 | branch = attributes; 113 | 114 | var walk = (function(node){ 115 | var elm, curBranch = branch, partial; 116 | 117 | if(!node.closed && node.type == "SEQUENCE" && node.value == "*"){ 118 | node.closed = true; 119 | node.type = "ATOM"; 120 | } 121 | 122 | // If the node was never closed, throw it 123 | if(!node.closed){ 124 | throw new Error("Unexpected end of input at position " + (this.pos + this.str.length - 1)); 125 | } 126 | 127 | switch(node.type.toUpperCase()){ 128 | case "LITERAL": 129 | case "STRING": 130 | case "SEQUENCE": 131 | elm = { 132 | type: node.type.toUpperCase(), 133 | value: node.value 134 | }; 135 | branch.push(elm); 136 | break; 137 | case "ATOM": 138 | if(node.value.toUpperCase() == "NIL"){ 139 | branch.push(null); 140 | break; 141 | } 142 | elm = { 143 | type: node.type.toUpperCase(), 144 | value: node.value 145 | }; 146 | branch.push(elm); 147 | break; 148 | case "SECTION": 149 | branch = branch[branch.length - 1].section = []; 150 | break; 151 | case "LIST": 152 | elm = []; 153 | branch.push(elm); 154 | branch = elm; 155 | break; 156 | case "PARTIAL": 157 | partial = node.value.split(".").map(Number); 158 | if(partial.slice(-1)[0] < partial.slice(0, 1)[0]){ 159 | throw new Error("Invalid partial value at position " + node.startPos); 160 | } 161 | branch[branch.length - 1].partial = partial; 162 | break; 163 | } 164 | 165 | node.childNodes.forEach(function(childNode){ 166 | walk(childNode); 167 | }); 168 | branch = curBranch; 169 | }).bind(this); 170 | 171 | walk(this.tree); 172 | 173 | return attributes; 174 | }; 175 | 176 | TokenParser.prototype.createNode = function(parentNode, startPos){ 177 | var node = { 178 | childNodes:[], 179 | type: false, 180 | value: "", 181 | closed: true 182 | }; 183 | 184 | if(parentNode){ 185 | node.parentNode = parentNode; 186 | } 187 | 188 | if(typeof startPos == "number"){ 189 | node.startPos = startPos; 190 | } 191 | 192 | if(parentNode){ 193 | parentNode.childNodes.push(node); 194 | } 195 | 196 | return node; 197 | }; 198 | 199 | TokenParser.prototype.processString = function(){ 200 | var chr, i, len, 201 | checkSP = (function(){ 202 | // jump to the next non whitespace pos 203 | while(this.str.charAt(i + 1) == " "){ 204 | i++; 205 | } 206 | }).bind(this); 207 | 208 | for(i = 0, len = this.str.length; i < len; i++){ 209 | 210 | chr = this.str.charAt(i); 211 | 212 | switch(this.state){ 213 | 214 | case "NORMAL": 215 | 216 | switch(chr){ 217 | 218 | // DQUOTE starts a new string 219 | case '"': 220 | this.currentNode = this.createNode(this.currentNode, this.pos + i); 221 | this.currentNode.type = "string"; 222 | this.state = "STRING"; 223 | this.currentNode.closed = false; 224 | break; 225 | 226 | // ( starts a new list 227 | case "(": 228 | this.currentNode = this.createNode(this.currentNode, this.pos + i); 229 | this.currentNode.type = "LIST"; 230 | this.currentNode.closed = false; 231 | break; 232 | 233 | // ) closes a list 234 | case ")": 235 | if(this.currentNode.type != "LIST"){ 236 | throw new Error("Unexpected list terminator ) at position " + (this.pos+i)); 237 | } 238 | 239 | this.currentNode.closed = true; 240 | this.currentNode.endPos = this.pos + i; 241 | this.currentNode = this.currentNode.parentNode; 242 | 243 | checkSP(); 244 | break; 245 | 246 | // ] closes section group 247 | case "]": 248 | if(this.currentNode.type != "SECTION"){ 249 | throw new Error("Unexpected section terminator ] at position " + (this.pos+i)); 250 | } 251 | this.currentNode.closed = true; 252 | this.currentNode.endPos = this.pos + i; 253 | this.currentNode = this.currentNode.parentNode; 254 | checkSP(); 255 | break; 256 | 257 | // < starts a new partial 258 | case "<": 259 | if(this.str.charAt(i - 1) != "]"){ 260 | this.currentNode = this.createNode(this.currentNode, this.pos + i); 261 | this.currentNode.type = "ATOM"; 262 | this.currentNode.value = chr; 263 | this.state = "ATOM"; 264 | }else{ 265 | this.currentNode = this.createNode(this.currentNode, this.pos + i); 266 | this.currentNode.type = "PARTIAL"; 267 | this.state = "PARTIAL"; 268 | this.currentNode.closed = false; 269 | } 270 | break; 271 | 272 | // { starts a new literal 273 | case "{": 274 | this.currentNode = this.createNode(this.currentNode, this.pos + i); 275 | this.currentNode.type = "LITERAL"; 276 | this.state = "LITERAL"; 277 | this.currentNode.closed = false; 278 | break; 279 | 280 | // ( starts a new sequence 281 | case "*": 282 | this.currentNode = this.createNode(this.currentNode, this.pos + i); 283 | this.currentNode.type = "SEQUENCE"; 284 | this.currentNode.value = chr; 285 | this.currentNode.closed = false; 286 | this.state = "SEQUENCE"; 287 | break; 288 | 289 | // normally a space should never occur 290 | case " ": 291 | // just ignore 292 | break; 293 | 294 | // [ starts section 295 | case "[": 296 | if(["OK", "NO", "BAD", "BYE", "PREAUTH"].indexOf(this.parent.command.toUpperCase()) >= 0){ 297 | this.currentNode.endPos = this.pos + i; 298 | 299 | this.currentNode = this.createNode(this.currentNode, this.pos + i); 300 | this.currentNode.type = "ATOM"; 301 | 302 | this.currentNode = this.createNode(this.currentNode, this.pos + i); 303 | this.currentNode.type = "SECTION"; 304 | this.currentNode.closed = false; 305 | this.state = "NORMAL"; 306 | }else{ 307 | this.currentNode = this.createNode(this.currentNode, this.pos + i); 308 | this.currentNode.type = "ATOM"; 309 | this.currentNode.value = chr; 310 | this.state = "ATOM"; 311 | } 312 | break; 313 | 314 | // Any ATOM supported char starts a new Atom sequence, otherwise throw an error 315 | default: 316 | // Allow \ as the first char for atom to support system flags 317 | // Allow % to support LIST "" % 318 | if(imapFormalSyntax["ATOM-CHAR"]().indexOf(chr) < 0 && chr != "\\" && chr != "%"){ 319 | throw new Error("Unexpected char at position " + (this.pos + i)); 320 | } 321 | 322 | this.currentNode = this.createNode(this.currentNode, this.pos + i); 323 | this.currentNode.type = "ATOM"; 324 | this.currentNode.value = chr; 325 | this.state = "ATOM"; 326 | break; 327 | } 328 | break; 329 | 330 | case "ATOM": 331 | 332 | // space finishes an atom 333 | if(chr == " "){ 334 | if([")", "]"].indexOf(this.str.charAt(i + 1)) >= 0){ 335 | throw new Error("Unexpected whitespace at position " + (this.pos + i + 1)); 336 | } 337 | this.currentNode.endPos = this.pos + i - 1; 338 | this.currentNode = this.currentNode.parentNode; 339 | this.state = "NORMAL"; 340 | break; 341 | } 342 | 343 | // 344 | if( 345 | this.currentNode.parentNode && 346 | ( 347 | (chr == ")" && this.currentNode.parentNode.type == "LIST") || 348 | (chr == "]" && this.currentNode.parentNode.type == "SECTION") 349 | ) 350 | ){ 351 | this.currentNode.endPos = this.pos + i - 1; 352 | this.currentNode = this.currentNode.parentNode; 353 | 354 | this.currentNode.closed = true; 355 | this.currentNode.endPos = this.pos + i; 356 | this.currentNode = this.currentNode.parentNode; 357 | this.state = "NORMAL"; 358 | 359 | checkSP(); 360 | break; 361 | } 362 | 363 | if((chr=="," || chr==":") && this.currentNode.value.match(/^\d+$/)){ 364 | this.currentNode.type = "SEQUENCE"; 365 | this.currentNode.closed = true; 366 | this.state = "SEQUENCE"; 367 | } 368 | 369 | // [ starts a section group for this element 370 | if(chr=="["){ 371 | // allowed only for selected elements 372 | if(["BODY", "BODY.PEEK"].indexOf(this.currentNode.value.toUpperCase()) < 0){ 373 | throw new Error("Unexpected section start char [ at position " + this.pos); 374 | } 375 | this.currentNode.endPos = this.pos + i; 376 | this.currentNode = this.createNode(this.currentNode.parentNode, this.pos + i); 377 | this.currentNode.type = "SECTION"; 378 | this.currentNode.closed = false; 379 | this.state = "NORMAL"; 380 | break; 381 | } 382 | 383 | if(chr == "<"){ 384 | throw new Error("Unexpected start of partial at position " + this.pos); 385 | } 386 | 387 | // if the char is not ATOM compatible, throw. Allow \* as an exception 388 | if(imapFormalSyntax["ATOM-CHAR"]().indexOf(chr) < 0 && chr != "]" && !(chr == "*" && this.currentNode.value == "\\")){ 389 | throw new Error("Unexpected char at position " + (this.pos+i)); 390 | }else if(this.currentNode.value == "\\*"){ 391 | throw new Error("Unexpected char at position " + (this.pos+i)); 392 | } 393 | 394 | this.currentNode.value += chr; 395 | break; 396 | 397 | case "STRING": 398 | 399 | // DQUOTE ends the string sequence 400 | if(chr == '"'){ 401 | this.currentNode.endPos = this.pos + i; 402 | this.currentNode.closed = true; 403 | this.currentNode = this.currentNode.parentNode; 404 | this.state = "NORMAL"; 405 | 406 | checkSP(); 407 | break; 408 | } 409 | 410 | // \ Escapes the following char 411 | if(chr == "\\"){ 412 | i++; 413 | if(i>=len){ 414 | throw new Error("Unexpected end of input at position " + (this.pos + i)); 415 | } 416 | } 417 | 418 | if(imapFormalSyntax["TEXT-CHAR"]().indexOf(chr) < 0){ 419 | throw new Error("Unexpected char at position " + (this.pos+i)); 420 | } 421 | 422 | this.currentNode.value += chr; 423 | break; 424 | 425 | case "PARTIAL": 426 | if(chr == ">"){ 427 | if(this.currentNode.value.substr(-1) == "."){ 428 | throw new Error("Unexpected end of partial at position " + this.pos); 429 | } 430 | this.currentNode.endPos = this.pos + i; 431 | this.currentNode.closed = true; 432 | this.currentNode = this.currentNode.parentNode; 433 | this.state = "NORMAL"; 434 | checkSP(); 435 | break; 436 | } 437 | 438 | if(chr=="." && (!this.currentNode.value.length || this.currentNode.value.match(/\./))){ 439 | throw new Error("Unexpected partial separator . at position "+ this.pos); 440 | } 441 | 442 | if(imapFormalSyntax.DIGIT().indexOf(chr) < 0 && chr != "."){ 443 | throw new Error("Unexpected char at position " + (this.pos+i)); 444 | } 445 | 446 | if(this.currentNode.value.match(/^0$|\.0$/) && chr != "."){ 447 | throw new Error("Invalid partial at position " + (this.pos + i)); 448 | } 449 | 450 | this.currentNode.value += chr; 451 | break; 452 | 453 | case "LITERAL": 454 | if(this.currentNode.started){ 455 | //if(imapFormalSyntax["CHAR8"]().indexOf(chr) < 0){ 456 | if(chr == "\u0000"){ 457 | throw new Error("Unexpected \\x00 at position " + (this.pos + i)); 458 | } 459 | this.currentNode.value += chr; 460 | 461 | if(this.currentNode.value.length >= this.currentNode.literalLength){ 462 | this.currentNode.endPos = this.pos + i; 463 | this.currentNode.closed = true; 464 | this.currentNode = this.currentNode.parentNode; 465 | this.state = "NORMAL"; 466 | checkSP(); 467 | } 468 | break; 469 | } 470 | 471 | if(chr == "+" && this.options.literalPlus){ 472 | this.currentNode.literalPlus = true; 473 | break; 474 | } 475 | 476 | if(chr == "}"){ 477 | if(!("literalLength" in this.currentNode)){ 478 | throw new Error("Unexpected literal prefix end char } at position " + (this.pos + i)); 479 | } 480 | if(this.str.charAt(i+1) == "\n"){ 481 | i++; 482 | }else if(this.str.charAt(i+1) == "\r" && this.str.charAt(i+2) == "\n"){ 483 | i += 2; 484 | }else{ 485 | throw new Error("Unexpected char at position " + (this.pos + i)); 486 | } 487 | this.currentNode.literalLength = Number(this.currentNode.literalLength); 488 | this.currentNode.started = true; 489 | break; 490 | } 491 | if(imapFormalSyntax.DIGIT().indexOf(chr) < 0){ 492 | throw new Error("Unexpected char at position " + (this.pos + i)); 493 | } 494 | if(this.currentNode.literalLength == "0"){ 495 | throw new Error("Invalid literal at position " + (this.pos + i)); 496 | } 497 | this.currentNode.literalLength = (this.currentNode.literalLength || "") + chr; 498 | break; 499 | 500 | case "SEQUENCE": 501 | // space finishes the sequence set 502 | if(chr == " "){ 503 | if(!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) != "*"){ 504 | throw new Error("Unexpected whitespace at position " + (this.pos + i)); 505 | } 506 | 507 | if(this.currentNode.value.substr(-1) == "*" && this.currentNode.value.substr(-2,1) != ":"){ 508 | throw new Error("Unexpected whitespace at position " + (this.pos + i)); 509 | } 510 | 511 | this.currentNode.closed = true; 512 | this.currentNode.endPos = this.pos + i - 1; 513 | this.currentNode = this.currentNode.parentNode; 514 | this.state = "NORMAL"; 515 | break; 516 | } 517 | 518 | if(chr == ":"){ 519 | if(!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) != "*"){ 520 | throw new Error("Unexpected range separator : at position " + (this.pos + i)); 521 | } 522 | }else if(chr == "*"){ 523 | if([",", ":"].indexOf(this.currentNode.value.substr(-1)) < 0) { 524 | throw new Error("Unexpected range wildcard at position " + (this.pos + i)); 525 | } 526 | }else if(chr == ","){ 527 | if(!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) != "*"){ 528 | throw new Error("Unexpected sequence separator , at position " + (this.pos + i)); 529 | } 530 | if(this.currentNode.value.substr(-1) == "*" && this.currentNode.value.substr(-2, 1) != ":"){ 531 | throw new Error("Unexpected sequence separator , at position " + (this.pos + i)); 532 | } 533 | }else if(!chr.match(/\d/)){ 534 | throw new Error("Unexpected char at position " + (this.pos + i)); 535 | } 536 | 537 | if(chr.match(/\d/) && this.currentNode.value.substr(-1) == "*"){ 538 | throw new Error("Unexpected number at position " + (this.pos + i)); 539 | } 540 | 541 | this.currentNode.value += chr; 542 | break; 543 | } 544 | } 545 | }; 546 | 547 | module.exports = function(command, options){ 548 | var parser, response = {}; 549 | 550 | options = options || {}; 551 | 552 | parser = new ParserInstance(command, options); 553 | 554 | response.tag = parser.getTag(); 555 | parser.getSpace(); 556 | response.command = parser.getCommand(); 557 | 558 | if(["UID", "AUTHENTICATE"].indexOf((response.command || "").toUpperCase()) >= 0){ 559 | parser.getSpace(); 560 | response.command += " " + parser.getElement(imapFormalSyntax.command()); 561 | } 562 | 563 | if(parser.remainder.length){ 564 | parser.getSpace(); 565 | response.attributes = parser.getAttributes(); 566 | } 567 | 568 | if(parser.humanReadable){ 569 | response.attributes = (response.attributes || []).concat({type:"TEXT", value: parser.humanReadable}); 570 | } 571 | 572 | return response; 573 | }; 574 | --------------------------------------------------------------------------------