VulpesOS/gaia/apps/email/js/ext/pop3/transport.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

362 lines
11 KiB
JavaScript
Executable file

define(['mimefuncs', 'exports'], function(mimefuncs, exports) {
/**
* This file contains the following classes:
*
* - Pop3Parser: Parses incoming POP3 requests
* - Pop3Protocol: Uses the Pop3Parser to match requests up with responses
* - Request: Encapsulates a request to the server
* - Response: Encapsulates a response from the server
*
* The Pop3Client (in pop3.js) hooks together a socket and an
* instance of Pop3Protocol to form a complete client. See pop3.js
* for a more detailed description of the hierarchy.
*/
var setTimeout = window.setTimeout.bind(window);
var clearTimeout = window.clearTimeout.bind(window);
var MAX_LINE_LENGTH = 512; // per POP3 spec, including CRLF
var CR = '\r'.charCodeAt(0);
var LF = '\n'.charCodeAt(0);
var PERIOD = '.'.charCodeAt(0);
var PLUS = '+'.charCodeAt(0);
var MINUS = '-'.charCodeAt(0);
var SPACE = ' '.charCodeAt(0);
var textEncoder = new TextEncoder('utf-8', { fatal: false });
function concatBuffers(a, b) {
var buffer = new Uint8Array(a.length + b.length);
buffer.set(a, 0);
buffer.set(b, a.length);
return buffer;
}
/**
* Pop3Parser receives binary data (presumably from a socket) and
* parse it according to the POP3 spec:
*
* var parser = new Pop3Parser();
* parser.push(myBinaryData);
* var rsp = parser.extractResponse(false);
* if (rsp) {
* // do something with the response
* }
*/
function Pop3Parser() {
this.buffer = new Uint8Array(0); // data not yet parsed into lines
this.unprocessedLines = [];
}
/**
* Add new data to be parsed. To actually parse the incoming data
* (to see if there is enough data to extract a full response), call
* `.extractResponse()`.
*
* @param {Uint8Array} data
*/
Pop3Parser.prototype.push = function(data) {
// append the data to be processed
var buffer = this.buffer = concatBuffers(this.buffer, data);
// pull out full lines
for (var i = 0; i < buffer.length - 1; i++) {
if (buffer[i] === CR && buffer[i + 1] === LF) {
var end = i + 1;
if (end > MAX_LINE_LENGTH) {
// Sadly, servers do this, so we can't bail here.
}
this.unprocessedLines.push(buffer.subarray(0, end + 1));
buffer = this.buffer = buffer.subarray(end + 1);
i = -1;
}
}
}
/**
* Attempt to parse and return a single message from the buffered
* data. Since the POP3 protocol does not provide a foolproof way to
* determine whether a given message is multiline without tracking
* request state, you must specify whether or not the response is
* expected to be multiline.
*
* Multiple responses may be available; you should call
* `.extractResponse()` repeatedly until no more responses are
* available. This method returns null if there was not enough data
* to parse and return a response.
*
* @param {boolean} multiline true to parse a multiline response.
* @return {Response|null}
*/
Pop3Parser.prototype.extractResponse = function(multiline) {
if (!this.unprocessedLines.length) {
return null;
}
if (this.unprocessedLines[0][0] !== PLUS) {
multiline = false; // Negative responses are never multiline.
}
if (!multiline) {
return new Response([this.unprocessedLines.shift()], false);
} else {
var endLineIndex = -1;
for (var i = 1; i < this.unprocessedLines.length; i++) {
var line = this.unprocessedLines[i];
if (line.byteLength === 3 &&
line[0] === PERIOD && line[1] === CR && line[2] === LF) {
endLineIndex = i;
break;
}
}
if (endLineIndex === -1) {
return null;
}
var lines = this.unprocessedLines.splice(0, endLineIndex + 1);
lines.pop(); // remove final ".\r\n" line
// the first line cannot be stuffed (it's the command OK/ERR
// response). Other lines may be period-stuffed.
for (var i = 1; i < endLineIndex; i++) {
if (lines[i][0] === PERIOD) {
lines[i] = lines[i].subarray(1);
}
}
return new Response(lines, true);
}
}
/**
* Represent a POP3 response (both success and failure). You should
* not have to instantiate this class directly; Pop3Parser returns
* these objects from `Pop3Parser.extractResponse()`.
*
* @param {UInt8Array[]} lines
* @param {boolean} isMultiline
*/
function Response(lines, isMultiline) {
this.lines = lines; // list of UInt8Arrays
this.isMultiline = isMultiline;
this.ok = (this.lines[0][0] === PLUS);
this.err = !this.ok;
this.request = null;
}
/**
* Return the description text for the status line as a string.
*/
Response.prototype.getStatusLine = function() {
return this.getLineAsString(0).replace(/^(\+OK|-ERR) /, '');
}
/**
* Return the line at `index` as a string.
*
* @param {int} index
* @return {String}
*/
Response.prototype.getLineAsString = function(index) {
return mimefuncs.fromTypedArray(this.lines[index]);
}
/**
* Return an array of strings, one for each line, including CRLFs.
* If you want to parse the data from a response, use
* `.getDataLines()`.
*
* @return {String[]}
*/
Response.prototype.getLinesAsString = function() {
var lines = [];
for (var i = 0; i < this.lines.length; i++) {
lines.push(this.getLineAsString(i));
}
return lines;
}
/**
* Return an array of strings, _excluding_ CRLFs, starting from the
* line after the +OK/-ERR line.
*/
Response.prototype.getDataLines = function() {
var lines = [];
for (var i = 1; i < this.lines.length; i++) {
var line = this.getLineAsString(i);
lines.push(line.slice(0, line.length - 2)); // strip CRLF
}
return lines;
}
/**
* Return the data portion of a multiline response as a string,
* with the lines' CRLFs intact.
*/
Response.prototype.getDataAsString = function() {
var lines = [];
for (var i = 1; i < this.lines.length; i++) {
lines.push(this.getLineAsString(i));
}
return lines.join(''); // lines already have '\r\n'
}
/**
* Return a string representation of the message, primarily for
* debugging purposes.
*/
Response.prototype.toString = function() {
return this.getLinesAsString().join('\r\n');
}
/**
* Represent a POP3 request, with enough data to allow the parser
* to parse out a response and invoke a callback upon receiving a
* response.
*
* @param {string} command The command, like RETR, USER, etc.
* @param {string[]} args Arguments to the command, as an array.
* @param {boolean} expectMultiline Whether or not the response will
* be multiline.
* @param {function(err, rsp)} cb The callback to invoke when a
* response is received.
*/
function Request(command, args, expectMultiline, cb) {
this.command = command;
this.args = args;
this.expectMultiline = expectMultiline;
this.onresponse = cb || null;
}
exports.Request = Request;
/**
* Encode the request into a byte array suitable for transport over
* a socket.
*/
Request.prototype.toByteArray = function() {
return textEncoder.encode(
this.command + (this.args.length ? ' ' + this.args.join(' ') : '') + '\r\n');
}
/**
* Trigger the response callback with '-ERR desc\r\n'.
*/
Request.prototype._respondWithError = function(desc) {
var rsp = new Response([textEncoder.encode(
'-ERR ' + desc + '\r\n')], false);
rsp.request = this;
this.onresponse(rsp, null);
}
/**
* Couple a POP3 parser with a request/response model, such that
* you can easily hook Pop3Protocol up to a socket (or other
* transport) to get proper request/response semantics.
*
* You must attach a handler to `.onsend`, which should fire data
* across the wire. Similarly, you should call `.onreceive(data)` to
* pass data back in from the socket.
*/
function Pop3Protocol() {
this.parser = new Pop3Parser();
this.onsend = function(data) {
throw new Error("You must implement Pop3Protocol.onsend to send data.");
};
this.unsentRequests = []; // if not pipelining, queue requests one at a time
this.pipeline = false;
this.pendingRequests = [];
this.closed = false;
}
exports.Response = Response;
exports.Pop3Protocol = Pop3Protocol;
/**
* Send a request to the server. Upon receiving a response, the
* callback will be invoked, node-style, with an err or a response.
* Negative replies (-ERR) are returned as an error to the callback;
* positive replies (+OK) as a response. Socket errors are returned
* as an error to the callback.
*
* @param {string} cmd The command like USER, RETR, etc.
* @param {string[]} args An array of arguments to the command.
* @param {boolean} expectMultiline Whether or not the response will
* be multiline.
* @param {function(err, rsp)} cb The callback to invoke upon
* receipt of a response.
*/
Pop3Protocol.prototype.sendRequest = function(
cmd, args, expectMultiline, cb) {
var req;
if (cmd instanceof Request) {
req = cmd;
} else {
req = new Request(cmd, args, expectMultiline, cb);
}
if (this.closed) {
req._respondWithError('(request sent after connection closed)');
return;
}
if (this.pipeline || this.pendingRequests.length === 0) {
this.onsend(req.toByteArray());
this.pendingRequests.push(req);
} else {
this.unsentRequests.push(req);
}
}
/**
* Call this function to send received data to the parser. This
* method automatically calls the appropriate response callback for
* its respective request.
*/
Pop3Protocol.prototype.onreceive = function(evt) {
this.parser.push(new Uint8Array(evt.data));
var response;
while (true) {
var req = this.pendingRequests[0];
response = this.parser.extractResponse(req && req.expectMultiline);
if (!response) {
break;
} else if (!req) {
// It's unclear how to handle this in the most nondestructive way;
// if we receive an unsolicited response, something has gone horribly
// wrong, and it's unlikely that we'll be able to recover.
console.error('Unsolicited response from server: ' + response);
break;
}
response.request = req;
this.pendingRequests.shift();
if (this.unsentRequests.length) {
this.sendRequest(this.unsentRequests.shift());
}
if (req.onresponse) {
if (response.err) {
req.onresponse(response, null);
} else {
req.onresponse(null, response);
}
}
}
}
/**
* Call this function when the socket attached to this protocol is
* closed. Any current requests that have been enqueued but not yet
* responded to will be sent a dummy "-ERR" response, indicating
* that the underlying connection closed without actually
* responding. This avoids the case where we hang if we never
* receive a response from the server.
*/
Pop3Protocol.prototype.onclose = function() {
this.closed = true;
var requestsToRespond = this.pendingRequests.concat(this.unsentRequests);
this.pendingRequests = [];
this.unsentRequests = [];
for (var i = 0; i < requestsToRespond.length; i++) {
var req = requestsToRespond[i];
req._respondWithError('(connection closed, no response)');
}
}
});