├── .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 |
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 | | <%= key %> |
52 | <%= capa.capability[key] %> |
53 |
54 |
55 | <% }) %>
56 |
57 | <% } %>
58 |
59 |
60 |
61 |
62 |
63 |
64 | | Hostname |
65 | <%= capa.host.host %> |
66 |
67 |
68 | | Port |
69 | <%= capa.host.port %> |
70 |
71 |
72 | | SSL connection |
73 | <%= capa.host.ssl ? "Yes" : "No" %> |
74 |
75 | <% if(!capa.host.ssl) {%>
76 |
77 | | Use STARTTLS (if available) |
78 | <%= capa.host.ignoreSTARTTLS ? "No" : "Yes" %> |
79 |
80 | <%}%>
81 |
82 |
83 |
84 |
85 | <% if(capa.transaction){ %>
86 |
87 | <% capa.transaction.forEach(function(item){ %>
88 |
89 |
90 | | <%= item.type %> |
91 | <%= item.payload %> |
92 |
93 |
94 | <% }) %>
95 |
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 |
--------------------------------------------------------------------------------