diff --git a/README.md b/README.md new file mode 100644 index 0000000..b0f880c --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +mailparser +========== + +**mailparser** is an asynchronous and non-blocking parser for mime encoded e-mail messages. Handles even large +attachments with ease - attachments are parsed in chunks that can be saved into disk or sent to database while parsing. + +**mailparser** parses raw source of e-mail messages to convert mime-stream into a structured object. + +Requirements +------------ + +You need to have (node-iconv)[http://github.com/bnoordhuis/node-iconv] installed. Update *mime.js* to point to the correct location. + +Usage +----- + +Create a new *mailparser* object + + var mp = new MailParser(); + +Set up listener for different events + + * Get mail headers as a structured object + + mp.on("header", function(headers){ + console.log(headers); + }); + + * Get mail body as a structured object + + mp.on("body", function(body){ + console.log(body); + }); + + * Get part of a binary attachment in the form of a Buffer + + mp.on("astream", function(id, buffer){ + console.log("attachment id" + id); + console.log(buffer); + }); + + * Attachment parsing completed + + mp.on("aend", function(id){ + console.log("attachment " + id + " finished"); + }); + +Feed the parser with data + + mp.feed(part1_of_the_message); + mp.feed(part2_of_the_message); + mp.feed(part3_of_the_message); + ... + mp.feed(partN_of_the_message); + +Finish the feeding + + mp.end(); \ No newline at end of file diff --git a/datetime.js b/datetime.js new file mode 100644 index 0000000..a20e497 --- /dev/null +++ b/datetime.js @@ -0,0 +1,304 @@ +/* + * More info at: http://phpjs.org + * + * This is version: 3.18 + * php.js is copyright 2010 Kevin van Zonneveld. + * + * Portions copyright Brett Zamir (http://brett-zamir.me), Kevin van Zonneveld + * (http://kevin.vanzonneveld.net), Onno Marsman, Theriault, Michael White + * (http://getsprink.com), Waldo Malqui Silva, Paulo Freitas, Jonas Raoni + * Soares Silva (http://www.jsfromhell.com), Jack, Philip Peterson, Ates Goral + * (http://magnetiq.com), Legaev Andrey, Ratheous, Alex, Martijn Wieringa, + * Nate, lmeyrick (https://sourceforge.net/projects/bcmath-js/), Philippe + * Baumann, Enrique Gonzalez, Webtoolkit.info (http://www.webtoolkit.info/), + * Ash Searle (http://hexmen.com/blog/), travc, Jani Hartikainen, Carlos R. L. + * Rodrigues (http://www.jsfromhell.com), Ole Vrijenhoek, WebDevHobo + * (http://webdevhobo.blogspot.com/), T.Wild, + * http://stackoverflow.com/questions/57803/how-to-convert-decimal-to-hex-in-javascript, + * pilus, GeekFG (http://geekfg.blogspot.com), Rafał Kukawski + * (http://blog.kukawski.pl), Johnny Mast (http://www.phpvrouwen.nl), Michael + * Grier, Erkekjetter, d3x, marrtins, Andrea Giammarchi + * (http://webreflection.blogspot.com), stag019, mdsjack + * (http://www.mdsjack.bo.it), Chris, Steven Levithan + * (http://blog.stevenlevithan.com), Arpad Ray (mailto:arpad@php.net), David, + * Joris, Tim de Koning (http://www.kingsquare.nl), Marc Palau, Michael White, + * Public Domain (http://www.json.org/json2.js), gettimeofday, felix, Aman + * Gupta, Pellentesque Malesuada, Thunder.m, Tyler Akins (http://rumkin.com), + * Karol Kowalski, Felix Geisendoerfer (http://www.debuggable.com/felix), + * Alfonso Jimenez (http://www.alfonsojimenez.com), Diplom@t + * (http://difane.com/), majak, Mirek Slugen, Mailfaker + * (http://www.weedem.fr/), Breaking Par Consulting Inc + * (http://www.breakingpar.com/bkp/home.nsf/0/87256B280015193F87256CFB006C45F7), + * Josh Fraser + * (http://onlineaspect.com/2007/06/08/auto-detect-a-time-zone-with-javascript/), + * Martin (http://www.erlenwiese.de/), Paul Smith, KELAN, Robin, saulius, AJ, + * Oleg Eremeev, Steve Hilder, gorthaur, Kankrelune + * (http://www.webfaktory.info/), Caio Ariede (http://caioariede.com), Lars + * Fischer, Sakimori, Imgen Tata (http://www.myipdf.com/), uestla, Artur + * Tchernychev, Wagner B. Soares, Christoph, nord_ua, class_exists, Der Simon + * (http://innerdom.sourceforge.net/), echo is bad, XoraX + * (http://www.xorax.info), Ozh, Alan C, Taras Bogach, Brad Touesnard, MeEtc + * (http://yass.meetcweb.com), Peter-Paul Koch + * (http://www.quirksmode.org/js/beat.html), T0bsn, Tim Wiel, Bryan Elliott, + * jpfle, JT, Thomas Beaucourt (http://www.webapp.fr), David Randall, Frank + * Forte, Eugene Bulkin (http://doubleaw.com/), noname, kenneth, Hyam Singer + * (http://www.impact-computing.com/), Marco, Raphael (Ao RUDLER), Ole + * Vrijenhoek (http://www.nervous.nl/), David James, Steve Clay, Jason Wong + * (http://carrot.org/), T. Wild, Paul, J A R, LH, strcasecmp, strcmp, JB, + * Daniel Esteban, strftime, madipta, Valentina De Rosa, Marc Jansen, + * Francesco, Stoyan Kyosev (http://www.svest.org/), metjay, Soren Hansen, + * 0m3r, Sanjoy Roy, Shingo, sankai, sowberry, hitwork, Rob, Norman "zEh" + * Fuchs, Subhasis Deb, josh, Yves Sucaet, Ulrich, Scott Baker, ejsanders, + * Nick Callen, Steven Levithan (stevenlevithan.com), Aidan Lister + * (http://aidanlister.com/), Philippe Jausions + * (http://pear.php.net/user/jausions), Zahlii, Denny Wardhana, Oskar Larsson + * Högfeldt (http://oskar-lh.name/), Brian Tafoya + * (http://www.premasolutions.com/), johnrembo, Gilbert, duncan, Thiago Mata + * (http://thiagomata.blog.com), Alexander Ermolaev + * (http://snippets.dzone.com/user/AlexanderErmolaev), Linuxworld, lmeyrick + * (https://sourceforge.net/projects/bcmath-js/this.), Jon Hohle, Pyerre, + * merabi, Saulo Vallory, HKM, ChaosNo1, djmix, Lincoln Ramsay, Adam Wallner + * (http://web2.bitbaro.hu/), paulo kuong, jmweb, Orlando, kilops, dptr1988, + * DxGx, Pedro Tainha (http://www.pedrotainha.com), Bayron Guevara, Le Torbi, + * James, Douglas Crockford (http://javascript.crockford.com), Devan + * Penner-Woelk, Jay Klehr, Kheang Hok Chin (http://www.distantia.ca/), Luke + * Smith (http://lucassmith.name), Rival, Amir Habibi + * (http://www.residence-mixte.com/), Blues (http://tech.bluesmoon.info/), Ben + * Bryan, booeyOH, Dreamer, Cagri Ekin, Diogo Resende, Howard Yeend, Pul, + * 3D-GRAF, jakes, Yannoo, Luke Godfrey, daniel airton wermann + * (http://wermann.com.br), Allan Jensen (http://www.winternet.no), Benjamin + * Lupton, davook, Atli Þór, Maximusya, Leslie Hoare, Bug?, setcookie, YUI + * Library: http://developer.yahoo.com/yui/docs/YAHOO.util.DateLocale.html, + * Blues at http://hacks.bluesmoon.info/strftime/strftime.js, Andreas, + * Michael, Christian Doebler, Gabriel Paderni, Marco van Oort, Philipp + * Lenssen, Arnout Kazemier (http://www.3rd-Eden.com), penutbutterjelly, Anton + * Ongson, DtTvB (http://dt.in.th/2008-09-16.string-length-in-bytes.html), + * meo, Greenseed, Yen-Wei Liu, mk.keck, William, rem, Jamie Beck + * (http://www.terabit.ca/), Russell Walker (http://www.nbill.co.uk/), + * Garagoth, Dino, Andrej Pavlovic, gabriel paderni, FGFEmperor, Scott Cariss, + * Slawomir Kaniecki, ReverseSyntax, Mateusz "loonquawl" Zalega, Francois, + * Kirk Strobeck, Billy, vlado houba, Jalal Berrami, date, Itsacon + * (http://www.itsacon.net/), Martin Pool, Pierre-Luc Paour, ger, john + * (http://www.jd-tech.net), mktime, Simon Willison + * (http://simonwillison.net), Nick Kolosov (http://sammy.ru), marc andreu, + * Arno, Nathan, Kristof Coomans (SCK-CEN Belgian Nucleair Research Centre), + * Fox, nobbler, stensi, Matteo, Riddler (http://www.frontierwebdev.com/), + * Tomasz Wesolowski, T.J. Leahy, rezna, Eric Nagel, Alexander M Beedie, baris + * ozdil, Greg Frazier, Bobby Drake, Ryan W Tenney (http://ryan.10e.us), Tod + * Gentille, Rafał Kukawski, FremyCompany, Manish, Cord, fearphage + * (http://http/my.opera.com/fearphage/), Victor, Brant Messenger + * (http://www.brantmessenger.com/), Matt Bradley, Luis Salazar + * (http://www.freaky-media.com/), Tim de Koning, taith, Rick Waldron, Mick@el + * + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL KEVIN VAN ZONNEVELD BE LIABLE FOR ANY CLAIM, DAMAGES + * OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ +this.strtotime = function(str, now) { + // http://kevin.vanzonneveld.net + // + original by: Caio Ariede (http://caioariede.com) + // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + input by: David + // + improved by: Caio Ariede (http://caioariede.com) + // + improved by: Brett Zamir (http://brett-zamir.me) + // + bugfixed by: Wagner B. Soares + // + bugfixed by: Artur Tchernychev + // % note 1: Examples all have a fixed timestamp to prevent tests to fail because of variable time(zones) + // * example 1: strtotime('+1 day', 1129633200); + // * returns 1: 1129719600 + // * example 2: strtotime('+1 week 2 days 4 hours 2 seconds', 1129633200); + // * returns 2: 1130425202 + // * example 3: strtotime('last month', 1129633200); + // * returns 3: 1127041200 + // * example 4: strtotime('2009-05-04 08:30:00'); + // * returns 4: 1241418600 + + var i, match, s, strTmp = '', parse = ''; + + strTmp = str; + strTmp = strTmp.replace(/\s{2,}|^\s|\s$/g, ' '); // unecessary spaces + strTmp = strTmp.replace(/[\t\r\n]/g, ''); // unecessary chars + + if (strTmp == 'now') { + return (new Date()).getTime()/1000; // Return seconds, not milli-seconds + } else if (!isNaN(parse = Date.parse(strTmp))) { + return (parse/1000); + } else if (now) { + now = new Date(now*1000); // Accept PHP-style seconds + } else { + now = new Date(); + } + + strTmp = strTmp.toLowerCase(); + + var __is = + { + day: + { + 'sun': 0, + 'mon': 1, + 'tue': 2, + 'wed': 3, + 'thu': 4, + 'fri': 5, + 'sat': 6 + }, + mon: + { + 'jan': 0, + 'feb': 1, + 'mar': 2, + 'apr': 3, + 'may': 4, + 'jun': 5, + 'jul': 6, + 'aug': 7, + 'sep': 8, + 'oct': 9, + 'nov': 10, + 'dec': 11 + } + }; + + var process = function (m) { + var ago = (m[2] && m[2] == 'ago'); + var num = (num = m[0] == 'last' ? -1 : 1) * (ago ? -1 : 1); + + switch (m[0]) { + case 'last': + case 'next': + switch (m[1].substring(0, 3)) { + case 'yea': + now.setFullYear(now.getFullYear() + num); + break; + case 'mon': + now.setMonth(now.getMonth() + num); + break; + case 'wee': + now.setDate(now.getDate() + (num * 7)); + break; + case 'day': + now.setDate(now.getDate() + num); + break; + case 'hou': + now.setHours(now.getHours() + num); + break; + case 'min': + now.setMinutes(now.getMinutes() + num); + break; + case 'sec': + now.setSeconds(now.getSeconds() + num); + break; + default: + var day; + if (typeof (day = __is.day[m[1].substring(0, 3)]) != 'undefined') { + var diff = day - now.getDay(); + if (diff == 0) { + diff = 7 * num; + } else if (diff > 0) { + if (m[0] == 'last') {diff -= 7;} + } else { + if (m[0] == 'next') {diff += 7;} + } + now.setDate(now.getDate() + diff); + } + } + break; + + default: + if (/\d+/.test(m[0])) { + num *= parseInt(m[0], 10); + + switch (m[1].substring(0, 3)) { + case 'yea': + now.setFullYear(now.getFullYear() + num); + break; + case 'mon': + now.setMonth(now.getMonth() + num); + break; + case 'wee': + now.setDate(now.getDate() + (num * 7)); + break; + case 'day': + now.setDate(now.getDate() + num); + break; + case 'hou': + now.setHours(now.getHours() + num); + break; + case 'min': + now.setMinutes(now.getMinutes() + num); + break; + case 'sec': + now.setSeconds(now.getSeconds() + num); + break; + } + } else { + return false; + } + break; + } + return true; + }; + + match = strTmp.match(/^(\d{2,4}-\d{2}-\d{2})(?:\s(\d{1,2}:\d{2}(:\d{2})?)?(?:\.(\d+))?)?$/); + if (match != null) { + if (!match[2]) { + match[2] = '00:00:00'; + } else if (!match[3]) { + match[2] += ':00'; + } + + s = match[1].split(/-/g); + + for (i in __is.mon) { + if (__is.mon[i] == s[1] - 1) { + s[1] = i; + } + } + s[0] = parseInt(s[0], 10); + + s[0] = (s[0] >= 0 && s[0] <= 69) ? '20'+(s[0] < 10 ? '0'+s[0] : s[0]+'') : (s[0] >= 70 && s[0] <= 99) ? '19'+s[0] : s[0]+''; + return parseInt(this.strtotime(s[2] + ' ' + s[1] + ' ' + s[0] + ' ' + match[2])+(match[4] ? match[4]/1000 : ''), 10); + } + + var regex = '([+-]?\\d+\\s'+ + '(years?|months?|weeks?|days?|hours?|min|minutes?|sec|seconds?'+ + '|sun\\.?|sunday|mon\\.?|monday|tue\\.?|tuesday|wed\\.?|wednesday'+ + '|thu\\.?|thursday|fri\\.?|friday|sat\\.?|saturday)'+ + '|(last|next)\\s'+ + '(years?|months?|weeks?|days?|hours?|min|minutes?|sec|seconds?'+ + '|sun\\.?|sunday|mon\\.?|monday|tue\\.?|tuesday|wed\\.?|wednesday'+ + '|thu\\.?|thursday|fri\\.?|friday|sat\\.?|saturday))'+ + '(\\sago)?'; + + match = strTmp.match(new RegExp(regex, 'gi')); // Brett: seems should be case insensitive per docs, so added 'i' + if (match == null) { + return false; + } + + for (i = 0; i < match.length; i++) { + if (!process(match[i].split(' '))) { + return false; + } + } + + return (now.getTime()/1000); +} \ No newline at end of file diff --git a/mail.txt b/mail.txt new file mode 100644 index 0000000..ca1e429 --- /dev/null +++ b/mail.txt @@ -0,0 +1,89 @@ +Return-Path: +Received: from andris-desktop.lan (87-119-170-102.tll.elisa.ee [87.119.170.102]) + by mx.google.com with ESMTPS id z55sm3472728eeh.3.2010.10.07.06.41.50 + (version=TLSv1/SSLv3 cipher=RC4-MD5); + Thu, 07 Oct 2010 06:41:51 -0700 (PDT) +Message-Id: +From: Andris Reinman +To: andris@kreata.ee +Content-Type: multipart/alternative; boundary=Apple-Mail-2-1061547935 +Mime-Version: 1.0 (Apple Message framework v936) +Subject: =?ISO-8859-4?Q?Simple_test_message_with_special_characters_like_?= + =?ISO-8859-4?Q?=B9_and_=F5?= +Date: Thu, 7 Oct 2010 16:41:49 +0300 +X-Mailer: Apple Mail (2.936) + + +--Apple-Mail-2-1061547935 +Content-Type: text/plain; + charset=ISO-8859-4; + format=flowed; + delsp=yes +Content-Transfer-Encoding: quoted-printable + +This is a simple e-mail with a) special characters (=F6=E4=F5=FC), b) = +HTML =20 +alternative and c) attachments + + +--Apple-Mail-2-1061547935 +Content-Type: multipart/related; + boundary=Apple-Mail-3-1061547936; + type="text/html" + + +--Apple-Mail-3-1061547936 +Content-Type: text/html; + charset=ISO-8859-4 +Content-Transfer-Encoding: quoted-printable + +This is a simple e-mail with a) = +special characters (=F6=E4=F5=FC), b) HTML alternative and c) = +attachments

= + +--Apple-Mail-3-1061547936 +Content-Disposition: inline; + filename=favourite.gif +Content-Transfer-Encoding: base64 +Content-Type: image/gif; + x-unix-mode=0755; + name="favourite.gif" +Content-Id: <4F26D18A-86BF-47CB-B58F-BFDCD36030FF@lan> + +R0lGODlhlgAXAOZ/AJmZmb+/v8uieP7ChrGxsZKSkva8gYSEhMrKytuELOexe/fo2NOnetnZ2dLS +0uzj2+np6YCAgOXl5aysrObYyf66df+ePaioqM6qhdGtiaKiotXV1dvb2/LZwbm5ufDw8NOzlP/6 +9ODg4PWTMf/u3OiscXl5eZWVlc57Kf/z5/Lp4MfHx4+Pj6CgoOPj4/+lSt3d3aWlpf/atPngyMHB +wdrApvn08LZuJfPu6X19fe6PMHd3d25uburMrry8vPWlVe2ZRbyCSfPt5+3i1uGSQsyHQbd5Pb54 +Muzs7Pj4+Pf395eXl/yXMu3t7dbW1viVMtWAK/3IksPDw//nz/2YM+7u7vDj1v/ixf/Onfbz7+yO +L/+rV9aEMvWZPf+9e9F9KsR2J8B3LeCVSf+nUP/17PahTf/Vq/GRMPeUMfPz8/DLpuzHop6entKh +cfTgy/Dp4/ju5PX19fn5+fLy8u/v7/T09Pv7+/b29vr6+vz8/P7+/v39/dHR0f+ZM////////yH5 +BAEAAH8ALAAAAACWABcAAAf/gH+Cg4SFhoeIiYV1HHyOj5CRko6Mk5aWlZeaj5mbmoqgoaKEG3R+ +p6ipqqt+fKWssKyuSHi1tre4uXizur26fKPBwoV8fnrHyMnKy3oOxczQy37OeEjW19jZ2njU2t7f +3MPiwc/R5sfOxufR03x2dPDx8vP0ds7v9Pn59oWsJCSsErGqUyfgID569ihcyFCEC4YQ9zhKGHHh +kCEVFepxlGeOx48gGzQASTIPR5IkKVBA+dFkv1VRohg8xAoGjJl/EGZUOIHAzj3OKGbMkGFnMz55 +CCpdWufCBKZL8zhLClUpBgxVCUp9qerFC5xcU61YgRNhnrNo05o4kLatSbNu/9EaKRL37EakcfLq +3bt2r9+3efz6nStYr0tCq6b06TNllUBVHwIE+OD4oJ66eRrwOCACc1DMFG4kcFP36J47qFOndrCZ +g2rVQPmcfp06dIIOtFEDDXtqwOIBlWmqaiC5QfCceuwoX768xQ4WHphLnyiduQAwWgxUV353dm42 +zz3kviNR9vg717OPl1iIDJYB8AdYWGwh/gAsZBLJESFSpBTJUvTXgAhyEILQck0QoMGCGqxVAAAE +XLAgAU0sR51yQoAgwIYCHAHFCF0owMCGIAjBnSN7KKEiBAoy6CCEMUwIgYrlpajiGxpy6CGIIpL4 +Bo3AICbDfIsVaaQFMvghEP8ENEjm5JMB0ACBkpbdIsUOOUQQwQEsLLEECwdsicAtE91SQxgoQAFF +AjowwYQOCUDBRQ+2dJfEnUnQgKWWXHoJpph41ohnEmemuWabb8Y5Z6BBDnIKCWMYaeQYAFEpXB0r +QOnkCgVZmtMecoQaqgsnsFDACUsAAMASJ2gAgaihOgMqrFaIocMITzCxGBNPlLEArLHNKiqppqKq +KquuAisrrHLUemuuu/b6q6i7IXZKCBVI2kcFIaDymB95bKDpBnl4e5CwoqYxwYOqqupBHczKgWK8 +WZQwAhVGVmADszXGqy677b7L77zM1ntvvvsC26ggqSgmaWPmCndKZFBSFvH/p4Pi2cABJ7QLgAgZ +34liyElQkAAaRs4Qcnl66LJxx+26kMtdLediMspFzjDzwn+kEoW2Ml0cFgya3nSxbComreIEJgAM +AA1KJy1r1CpigMIZRg5AdWzJVce001JUd1TX0mVwtZFRiO1AWFv85lsfW6TyrR8IFEdcAAjIfW5u +EeRQQAA+dBxDbijmFsQXIxhgwBNU/EAby9sd4Ldkgot9oXSHjzDA4o1bzlUKfbxwxSlXvNBHCkJb +64cSAazQxClNZKrE0d6lJgEPBTSAmhMAFAABbbLS9sANROB2RwdAjKDCa1y7BQHuMJzVQO9VtDW2 +W28QP8NZyI8Ah/Vrq26G4RfdohKCF2ak7ugpEjhhRyp2OCHB0YH55UELVexVxQQrCMaRYCBoAw72 +goMSrOEvE3GLDzQQh7TUgQAIsF4C21KDNtggLTZQgBok+DlYoO4U35odK0ToKaRARQRZkQBUpgKV +B2TFhVEJSkQgkJEmQGRsEVFBRuBww/CN44eJQApLhsiSkxDxiHN4i1B+khGaMfEnGwGiFA/hDn1Y +cR73uKIW7VGOdTCjHerwojR8OMUyOqEJvkijLRxwRjWqkY0Wi4Ucp+GEOM4RFmQsoxTnsAFPbIKP +ftQEIANpiUESUhKBAAA7 + +--Apple-Mail-3-1061547936-- + +--Apple-Mail-2-1061547935-- diff --git a/mailparser.js b/mailparser.js new file mode 100644 index 0000000..554d177 --- /dev/null +++ b/mailparser.js @@ -0,0 +1,568 @@ +var EventEmitter = require('events').EventEmitter, + sys = require('sys'), + mime = require("./mime"), + datetime = require("./datetime"); + +var PARSE_HEADERS = 1, + PARSE_BODY = 2; + +MailParser = function(mailFrom, rcptTo){ + EventEmitter.call(this); + this.mailFrom = mailFrom; + this.rcptTo = rcptTo; + this.headerStr = ""; + + this.waitFor = 0; + this.receivedAll = false; + + this.headers = {}; + this.bodyData = {bodyText:"", bodyHTML:"", bodyAlternate:[], attachments:[]}; + + this.state = PARSE_HEADERS; +} +sys.inherits(MailParser, EventEmitter); + +exports.MailParser = MailParser; + +MailParser.prototype.feed = function(data){ + data = data.replace(/\r\n\.\./g,"\r\n."); + if(this.state == PARSE_HEADERS){ + data = this.parseHeaders(data); + this.parseBodyStart(data); + }else if(this.state == PARSE_BODY){ + data = this.parseBody(data); + } +} + +MailParser.prototype.end = function(){ + if(this.headers.multipart && (this.waitFor || !this.receivedAll)) + return; + + if(this.state == PARSE_BODY){ + this.parseBodyEnd(); + } + + this.emit("end"); +} + +MailParser.prototype.parseHeaders = function(data){ + var pos, body = ""; + if((pos=data.indexOf("\r\n\r\n"))>=0){ + this.headerStr += data.substr(0, pos); + body = data.substr(pos+4); + this.headerObj = mime.parseHeaders(this.headerStr); + this.analyzeHeaders(this.headerObj, this.headers); + delete this.headerObj; // not needed anymore + delete this.headerStr; // just ditch it + this.emit("headers", this.headers); + this.state = PARSE_BODY; + }else + this.headerStr += data; + return body; +} + +MailParser.prototype.analyzeHeaders = function(headerObj, headers){ + var parts, headersUsed = []; + + // mime version + headersUsed.push("mime-version"); + headers.useMime = !!parseFloat(headerObj["mime-version"] && headerObj["mime-version"][0]); + + // content type + headersUsed.push("content-type"); + parts = {}; + if(headerObj["content-type"]){ + parts = mime.parseHeaderLine(headerObj["content-type"] && headerObj["content-type"][0]); + } + headers.contentType = parts.defaultValue && parts.defaultValue.toLowerCase() || "text/plain"; + + // charset + headers.charset = parts.charset || "us-ascii"; + + // format=fixed|flowed (RFC2646) + headers.format = parts.format && parts.format.toLowerCase() || "fixed"; + + // filename + headers.filename = parts.name && mime.parseMimeWords(parts.name.replace(/^[\s"']+|[\s"']+$/g,"")).trim() || false; + + // mime-boundary + headers.multipart = false; + headers.mimeBoundary = false; + if(headers.contentType.substr(0,"multipart/".length)=="multipart/"){ + headers.mimeBoundary = parts.boundary.replace(/^[\s"']+|[\s"']+$/g,"").trim(); + headers.multipart = true; + } + + // message ID + headersUsed.push("message-id"); + parts = {}; + if(headerObj["message-id"]){ + parts = mime.parseHeaderLine(headerObj["message-id"] && headerObj["message-id"][0]); + } + headers.messageId = (parts.defaultValue || "").replace(/^*$/, ''); + + // content ID + headersUsed.push("content-id"); + parts = {}; + if(headerObj["content-id"]){ + parts = mime.parseHeaderLine(headerObj["content-id"] && headerObj["content-id"][0]); + } + headers.contentId = (parts.defaultValue || "").replace(/^*$/, ''); + + // date + headersUsed.push("date"); + parts = {}; + if(headerObj["date"]){ + parts = mime.parseHeaderLine(headerObj["date"] && headerObj["date"][0]); + } + headers.messageDate = parts.defaultValue && datetime.strtotime(parts.defaultValue)*1000 || Date.now(); + + // content-transfer-encoding + headersUsed.push("content-transfer-encoding"); + parts = {}; + if(headerObj["content-transfer-encoding"]){ + parts = mime.parseHeaderLine(headerObj["content-transfer-encoding"] && headerObj["content-transfer-encoding"][0]); + } + headers.contentTransferEncoding = parts.defaultValue || "7bit"; + + // from + headersUsed.push("from"); + headers.addressesFrom = []; + if(headerObj["from"]){ + for(var i=0, len = headerObj["from"].length;i3) + headers.priority = 5; + if(nr<3) + headers.priority = 1; + }else if(headerObj["x-priority"] || headerObj["x-msmail-priority"]){ + switch((headerObj["x-priority"] || headerObj["x-msmail-priority"])[0].toLowerCase().trim()){ + case "low": + headers.priority = 5; + break; + case "normal": + headers.priority = 3; + break; + case "hight": + headers.priority = 1; + break; + } + }else if(headerObj["importance"]){ + switch(headerObj["importance"][0].toLowerCase().trim()){ + case "non-urgent": + headers.priority = 5; + break; + case "normal": + headers.priority = 3; + break; + case "urgent": + headers.priority = 1; + break; + } + } + + + + // content-disposition + headersUsed.push("content-disposition"); + parts = {}; + if(headerObj["content-disposition"]){ + parts = mime.parseHeaderLine(headerObj["content-disposition"] && headerObj["content-disposition"][0]); + } + headers.contentDisposition = parts.defaultValue || false; + + if(!headers.filename && parts.filename) + headers.filename = mime.parseMimeWords(parts.filename.replace(/^[\s"']+|[\s"']+$/g,"")).trim(); + + headers.secondary = []; + var keys = Object.keys(headerObj); + for(var i=0, len=keys.length; i=0){ + this.body.headerStr += data.substring(pos, pos2); + pos = pos2+4; + this.body.headerStrComplete = true; + this.body.headerObj = mime.parseHeaders(this.body.headerStr.trim()); + + this.body.headers = {}; + this.analyzeHeaders(this.body.headerObj, this.body.headers); + + this.waitFor++; + + // TEXT + if(this.body.headers.contentType.substr(0,"text/".length)=="text/"){ + this.body.ds = new DataStore("text", this.body.headers.contentTransferEncoding, this.body.headers.charset); + this.setUpDSCallback(this.body.headers); + } + + // MULTIPART + else if(this.body.headers.contentType.substr(0,"multipart/".length)=="multipart/"){ + this.body.ds = new MailParser(); + this.setUpMPCallback(this.body.headers); + this.body.ds.feed(this.body.headerStr.trim()+"\r\n\r\n"); + } + + // BINARY + else{ + this.body.ds = new DataStore("binary", this.body.headers.contentTransferEncoding, this.body.headers.charset); + this.setUpDSCallback(this.body.headers); + } + + + this.body.headerStr = ""; + }else{ + this.body.headerStr += data.substr(pos); + break; + } + } + + if(this.body.ds){ + pos3 = data.indexOf("--"+this.headers.mimeBoundary, pos); + this.body.ds.feed(pos3>=0?data.substring(pos,pos3):data.substr(pos)); + if(pos3>=0){ + this.body.ds.end(); + this.body.ds = null; + } + } + + } + + pos = pos3!==false?pos3:data.indexOf("--"+this.headers.mimeBoundary, pos); + if(pos>=0){ + pos += ("--"+this.headers.mimeBoundary).length; + this.body.headerStrComplete = false; + this.body.headerStr = ""; + this.body.mimeContents = true; + if(data.substr(pos,2)=="--"){ + // last boundary + this.receivedAll = true; + return; + } + } + }while(pos>=0); + }else{ + this.bodyData.bodyText += data; + } +} + +MailParser.prototype.setUpDSCallback = function(headers){ + this.body.ds.on("astream", (function(id, data){ + this.emit("astream",id, data); + }).bind(this)); + + this.body.ds.on("end", (function(data){ + var done = false; + if(!headers.contentDisposition){ + // body + switch(headers.contentType){ + case "text/plain": + if(!this.bodyData.bodyText){ + this.bodyData.bodyText = data; + done = true; + } + break; + case "text/html": + if(!this.bodyData.bodyHTML){ + this.bodyData.bodyHTML = data; + done = true; + } + break; + } + if(!done) + this.bodyData.bodyAlternate.push({ + contentType: headers.contentType, + data: data + }); + }else{ + // attachments + this.bodyData.attachments.push({ + contentType: headers.contentType, + contentDisposition: headers.contentDisposition, + contentId: headers.contentId, + filename: headers.filename, + data: data + }); + this.emit("aend",data.id); + } + + if(!(--this.waitFor)){ + this.end(); + } + }).bind(this)); +} + + +MailParser.prototype.setUpMPCallback = function(headers){ + this.body.ds.on("astream", (function(id, data){ + this.emit("astream",id, data); + }).bind(this)); + + this.body.ds.on("aend", (function(id){ + this.emit("aend",id); + }).bind(this)); + + this.body.ds.on("headers", (function(data){}).bind(this)); + this.body.ds.on("body", (function(data){ + + this.bodyData.attachments.push({ + contentType: headers.contentType, + body: data + }); + + if(!(--this.waitFor)){ + this.end(); + } + }).bind(this)); +} + +MailParser.prototype.parseBodyEnd = function(){ + + if(!this.headers.multipart && this.bodyData.bodyText){ + switch(this.headers.contentTransferEncoding.toLowerCase()){ + case "quoted-printable": + this.bodyData.bodyText = mime.decodeQuotedPrintable(this.bodyData.bodyText, false, this.headers.charset); + break; + case "base64": + this.bodyData.bodyText = mime.decodeBase64(this.bodyData.bodyText, false, this.headers.charset); + break; + } + this.bodyData.bodyText = this.bodyData.bodyText.trim(); + if(this.headers.contentType=="text/html"){ + this.bodyData.bodyHTML = this.bodyData.bodyText; + this.bodyData.bodyText = false; + } + } + + if(this.bodyData.bodyText && !!this.bodyData.bodyHTML) + this.bodyData.bodyText = stripHTML(this.bodyData.bodyText); + + this.emit("body",this.bodyData); + return false; +} + + + +// DataStore - load text into memory, put binary to Mongo GridStore +// return a) text - fulltext, b) binary - key +function DataStore(type, encoding, charset){ + EventEmitter.call(this); + this.type = type || "text"; + this.encoding = encoding || "7bit"; + this.charset = charset || "us-ascii"; + this.data = ""; + + this.id = generateAttachmentId(); + + this.stream = false; + if(this.type=="binary"){ + this.data = 0; + // FIX: assumes that binary is always base64! + this.stream = new Base64Stream(); + this.stream.on("stream", this.onStream.bind(this)); + this.stream.on("end", this.onStreamEnd.bind(this)); + } +} +sys.inherits(DataStore, EventEmitter); + +DataStore.prototype.feed = function(data){ + if(this.type=="text")this.feedText(data); + if(this.type=="binary")this.feedBinary(data); +} + +DataStore.prototype.feedText = function(data){ + this.data += data; +} + +DataStore.prototype.feedBinary = function(data){ + this.stream.feed(data); +} + +DataStore.prototype.onStream = function(buffer){ + this.emit("astream", this.id, buffer) +} + +DataStore.prototype.onStreamEnd = function(){ + this.emit("end", {id:this.id, body:null}); +} + +DataStore.prototype.end = function(){ + if(this.type=="text"){ + if(this.encoding=="quoted-printable") + this.data = mime.decodeQuotedPrintable(this.data, false, this.charset).trim(); + if(this.encoding=="base64") + this.data = mime.decodeBase64(this.data, this.charset).trim(); + + this.emit("end", {id:this.id, body:this.data}); + + }else{ + this.stream.end(); + } +} + + +// base64 stream decoder +function Base64Stream(){ + EventEmitter.call(this); + this.current = ""; +} +sys.inherits(Base64Stream, EventEmitter); + +Base64Stream.prototype.feed = function(data){ + var remainder = 0; + this.current += data.replace(/[^\w+\/=]/g,''); + this.emit("stream", new Buffer(this.current.substr(0, this.current.length - this.current.length % 4),"base64")); + this.current = (remainder=this.current.length % 4)?this.current.substr(- remainder):""; +} + +Base64Stream.prototype.end = function(){ + if(this.current.length) + this.emit("stream", new Buffer(this.current,"base64")); + this.emit("end"); +} + + +var attachment_id_counter = 0; +function generateAttachmentId(){ + return "#"+Date.now()+"-"+(++attachment_id_counter); +} + +function stripHTML(str){ + str = str.replace(/\r?\n/g," "); + str = str.replace(/<(?:\/p|br|\/tr|\/table|\/div)>/g,"\n"); + + // hide newlines with two 00 chars (enables multiline matches) + str = str.replace(/\r?\n/g,"-\u0000\u0000-"); + + // H1-H6, add underline + str = str.replace(/<[hH]\d[^>]*>(.*?)<\/[hH]\d[^>]*>/g,function(a,b){ + var line = ""; + b = b.replace(/<[^>]*>/g," "); + b = b.replace(/\s\s+/g," "); + b = b.trim(); + + if(!b) + return ""; + for(var i=0, len = b.length; i]*>(.*?)<\/?(?:li|ol|ul)[^>]*>/ig,function(a,b){ + b = b.replace(/<[^>]*>/g," "); + b = b.replace(/\s\s+/g," "); + b = b.trim(); + + if(!b) + return ""; + return "-®®®®-* "+b+"\n"; + }); + + // PRE, indent by 4 spaces + str = str.replace(/]*>(.*?)<\/pre[^>]*>/ig,function(a,b){ + b = b.replace(/<[^>]*>/g," "); + b = b.replace(/\s\s+/g," "); + b = b.trim(); + + if(!b) + return ""; + + b = b.replace(/[ \t]*\n[ \t]*/g,"\n-®®®®--®®®®-"); + + return "\n-®®®®--®®®®-"+b.trim()+"\n\n"; + }); + + // restore + str = str.replace(/\s*-\u0000\u0000-\s*/g,"\n"); + + // remove all remaining html tags + str = str.replace(/<[^>]*>/g," "); + // remove duplicate spaces + str = str.replace(/[ ][ ]+/g," "); + // remove spaces before and after newlines + str = str.replace(/[ \t]*\n[ \t]*/g,"\n"); + // remove more than 2 newlines in a row + str = str.replace(/\n\n+/g,"\n\n"); + // restore hidden spaces (four (r) signs for two spaces) + str = str.replace(/-®®®®-/g," "); + return str.trim(); +} + diff --git a/mime.js b/mime.js new file mode 100644 index 0000000..5690a17 --- /dev/null +++ b/mime.js @@ -0,0 +1,423 @@ + +// see http://github.com/bnoordhuis/node-iconv for more info +var Iconv = require("../node-iconv/build/default/iconv").Iconv; + +/* mime related functions - encoding/decoding etc*/ +/* TODO: Only UTF-8 and Latin1 are allowed with encodeQuotedPrintable */ +/* TODO: Check if the input string even needs encoding */ + +/** + * mime.foldLine(str, maxLength, foldAnywhere) -> String + * - str (String): mime string that might need folding + * - maxLength (Number): max length for a line, defaults to 78 + * - foldAnywhere (Boolean): can fold at any location (ie. in base64) + * - afterSpace (Boolean): If [true] fold after the space + * + * Folds a long line according to the RFC 5322 + * + * + * For example: + * Content-Type: multipart/alternative; boundary="----bd_n3-lunchhour1283962663300----" + * will become + * Content-Type: multipart/alternative; + * boundary="----bd_n3-lunchhour1283962663300----" + * + **/ +this.foldLine = function(str, maxLength, foldAnywhere, afterSpace){ + var line=false, curpos=0, response="", lf; + maxLength = maxLength || 78; + + // return original if no need to fold + if(str.length<=maxLength) + return str; + + // read in bytes and try to fold it + while(line = str.substr(curpos, maxLength)){ + if(!!foldAnywhere){ + response += line; + if(curpos+maxLength=maxLength && lf>0){ + if(!!afterSpace){ + // move forward until line end or no more \s and \t + while(lf String + * - str (String): String to be encoded + * - encoding (String): Encoding Q for quoted printable or B (def.) for base64 + * - charset (String): Charset to be used + * + * Encodes a string into mime encoded word format + * + * + * For example: + * See on õhin test + * Becomes with UTF-8 and Quoted-printable encoding + * =?UTF-8?q?See_on_=C3=B5hin_test?= + * + **/ +this.encodeMimeWord = function(str, encoding, charset){ + charset = charset || "UTF-8"; + encoding = encoding && encoding.toUpperCase() || "B"; + + if(encoding=="Q"){ + str = this.encodeQuotedPrintable(str, true, charset); + } + + if(encoding=="B"){ + str = this.encodeBase64(str); + } + + return "=?"+charset+"?"+encoding+"?"+str+"?="; +} + +/** + * mime.decodeMimeWord(str, encoding, charset) -> String + * - str (String): String to be encoded + * - encoding (String): Encoding Q for quoted printable or B (def.) for base64 + * - charset (String): Charset to be used, defaults to UTF-8 + * + * Decodes a string from mime encoded word format, see [[encodeMimeWord]] + * + **/ + +this.decodeMimeWord = function(str){ + var parts = str.split("?"), + charset = parts && parts[1], + encoding = parts && parts[2], + text = parts && parts[3]; + if(!charset || !encoding || !text) + return str; + if(encoding.toUpperCase()=="Q"){ + return this.decodeQuotedPrintable(text, true, charset); + } + + if(encoding.toUpperCase()=="B"){ + return this.decodeBase64(text); + } + + return text; +} + + +/** + * mime.encodeQuotedPrintable(str, mimeWord, charset) -> String + * - str (String): String to be encoded into Quoted-printable + * - mimeWord (Boolean): Use mime-word mode (defaults to false) + * - charset (String): Destination charset, defaults to UTF-8 + * TODO: Currently only allowed charsets: UTF-8, LATIN1 + * + * Encodes a string into Quoted-printable format. + **/ +this.encodeQuotedPrintable = function(str, mimeWord, charset){ + charset = charset || "UTF-8"; + + /* + * Characters from 33-126 OK (except for =; and ?_ when in mime word mode) + * Spaces + tabs OK (except for line beginnings and endings) + * \n + \r OK + */ + + str = str.replace(/[^\sa-zA-Z\d]/gm,function(c){ + if(!!mimeWord){ + if(c=="?")return "=3F"; + if(c=="_")return "=5F"; + } + if(c!=="=" && c.charCodeAt(0)>=33 && c.charCodeAt(0)<=126) + return c; + return c=="="?"=3D":(charset=="UTF-8"?encodeURIComponent(c):escape(c)).replace(/%/g,'='); + }); + + str = lineEdges(str); + + if(!mimeWord){ + // lines might not be longer than 76 bytes, soft break: "=\r\n" + var lines = str.split(/\r?\n/); + for(var i=0, len = lines.length; i76){ + lines[i] = this.foldLine(lines[i],76, false, true).replace(/\r\n/g,"=\r\n"); + } + } + str = lines.join("\r\n"); + }else{ + str = str.replace(/\s/g, function(a){ + if(a==" ")return "_"; + if(a=="\t")return "=09"; + return a=="\r"?"=0D":"=0A"; + }); + } + + return str; +} + +/** + * mime.deccodeQuotedPrintable(str, mimeWord, charset) -> String + * - str (String): String to be decoded + * - mimeWord (Boolean): Use mime-word mode (defaults to false) + * - charset (String): Charset to be used, defaults to UTF-8 + * + * Decodes a string from Quoted-printable format. + **/ +this.decodeQuotedPrintable = function(str, mimeWord, charset){ + charset = charset && charset.toUpperCase() || "UTF-8"; + + if(mimeWord){ + str = str.replace(/_/g," "); + }else{ + str = str.replace(/=\r\n/gm,''); + str = str.replace(/=$/,""); + } + if(charset == "UTF-8") + str = decodeURIComponent(str.replace(/=/g,"%")); + else{ + str = str.replace(/=/g,"%"); + if(charset=="ISO-8859-1" || charset=="LATIN1") + str = unescape(str); + else{ + str = decodeBytestreamUrlencoding(str); + str = fromCharset(charset, str); + } + } + return str; +} + +/** + * mime.encodeBase64(str) -> String + * - str (String): String to be encoded into Base64 + * - charset (String): Destination charset, defaults to UTF-8 + * + * Encodes a string into Base64 format. Base64 is mime-word safe. + **/ +this.encodeBase64 = function(str, charset){ + var buffer; + if(charset && charset.toUpperCase()!="UTF-8") + buffer = toCharset(charset, str); + else + buffer = new Buffer(str, "UTF-8"); + return buffer.toString("base64"); +} + +/** + * mime.decodeBase64(str) -> String + * - str (String): String to be decoded from Base64 + * - charset (String): Source charset, defaults to UTF-8 + * + * Decodes a string from Base64 format. Base64 is mime-word safe. + * NB! Always returns UTF-8 + **/ +this.decodeBase64 = function(str, charset){ + var buffer = new Buffer(str, "base64"); + + if(charset && charset.toUpperCase()!="UTF-8"){ + return fromCharset(charset, buffer); + } + + // defaults to utf-8 + return buffer.toString("UTF-8"); +} + +/** + * mime.parseHeaders(headers) -> Array + * - headers (String): header section of the e-mail + * + * Parses header lines into an array of objects (see [[parseHeaderLine]]) + * FIXME: This should probably not be here but in "envelope" instead + **/ +this.parseHeaders = function(headers){ + var text, lines, line, i, name, value, cmd, header_lines = {}; + // unfold + headers = headers.replace(/\r?\n([ \t])/gm," "); + + // split lines + lines = headers.split(/\r?\n/); + for(i=0; i Array + * - addresses (String): string with comma separated e-mail addresses + * + * Parses names and addresses from a from, to, cc or bcc line + **/ +this.parseAddresses = function(addresses){ + if(!addresses) + return {}; + + addresses = addresses.replace(/=\?[^?]+\?[QqBb]\?[^?]+\?=/g, (function(a){return this.decodeMimeWord(a)}).bind(this)); + + // not sure if it's even needed - urlencode escaped \\ and \" and \' + addresses = addresses.replace(/\\\\/g,function(a){return escape(a.charAt(1))}); + addresses = addresses.replace(/\\["']/g,function(a){return escape(a.charAt(1))}); + + var list = addresses.split(","), address, addressArr = [], name, email; + for(var i=0, len=list.length; i()]/)){ // address with comments (name) + email = false; + address = address.replace(/<([^>]+)>/,function(a,b){ + email = b.indexOf("@")>=0 && b; + return email?"":a; + }); + address = address.trim(); + if(email){ + name = address.replace(/"/g,"").trim(); + }else{ // try brackets + address = address.replace(/\(([^)]+)\)/,function(a,b){ + name = b; + return ""; + }); + email = address.indexOf("@")>=0 && address.trim(); + } + // just in case something got mixed up + if(!email && name.indexOf("@")>=0){ + email = name; + name = false; + } + if(email) + addressArr.push({address:decodeURIComponent(email), name: decodeURIComponent(name)}); + }else if(address.indexOf("@")>=0) + addressArr.push({address:address, name:false}); + } + return addressArr; +} + +/** + * mime.parseMimeWords(str) -> String + * - str (String): string to be parsed + * + * Parses mime-words into UTF-8 strings + **/ +this.parseMimeWords = function(str){ + return str.replace(/=\?[^?]+\?[QqBb]\?[^?]+\?=/g, (function(a){ + return this.decodeMimeWord(a); + }).bind(this)); +} + +/** + * mime.parseHeaderLine(line) -> Object + * - line (String): a line from a message headers + * + * Parses a header line to search for additional parameters. + * For example with "text/plain; charset=utf-8" the output would be + * - defaultValue = text/plain + * - charset = utf-8 + **/ +this.parseHeaderLine = function(line){ + if(!line) + return {}; + var result = {}, parts = line.split(";"), pos; + for(var i=0, len = parts.length; i String + * - str (String): String to be processed + * + * Replaces all spaces and tabs in the beginning and end of the string + * with quoted printable encoded chars. Needed by [[encodeQuotedPrintable]] + **/ +function lineEdges(str){ + str = str.replace(/^[ \t]+/gm, function(wsc){ + return wsc.replace(/ /g,"=20").replace(/\t/g,"=09"); + }); + + str = str.replace(/[ \t]+$/gm, function(wsc){ + return wsc.replace(/ /g,"=20").replace(/\t/g,"=09"); + }); + return str; +} + +/** + * fromCharset(charset, buffer, keep_buffer) -> String | Buffer + * - charset (String): Source charset + * - buffer (Buffer): Buffer in + * - keep_buffer (Boolean): If true, return buffer, otherwise UTF-8 string + * + * Converts a buffer in codepage into UTF-8 string + **/ +function fromCharset(charset, buffer, keep_buffer){ + var iconv = new Iconv(charset,'UTF-8'), + buffer = iconv.convert(buffer); + return keep_buffer?buffer:buffer.toString("utf-8"); +} + +/** + * toCharset(charset, buffer) -> Buffer + * - charset (String): Source charset + * - buffer (Buffer): Buffer in UTF-8 or string + * + * Converts a string or buffer to codepage + **/ +function toCharset(charset, buffer){ + var iconv = new Iconv('UTF-8',charset); + return iconv.convert(buffer); +} + +/** + * decodeBytestreamUrlencoding(encoded_string) -> Buffer + * - encoded_string (String): String in urlencode coding + * + * Converts an urlencoded string into a bytestream buffer. If the used + * charset is known the resulting string can be converted to UTF-8 with + * [[fromCharset]]. + * NB! For UTF-8 use decodeURIComponent and for Latin 1 decodeURL instead + **/ +function decodeBytestreamUrlencoding(encoded_string){ + var c, i, j=0, prcnts = encoded_string.match(/%/g) || "", + buffer_length = encoded_string.length - (prcnts.length*2), + buffer = new Buffer(buffer_length); + + for(var i=0; i