libqp.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. 'use strict';
  2. var stream = require('stream');
  3. var util = require('util');
  4. var Transform = stream.Transform;
  5. // expose to the world
  6. module.exports = {
  7. encode: encode,
  8. decode: decode,
  9. wrap: wrap,
  10. Encoder: Encoder,
  11. Decoder: Decoder
  12. };
  13. /**
  14. * Encodes a Buffer into a Quoted-Printable encoded string
  15. *
  16. * @param {Buffer} buffer Buffer to convert
  17. * @returns {String} Quoted-Printable encoded string
  18. */
  19. function encode(buffer) {
  20. if (typeof buffer === 'string') {
  21. buffer = new Buffer(buffer, 'utf-8');
  22. }
  23. // usable characters that do not need encoding
  24. var ranges = [
  25. // https://tools.ietf.org/html/rfc2045#section-6.7
  26. [0x09], // <TAB>
  27. [0x0A], // <LF>
  28. [0x0D], // <CR>
  29. [0x20, 0x3C], // <SP>!"#$%&'()*+,-./0123456789:;
  30. [0x3E, 0x7E] // >?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}
  31. ];
  32. var result = '';
  33. var ord;
  34. for (var i = 0, len = buffer.length; i < len; i++) {
  35. ord = buffer[i];
  36. // if the char is in allowed range, then keep as is, unless it is a ws in the end of a line
  37. if (checkRanges(ord, ranges) && !((ord === 0x20 || ord === 0x09) && (i === len - 1 || buffer[i + 1] === 0x0a || buffer[i + 1] === 0x0d))) {
  38. result += String.fromCharCode(ord);
  39. continue;
  40. }
  41. result += '=' + (ord < 0x10 ? '0' : '') + ord.toString(16).toUpperCase();
  42. }
  43. return result;
  44. }
  45. /**
  46. * Decodes a Quoted-Printable encoded string to a Buffer object
  47. *
  48. * @param {String} str Quoted-Printable encoded string
  49. * @returns {Buffer} Decoded value
  50. */
  51. function decode(str) {
  52. str = (str || '').toString().
  53. // remove invalid whitespace from the end of lines
  54. replace(/[\t ]+$/gm, '').
  55. // remove soft line breaks
  56. replace(/\=(?:\r?\n|$)/g, '');
  57. var encodedBytesCount = (str.match(/\=[\da-fA-F]{2}/g) || []).length,
  58. bufferLength = str.length - encodedBytesCount * 2,
  59. chr, hex,
  60. buffer = new Buffer(bufferLength),
  61. bufferPos = 0;
  62. for (var i = 0, len = str.length; i < len; i++) {
  63. chr = str.charAt(i);
  64. if (chr === '=' && (hex = str.substr(i + 1, 2)) && /[\da-fA-F]{2}/.test(hex)) {
  65. buffer[bufferPos++] = parseInt(hex, 16);
  66. i += 2;
  67. continue;
  68. }
  69. buffer[bufferPos++] = chr.charCodeAt(0);
  70. }
  71. return buffer;
  72. }
  73. /**
  74. * Adds soft line breaks to a Quoted-Printable string
  75. *
  76. * @param {String} str Quoted-Printable encoded string that might need line wrapping
  77. * @param {Number} [lineLength=76] Maximum allowed length for a line
  78. * @returns {String} Soft-wrapped Quoted-Printable encoded string
  79. */
  80. function wrap(str, lineLength) {
  81. str = (str || '').toString();
  82. lineLength = lineLength || 76;
  83. if (str.length <= lineLength) {
  84. return str;
  85. }
  86. var pos = 0,
  87. len = str.length,
  88. match, code, line,
  89. lineMargin = Math.floor(lineLength / 3),
  90. result = '';
  91. // insert soft linebreaks where needed
  92. while (pos < len) {
  93. line = str.substr(pos, lineLength);
  94. if ((match = line.match(/\r\n/))) {
  95. line = line.substr(0, match.index + match[0].length);
  96. result += line;
  97. pos += line.length;
  98. continue;
  99. }
  100. if (line.substr(-1) === '\n') {
  101. // nothing to change here
  102. result += line;
  103. pos += line.length;
  104. continue;
  105. } else if ((match = line.substr(-lineMargin).match(/\n.*?$/))) {
  106. // truncate to nearest line break
  107. line = line.substr(0, line.length - (match[0].length - 1));
  108. result += line;
  109. pos += line.length;
  110. continue;
  111. } else if (line.length > lineLength - lineMargin && (match = line.substr(-lineMargin).match(/[ \t\.,!\?][^ \t\.,!\?]*$/))) {
  112. // truncate to nearest space
  113. line = line.substr(0, line.length - (match[0].length - 1));
  114. } else {
  115. if (line.match(/\=[\da-f]{0,2}$/i)) {
  116. // push incomplete encoding sequences to the next line
  117. if ((match = line.match(/\=[\da-f]{0,1}$/i))) {
  118. line = line.substr(0, line.length - match[0].length);
  119. }
  120. // ensure that utf-8 sequences are not split
  121. while (line.length > 3 && line.length < len - pos && !line.match(/^(?:=[\da-f]{2}){1,4}$/i) && (match = line.match(/\=[\da-f]{2}$/ig))) {
  122. code = parseInt(match[0].substr(1, 2), 16);
  123. if (code < 128) {
  124. break;
  125. }
  126. line = line.substr(0, line.length - 3);
  127. if (code >= 0xC0) {
  128. break;
  129. }
  130. }
  131. }
  132. }
  133. if (pos + line.length < len && line.substr(-1) !== '\n') {
  134. if (line.length === lineLength && line.match(/\=[\da-f]{2}$/i)) {
  135. line = line.substr(0, line.length - 3);
  136. } else if (line.length === lineLength) {
  137. line = line.substr(0, line.length - 1);
  138. }
  139. pos += line.length;
  140. line += '=\r\n';
  141. } else {
  142. pos += line.length;
  143. }
  144. result += line;
  145. }
  146. return result;
  147. }
  148. /**
  149. * Helper function to check if a number is inside provided ranges
  150. *
  151. * @param {Number} nr Number to check for
  152. * @param {Array} ranges An Array of allowed values
  153. * @returns {Boolean} True if the value was found inside allowed ranges, false otherwise
  154. */
  155. function checkRanges(nr, ranges) {
  156. for (var i = ranges.length - 1; i >= 0; i--) {
  157. if (!ranges[i].length) {
  158. continue;
  159. }
  160. if (ranges[i].length === 1 && nr === ranges[i][0]) {
  161. return true;
  162. }
  163. if (ranges[i].length === 2 && nr >= ranges[i][0] && nr <= ranges[i][1]) {
  164. return true;
  165. }
  166. }
  167. return false;
  168. }
  169. /**
  170. * Creates a transform stream for encoding data to Quoted-Printable encoding
  171. *
  172. * @constructor
  173. * @param {Object} options Stream options
  174. * @param {Number} [options.lineLength=76] Maximum lenght for lines, set to false to disable wrapping
  175. */
  176. function Encoder(options) {
  177. // init Transform
  178. this.options = options || {};
  179. if (this.options.lineLength !== false) {
  180. this.options.lineLength = this.options.lineLength || 76;
  181. }
  182. this._curLine = '';
  183. this.inputBytes = 0;
  184. this.outputBytes = 0;
  185. Transform.call(this, this.options);
  186. }
  187. util.inherits(Encoder, Transform);
  188. Encoder.prototype._transform = function(chunk, encoding, done) {
  189. var qp, _self = this;
  190. if (encoding !== 'buffer') {
  191. chunk = new Buffer(chunk, encoding);
  192. }
  193. if (!chunk || !chunk.length) {
  194. return done();
  195. }
  196. this.inputBytes += chunk.length;
  197. if (this.options.lineLength) {
  198. qp = this._curLine + encode(chunk);
  199. qp = wrap(qp, this.options.lineLength);
  200. qp = qp.replace(/(^|\n)([^\n]*)$/, function(match, lineBreak, lastLine) {
  201. _self._curLine = lastLine;
  202. return lineBreak;
  203. });
  204. if (qp) {
  205. this.outputBytes += qp.length;
  206. this.push(qp);
  207. }
  208. } else {
  209. qp = encode(chunk);
  210. this.outputBytes += qp.length;
  211. this.push(qp, 'ascii');
  212. }
  213. done();
  214. };
  215. Encoder.prototype._flush = function(done) {
  216. if (this._curLine) {
  217. this.outputBytes += this._curLine.length;
  218. this.push(this._curLine, 'ascii');
  219. }
  220. done();
  221. };
  222. /**
  223. * Creates a transform stream for decoding Quoted-Printable encoded strings
  224. *
  225. * @constructor
  226. * @param {Object} options Stream options
  227. */
  228. function Decoder(options) {
  229. // init Transform
  230. this.options = options || {};
  231. this._curLine = '';
  232. this.inputBytes = 0;
  233. this.outputBytes = 0;
  234. Transform.call(this, this.options);
  235. }
  236. util.inherits(Decoder, Transform);
  237. Decoder.prototype._transform = function(chunk, encoding, done) {
  238. var qp, buf, _self = this;
  239. chunk = chunk.toString('ascii');
  240. if (!chunk || !chunk.length) {
  241. return done();
  242. }
  243. this.inputBytes += chunk.length;
  244. qp = (this._curLine + chunk);
  245. this._curLine = '';
  246. qp = qp.replace(/=[^\n]?$/, function(lastLine) {
  247. _self._curLine = lastLine;
  248. return '';
  249. });
  250. if (qp) {
  251. buf = decode(qp);
  252. this.outputBytes += buf.length;
  253. this.push(buf);
  254. }
  255. done();
  256. };
  257. Decoder.prototype._flush = function(done) {
  258. var qp, buf;
  259. if (this._curLine) {
  260. buf = decode(this._curLine);
  261. this.outputBytes += buf.length;
  262. this.push(buf);
  263. }
  264. done();
  265. };