PageRenderTime 58ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 1ms

/apps/email/js/ext/pop3/transport.js

https://gitlab.com/lcuguen/gaia
JavaScript | 362 lines | 189 code | 29 blank | 144 comment | 35 complexity | 47601e99f21c33105223fbfbf2d59f2a MD5 | raw file
  1. define(['mimefuncs', 'exports'], function(mimefuncs, exports) {
  2. /**
  3. * This file contains the following classes:
  4. *
  5. * - Pop3Parser: Parses incoming POP3 requests
  6. * - Pop3Protocol: Uses the Pop3Parser to match requests up with responses
  7. * - Request: Encapsulates a request to the server
  8. * - Response: Encapsulates a response from the server
  9. *
  10. * The Pop3Client (in pop3.js) hooks together a socket and an
  11. * instance of Pop3Protocol to form a complete client. See pop3.js
  12. * for a more detailed description of the hierarchy.
  13. */
  14. var setTimeout = window.setTimeout.bind(window);
  15. var clearTimeout = window.clearTimeout.bind(window);
  16. var MAX_LINE_LENGTH = 512; // per POP3 spec, including CRLF
  17. var CR = '\r'.charCodeAt(0);
  18. var LF = '\n'.charCodeAt(0);
  19. var PERIOD = '.'.charCodeAt(0);
  20. var PLUS = '+'.charCodeAt(0);
  21. var MINUS = '-'.charCodeAt(0);
  22. var SPACE = ' '.charCodeAt(0);
  23. var textEncoder = new TextEncoder('utf-8', { fatal: false });
  24. function concatBuffers(a, b) {
  25. var buffer = new Uint8Array(a.length + b.length);
  26. buffer.set(a, 0);
  27. buffer.set(b, a.length);
  28. return buffer;
  29. }
  30. /**
  31. * Pop3Parser receives binary data (presumably from a socket) and
  32. * parse it according to the POP3 spec:
  33. *
  34. * var parser = new Pop3Parser();
  35. * parser.push(myBinaryData);
  36. * var rsp = parser.extractResponse(false);
  37. * if (rsp) {
  38. * // do something with the response
  39. * }
  40. */
  41. function Pop3Parser() {
  42. this.buffer = new Uint8Array(0); // data not yet parsed into lines
  43. this.unprocessedLines = [];
  44. }
  45. /**
  46. * Add new data to be parsed. To actually parse the incoming data
  47. * (to see if there is enough data to extract a full response), call
  48. * `.extractResponse()`.
  49. *
  50. * @param {Uint8Array} data
  51. */
  52. Pop3Parser.prototype.push = function(data) {
  53. // append the data to be processed
  54. var buffer = this.buffer = concatBuffers(this.buffer, data);
  55. // pull out full lines
  56. for (var i = 0; i < buffer.length - 1; i++) {
  57. if (buffer[i] === CR && buffer[i + 1] === LF) {
  58. var end = i + 1;
  59. if (end > MAX_LINE_LENGTH) {
  60. // Sadly, servers do this, so we can't bail here.
  61. }
  62. this.unprocessedLines.push(buffer.subarray(0, end + 1));
  63. buffer = this.buffer = buffer.subarray(end + 1);
  64. i = -1;
  65. }
  66. }
  67. }
  68. /**
  69. * Attempt to parse and return a single message from the buffered
  70. * data. Since the POP3 protocol does not provide a foolproof way to
  71. * determine whether a given message is multiline without tracking
  72. * request state, you must specify whether or not the response is
  73. * expected to be multiline.
  74. *
  75. * Multiple responses may be available; you should call
  76. * `.extractResponse()` repeatedly until no more responses are
  77. * available. This method returns null if there was not enough data
  78. * to parse and return a response.
  79. *
  80. * @param {boolean} multiline true to parse a multiline response.
  81. * @return {Response|null}
  82. */
  83. Pop3Parser.prototype.extractResponse = function(multiline) {
  84. if (!this.unprocessedLines.length) {
  85. return null;
  86. }
  87. if (this.unprocessedLines[0][0] !== PLUS) {
  88. multiline = false; // Negative responses are never multiline.
  89. }
  90. if (!multiline) {
  91. return new Response([this.unprocessedLines.shift()], false);
  92. } else {
  93. var endLineIndex = -1;
  94. for (var i = 1; i < this.unprocessedLines.length; i++) {
  95. var line = this.unprocessedLines[i];
  96. if (line.byteLength === 3 &&
  97. line[0] === PERIOD && line[1] === CR && line[2] === LF) {
  98. endLineIndex = i;
  99. break;
  100. }
  101. }
  102. if (endLineIndex === -1) {
  103. return null;
  104. }
  105. var lines = this.unprocessedLines.splice(0, endLineIndex + 1);
  106. lines.pop(); // remove final ".\r\n" line
  107. // the first line cannot be stuffed (it's the command OK/ERR
  108. // response). Other lines may be period-stuffed.
  109. for (var i = 1; i < endLineIndex; i++) {
  110. if (lines[i][0] === PERIOD) {
  111. lines[i] = lines[i].subarray(1);
  112. }
  113. }
  114. return new Response(lines, true);
  115. }
  116. }
  117. /**
  118. * Represent a POP3 response (both success and failure). You should
  119. * not have to instantiate this class directly; Pop3Parser returns
  120. * these objects from `Pop3Parser.extractResponse()`.
  121. *
  122. * @param {UInt8Array[]} lines
  123. * @param {boolean} isMultiline
  124. */
  125. function Response(lines, isMultiline) {
  126. this.lines = lines; // list of UInt8Arrays
  127. this.isMultiline = isMultiline;
  128. this.ok = (this.lines[0][0] === PLUS);
  129. this.err = !this.ok;
  130. this.request = null;
  131. }
  132. /**
  133. * Return the description text for the status line as a string.
  134. */
  135. Response.prototype.getStatusLine = function() {
  136. return this.getLineAsString(0).replace(/^(\+OK|-ERR) /, '');
  137. }
  138. /**
  139. * Return the line at `index` as a string.
  140. *
  141. * @param {int} index
  142. * @return {String}
  143. */
  144. Response.prototype.getLineAsString = function(index) {
  145. return mimefuncs.fromTypedArray(this.lines[index]);
  146. }
  147. /**
  148. * Return an array of strings, one for each line, including CRLFs.
  149. * If you want to parse the data from a response, use
  150. * `.getDataLines()`.
  151. *
  152. * @return {String[]}
  153. */
  154. Response.prototype.getLinesAsString = function() {
  155. var lines = [];
  156. for (var i = 0; i < this.lines.length; i++) {
  157. lines.push(this.getLineAsString(i));
  158. }
  159. return lines;
  160. }
  161. /**
  162. * Return an array of strings, _excluding_ CRLFs, starting from the
  163. * line after the +OK/-ERR line.
  164. */
  165. Response.prototype.getDataLines = function() {
  166. var lines = [];
  167. for (var i = 1; i < this.lines.length; i++) {
  168. var line = this.getLineAsString(i);
  169. lines.push(line.slice(0, line.length - 2)); // strip CRLF
  170. }
  171. return lines;
  172. }
  173. /**
  174. * Return the data portion of a multiline response as a string,
  175. * with the lines' CRLFs intact.
  176. */
  177. Response.prototype.getDataAsString = function() {
  178. var lines = [];
  179. for (var i = 1; i < this.lines.length; i++) {
  180. lines.push(this.getLineAsString(i));
  181. }
  182. return lines.join(''); // lines already have '\r\n'
  183. }
  184. /**
  185. * Return a string representation of the message, primarily for
  186. * debugging purposes.
  187. */
  188. Response.prototype.toString = function() {
  189. return this.getLinesAsString().join('\r\n');
  190. }
  191. /**
  192. * Represent a POP3 request, with enough data to allow the parser
  193. * to parse out a response and invoke a callback upon receiving a
  194. * response.
  195. *
  196. * @param {string} command The command, like RETR, USER, etc.
  197. * @param {string[]} args Arguments to the command, as an array.
  198. * @param {boolean} expectMultiline Whether or not the response will
  199. * be multiline.
  200. * @param {function(err, rsp)} cb The callback to invoke when a
  201. * response is received.
  202. */
  203. function Request(command, args, expectMultiline, cb) {
  204. this.command = command;
  205. this.args = args;
  206. this.expectMultiline = expectMultiline;
  207. this.onresponse = cb || null;
  208. }
  209. exports.Request = Request;
  210. /**
  211. * Encode the request into a byte array suitable for transport over
  212. * a socket.
  213. */
  214. Request.prototype.toByteArray = function() {
  215. return textEncoder.encode(
  216. this.command + (this.args.length ? ' ' + this.args.join(' ') : '') + '\r\n');
  217. }
  218. /**
  219. * Trigger the response callback with '-ERR desc\r\n'.
  220. */
  221. Request.prototype._respondWithError = function(desc) {
  222. var rsp = new Response([textEncoder.encode(
  223. '-ERR ' + desc + '\r\n')], false);
  224. rsp.request = this;
  225. this.onresponse(rsp, null);
  226. }
  227. /**
  228. * Couple a POP3 parser with a request/response model, such that
  229. * you can easily hook Pop3Protocol up to a socket (or other
  230. * transport) to get proper request/response semantics.
  231. *
  232. * You must attach a handler to `.onsend`, which should fire data
  233. * across the wire. Similarly, you should call `.onreceive(data)` to
  234. * pass data back in from the socket.
  235. */
  236. function Pop3Protocol() {
  237. this.parser = new Pop3Parser();
  238. this.onsend = function(data) {
  239. throw new Error("You must implement Pop3Protocol.onsend to send data.");
  240. };
  241. this.unsentRequests = []; // if not pipelining, queue requests one at a time
  242. this.pipeline = false;
  243. this.pendingRequests = [];
  244. this.closed = false;
  245. }
  246. exports.Response = Response;
  247. exports.Pop3Protocol = Pop3Protocol;
  248. /**
  249. * Send a request to the server. Upon receiving a response, the
  250. * callback will be invoked, node-style, with an err or a response.
  251. * Negative replies (-ERR) are returned as an error to the callback;
  252. * positive replies (+OK) as a response. Socket errors are returned
  253. * as an error to the callback.
  254. *
  255. * @param {string} cmd The command like USER, RETR, etc.
  256. * @param {string[]} args An array of arguments to the command.
  257. * @param {boolean} expectMultiline Whether or not the response will
  258. * be multiline.
  259. * @param {function(err, rsp)} cb The callback to invoke upon
  260. * receipt of a response.
  261. */
  262. Pop3Protocol.prototype.sendRequest = function(
  263. cmd, args, expectMultiline, cb) {
  264. var req;
  265. if (cmd instanceof Request) {
  266. req = cmd;
  267. } else {
  268. req = new Request(cmd, args, expectMultiline, cb);
  269. }
  270. if (this.closed) {
  271. req._respondWithError('(request sent after connection closed)');
  272. return;
  273. }
  274. if (this.pipeline || this.pendingRequests.length === 0) {
  275. this.onsend(req.toByteArray());
  276. this.pendingRequests.push(req);
  277. } else {
  278. this.unsentRequests.push(req);
  279. }
  280. }
  281. /**
  282. * Call this function to send received data to the parser. This
  283. * method automatically calls the appropriate response callback for
  284. * its respective request.
  285. */
  286. Pop3Protocol.prototype.onreceive = function(evt) {
  287. this.parser.push(new Uint8Array(evt.data));
  288. var response;
  289. while (true) {
  290. var req = this.pendingRequests[0];
  291. response = this.parser.extractResponse(req && req.expectMultiline);
  292. if (!response) {
  293. break;
  294. } else if (!req) {
  295. // It's unclear how to handle this in the most nondestructive way;
  296. // if we receive an unsolicited response, something has gone horribly
  297. // wrong, and it's unlikely that we'll be able to recover.
  298. console.error('Unsolicited response from server: ' + response);
  299. break;
  300. }
  301. response.request = req;
  302. this.pendingRequests.shift();
  303. if (this.unsentRequests.length) {
  304. this.sendRequest(this.unsentRequests.shift());
  305. }
  306. if (req.onresponse) {
  307. if (response.err) {
  308. req.onresponse(response, null);
  309. } else {
  310. req.onresponse(null, response);
  311. }
  312. }
  313. }
  314. }
  315. /**
  316. * Call this function when the socket attached to this protocol is
  317. * closed. Any current requests that have been enqueued but not yet
  318. * responded to will be sent a dummy "-ERR" response, indicating
  319. * that the underlying connection closed without actually
  320. * responding. This avoids the case where we hang if we never
  321. * receive a response from the server.
  322. */
  323. Pop3Protocol.prototype.onclose = function() {
  324. this.closed = true;
  325. var requestsToRespond = this.pendingRequests.concat(this.unsentRequests);
  326. this.pendingRequests = [];
  327. this.unsentRequests = [];
  328. for (var i = 0; i < requestsToRespond.length; i++) {
  329. var req = requestsToRespond[i];
  330. req._respondWithError('(connection closed, no response)');
  331. }
  332. }
  333. });