Intial Commit

This commit is contained in:
valki
2020-10-17 18:42:50 +02:00
commit 664c6d8ca3
5892 changed files with 759183 additions and 0 deletions

View File

@@ -0,0 +1,153 @@
EUROOPA LIIDU TARKVARA VABA KASUTUSE LITSENTS v. 1.2
EUPL © Euroopa Liit 2007, 2016
Euroopa Liidu tarkvara vaba kasutuse litsents („EUPL“) kehtib allpool määratletud teose suhtes, mida levitatakse vastavalt käesoleva litsentsi tingimustele. Teost on keelatud kasutada muul viisil kui vastavalt käesoleva litsentsi tingimustele niivõrd, kuivõrd sellise kasutamise suhtes kehtivad teose autoriõiguse omaja õigused).
Teost levitatakse vastavalt käesoleva litsentsi tingimustele, kui allpool määratletud litsentsiandja paneb vahetult teose autoriõiguse märke järele järgmise märke:
Litsentsitud EUPL alusel
või on muul viisil väljendanud soovi litsentsida originaalteos EUPL alusel.
1. Mõisted
Käesolevas litsentsis kasutatakse järgmisi mõisteid:
— „litsents“ käesolev litsents;
— „originaalteos“ teos või tarkvara, mida litsentsiandja käesoleva litsentsi alusel levitab või edastab ning mis on kättesaadav lähtekoodina ja teatavatel juhtudel ka täitmiskoodina;
— „tuletatud teos“ teos või tarkvara, mida litsentsisaajal on olnud võimalik luua tuginedes originaalteosele või selle muudetud versioonile. Käesolevas litsentsis ei määratleta muudatuse ulatust või sõltuvust originaalteosest, mis on vajalik selleks, et klassifitseerida teos tuletatud teoseks; selline ulatus määratakse kindlaks vastavalt artiklis 15 nimetatud riigis kehtivatele autoriõigust käsitlevatele õigusaktidele;
— „teos“ originaalteos või sellest tuletatud teosed;
— „lähtekood“ teose inimloetav vorm, mida inimestel on kõige lihtsam uurida ja muuta;
— „täitmiskood“ igasugune kood, mille on tavaliselt koostanud arvuti ja mis on mõeldud arvuti poolt programmina tõlgendamiseks;
— „litsentsiandja“ füüsiline või juriidiline isik, kes levitab või edastab teost litsentsi alusel;
— „edasiarendaja“ füüsiline või juriidiline isik, kes muudab teost litsentsi alusel või osaleb muul moel tuletatud teose loomises;
— „litsentsisaaja“ või „Teie“ iga füüsiline või juriidiline isik, kes kasutab teost ükskõik mis moel vastavalt litsentsi tingimustele;
— „levitamine“ või „edastamine“ teose koopiate müümine, teisele isikule andmine, laenutamine, rentimine, levitamine, edastamine, ülekandmine või teose koopiate või selle oluliste funktsioonide muul viisil teistele füüsilistele või juriidilistele isikutele kättesaadavaks tegemine võrgus või võrguväliselt.
2. Litsentsiga antavate õiguste ulatus
Käesolevaga annab litsentsiandja Teile ülemaailmse, tasuta, all-litsentsi andmise õigusega lihtlitsentsi, mille alusel võite teha originaalteose autoriõiguse kehtivusaja jooksul järgmist:
— kasutada teost mis tahes eesmärgil ja mis tahes viisil,
— teost reprodutseerida,
— teost muuta ja luua teosel põhinevaid tuletatud teoseid,
— teost või selle koopiaid üldsusele edastada, sealhulgas neid kättesaadavaks teha või eksponeerida, samuti avalikult esitada,
— teost või selle koopiaid levitada,
— teost või selle koopiaid laenutada ja rentida,
— anda all-litsentse teose või selle koopiate suhtes kehtivate õiguste kohta.
Neid õigusi võib teostada mistahes praegu tuntud või hiljem leiutatud keskkonnas, toel või formaadis, ja sellises ulatuses, nagu lubab kohaldatav õigus.
Riikides, kus kehtivad isiklikud õigused, loobub litsentsiandja seadusega lubatud ulatuses oma õigusest teostada isiklikke õigusi, et oleks võimalik eespool loetletud varalisi õigusi litsentsida.
Litsentsiandja annab litsentsisaajale tasuta lihtlitsentsi kõigi litsentsiandjale kuuluvate patentide kasutamiseks ulatuses, mis on vajalik teose suhtes käesoleva litsentsiga antud õiguste kasutamiseks.
3. Lähtekoodi edastamine
Litsentsiandja võib teose kättesaadavaks teha kas lähtekoodi või täitmiskoodi kujul. Kui teos tehakse kättesaadavaks täitmiskoodi kujul, lisab litsentsiandja igale tema poolt levitatavale teose koopiale lähtekoodi masinloetava koopia või näitab teose autoriõiguse märke järele lisatud märkega ära hoidla, kust lähtekood on kergesti ja tasuta kättesaadav seni, kuni litsentsiandja jätkab teose levitamist või edastamist.
4. Autoriõiguse piiramine
Käesolev litsents ei võta litsentsisaajalt võimalusi, mis tulenevad teose õiguste omaja ainuõiguste suhtes kehtestatud eranditest või ainuõiguste piiramisest, nende õiguste ammendumisest või muudest nende õiguste suhtes kohaldatavatest piirangutest.
5. Litsentsisaaja kohustused
Eespool nimetatud õigused antakse litsentsisaajale tingimusel, et ta peab kinni teatavatest piirangutest ja täidab teatavaid talle pandud kohustusi. Need kohustused on järgmised:
Õigus autorsusele. Litsentsisaaja hoiab puutumatuna kõik autoriõiguse, patentide ja kaubamärkide kohta käivad märked ja kõik märked, mis viitavad litsentsile ja garantii puudumisele. Litsentsisaaja peab teose iga tema poolt levitatavale või edastatavale koopiale lisama nimetatud märgete koopiad ja litsentsi koopia. Litsentsisaaja peab tagama, et iga tuletatud teos kannab selget märget selle kohta, et teost on muudetud ja muutmise kuupäeva.
Klausel vaba kasutamise tagamise kohta (copyleft). Kui litsentsisaaja levitab või edastab originaalteose või tuletatud teoste koopiaid, toimub see levitamine või edastamine vastavalt käesoleva litsentsi tingimustele või käesoleva litsentsi hilisema versiooni tingimustele, välja arvatud juhul, kui originaalteost levitatakse ainult vastavalt litsentsi käesolevale versioonile (näiteks märkides „ainult EUPL v.1.2“). Litsentsisaaja ei tohi (litsentsiandjaks saades) teosele või tuletatud teosele lisada ega kehtestada mingeid lisatingimusi, mis muudavad või piiravad litsentsi tingimusi.
Ühilduvuse klausel. Kui litsentsisaaja levitab või edastab tuletatud teoseid või nende koopiaid, mis põhinevad nii teosel kui ka teisel, käesoleva litsentsiga ühilduva litsentsi alusel litsentsitud teosel, võib see levitamine või edastamine toimuda vastavalt nimetatud ühilduva litsentsi tingimustele. Käesolevas klauslis tähendab „ühilduv litsents“ käesoleva litsentsi lisas loetletud litsentse. Kui litsentsisaaja ühilduvale litsentsile vastavad kohustused on vastuolus tema kohustustega vastavalt käesolevale litsentsile, loetakse kehtivaks ühilduva litsentsi kohased kohustused.
Lähtekoodi lisamine. Teose koopiate levitamisel või edastamisel lisab litsentsisaaja igale koopiale lähtekoodi masinloetava koopia või näitab ära hoidla, kust lähtekood on kergesti ja tasuta kättesaadav seni, kuni litsentsisaaja jätkab teose levitamist või edastamist.
Õiguskaitse. Käesoleva litsentsiga ei anta luba kasutada litsentsiandja ärinimesid, kaubamärke, teenindusmärke või nimesid, välja arvatud juhul, kui see on vajalik mõistlikuks ja tavapäraseks kasutamiseks teose päritolu kirjeldamisel ja autoriõiguse märke sisu reprodutseerimisel.
6. Autorsuse ahel
Esialgne litsentsiandja tagab, et käesoleva litsentsiga antav autoriõigus originaalteosele kuulub talle või on talle litsentsitud ja et tal on õigus seda litsentsiga edasi anda.
Iga edasiarendaja tagab, et autoriõigus tema poolt teosele tehtavatele muudatustele kuulub talle või on talle litsentsitud ja et tal on õigus litsentsi anda.
Iga kord, kui Te võtate vastu litsentsi, annavad esialgne litsentsiandja ja hilisemad edasiarendajad Teile litsentsi nende osaluse kasutamiseks teoses vastavalt käesoleva litsentsi tingimustele.
7. Garantii puudumine
Teos on veel pooleli ja arvukad edasiarendajad parendavad seda järjepidevalt. See ei ole lõpetatud teos ja võib seetõttu sisaldada defekte ja programmivigu, mis on omased seda liiki arendustegevusele.
Seetõttu levitatakse teost litsentsi alusel „sellisena, nagu see on“ ilma teose suhtes kehtiva garantiita, muu hulgas garantiita kaubandusliku kvaliteedi kohta, garantiita sobivuse kohta mingi kindla eesmärgi jaoks, garantiita defektide ja vigade puudumise kohta, garantiita täpsuse kohta ja selle kohta, et ei ole rikutud muid intellektuaalse omandi õigusi peale käesoleva litsentsi artiklis 6 nimetatud autoriõiguse.
Käesolev garantii puudumise klausel on litsentsi oluline osa ja teosele õiguste andmise eeltingimus.
8. Vastutuse välistamine
Välja arvatud tahtliku õiguserikkumise või füüsilistele isikutele tekitatud otsese kahju puhul, ei vastuta litsentsiandja mitte mingil juhul litsentsi või teose kasutamise tagajärjel tekkinud mistahes otsese või kaudse, varalise või moraalse kahju eest, muu hulgas maineväärtuse langusest tekkinud kahju, tööseisakute, arvutirikke ja talitlushäirete, andmete kadumise ja ärikahju eest, isegi juhul kui litsentsiandjat on sellise kahju tekkimise võimalikkusest teavitatud. Litsentsiandja vastutab siiski vastavalt tootevastutust käsitlevatele õigusaktidele niivõrd, kuivõrd need õigusaktid on teose suhtes kohaldatavad.
9. Lisakokkulepped
Teose levitamisel võite Te sõlmida lisakokkuleppe, milles määratakse kindlaks käesoleva litsentsiga vastavuses olevad kohustused või teenused. Kuid kui olete nõus võtma kohustusi, võite Te tegutseda ainult iseenda nimel ja vastutusel, mitte esialgse litsentsiandja või teiste edasiarendajate nimel, ja ainult juhul, kui Te nõustute vabastama edasiarendajad vastutusest, kaitsma ja hoidma neid kahju tekkimise eest vastutuse või nõuete osas, mida nende vastu võidakse esitada selle tagajärjel, et Teie pakute garantiid või võtate lisavastutuse.
10. Litsentsiga nõustumine
Käesoleva litsentsi sätetega saab nõustuda, klõpsates ikoonile „Nõustun“ litsentsi teksti näitava akna all või väljendades nõusolekut muul sarnasel viisil vastavalt kehtivatele õigusaktidele. Sellele ikoonile klõpsates väljendate selgelt ja pöördumatult oma nõusolekut käesoleva litsentsi ja kõigi selle tingimuste suhtes.
Samuti nõustute pöördumatult käesoleva litsentsi ja kõigi selle tingimustega, kui teostate Teile käesoleva litsentsi artikliga 2 antud õigusi, näiteks kasutate teost, loote tuletatud teose või levitate või edastate teost või selle koopiaid.
11. Üldsuse teavitamine
Juhul, kui Te kasutate teose levitamiseks või edastamiseks elektroonilisi sidevahendeid (näiteks võimaldate teose allalaadimist veebisaidilt), tuleb levitamiskanalil või andmekandjal (näiteks veebisaidil) teha üldsusele kättesaadavaks vähemalt kohaldatava õiguse alusel kohustuslik teave litsentsiandja ja litsentsi kohta ning selle kohta, kuidas see on litsentsisaajale kättesaadav, litsentsilepingu sõlmimise kohta ja selle kohta, kuidas litsentsisaaja saab litsentsi säilitada ja reprodutseerida.
12. Litsentsi lõppemine
Litsents ja sellega antud õigused lõppevad automaatselt juhul, kui litsentsisaaja rikub litsentsi tingimusi.
Sellise lõppemise korral ei lõpe ühegi sellise isiku litsents, kes sai teose litsentsisaajalt vastavalt litsentsi tingimustele, juhul kui see isik täidab jätkuvalt litsentsi tingimusi.
13. Muud sätted
Ilma et see piiraks litsentsi artikli 9 kohaldamist, sisaldab litsents kogu kokkulepet, mis osapoolte vahel seose teosega on.
Kui mõni litsentsi säte on vastavalt kohaldatavatele õigusaktidele kehtetu või seda ei ole võimalik jõustada, ei mõjuta see litsentsi kui terviku kehtivust ega jõustatavust. Sellist sätet tõlgendatakse või muudetakse nii nagu vaja, et see kehtiks ja saaks tagada selle täitmise.
Euroopa Komisjon võib avaldada käesoleva litsentsi muid keeleversioone või uusi versioone või lisa uuendatud versioone, kuivõrd see on vajalik ja mõistlik, vähendamata sellega litsentsiga antavate õiguste ulatust. Litsentsi uued versioonid avaldatakse kordumatu versiooninumbriga.
Käesoleva litsentsi kõik keeleversioonid, mille Euroopa Komisjon on heaks kiitnud, on võrdväärsed. Osapooled võivad kasutada enda valitud keeleversiooni.
14. Kohtualluvus
Ilma et see piiraks konkreetse osapooltevahelise kokkuleppe kohaldamist,
— kuuluvad käesoleva litsentsi tõlgendamisest tulenevad kohtuvaidlused Euroopa Liidu institutsioonide, organite, büroode või asutuste kui litsentsiandja ja mistahes litsentsisaaja vahel Euroopa Liidu Kohtu pädevusse, nagu on sätestatud Euroopa Liidu toimimise lepingu artiklis 272,
— kuuluvad käesoleva litsentsi tõlgendamisest tulenevad kohtuvaidlused muude osapoolte vahel selle pädeva kohtu ainupädevusse, kelle tööpiirkonnas asub litsentsiandja elukoht või peamine tegevuskoht.
15. Kohaldatav õigus
Ilma et see piiraks konkreetse osapooltevahelise kokkuleppe kohaldamist,
— kohaldatakse käesoleva litsentsi suhtes selle Euroopa Liidu liikmesriigi õigust, kus paikneb litsentsiandja peakorter, elukoht või asukoht,
— kohaldatakse litsentsi suhtes Belgia õigust, kui litsentsiandja peakorter, elukoht või asukoht asub väljaspool Euroopa Liidu liikmesriike.
Liide
Euroopa Liidu tarkvara vaba kasutuse litsentsi artiklis 5 osutatud „ühilduvad litsentsid“ on järgmised:
— GNU General Public License (GPL) v. 2, v. 3
— GNU Affero General Public License (AGPL) v. 3
— Open Software License (OSL) v. 2.1, v. 3.0
— Eclipse Public License (EPL) v. 1.0
— CeCILL v. 2.0, v. 2.1
— Mozilla Public Licence (MPL) v. 2
— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) muude teoste puhul peale tarkvara
— European Union Public Licence (EUPL) v. 1.1, v. 1.2
— Québec Free and Open-Source Licence Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+)
Euroopa Komisjon võib käesolevat lisa ajakohastada loetletud litsentside hilisemate versioonidega ilma, et peaks selleks koostama EUPLi uue versiooni, eeldusel et nende litsentsidega tagatakse käesoleva litsentsi artiklis 2 sätestatud õigused ja kaitstakse hõlmatud lähtekoodi eksklusiivse omastamise eest.
Kõigi muude käesoleva liite muudatuste või täienduste jaoks on vaja koostada EUPLi uus versioon.

16
nodered/rootfs/data/node_modules/mailsplit/LICENSE.MIT generated vendored Normal file
View File

@@ -0,0 +1,16 @@
Copyright (c) 2011-2019 Andris Reinman
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 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 THE
AUTHORS OR COPYRIGHT HOLDERS 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.

211
nodered/rootfs/data/node_modules/mailsplit/README.md generated vendored Normal file
View File

@@ -0,0 +1,211 @@
# mailsplit
Split an email message stream into structured parts and join these parts back into an email message stream. If you do not modify the parsed data then the rebuilt message should be an exact copy of the original.
This is useful if you want to modify some specific parts of an email, for example to add tracking images or unsubscribe links to the HTML part of the message without changing any other parts of the email.
Supports both <CR><LF> and <LF> (or mixed) line endings. Embedded rfc822 messages are also parsed, in this case you would get two sequential 'node' objects with no 'data' or 'body' in between (first 'node' is for the container node and second for the root node of the embedded message).
In general this module is a primitive for building e-mail parsers/handlers like [mailparser](https://www.npmjs.com/package/mailparser). Alternatively you could use it to parse other MIME-like structures, for example _mbox_ files or multipart/form-data uploads.
See [rewrite-html.js](examples/rewrite-html.js) for an usage example where HTML content is modified on the fly (example script adds a link to every _text/html_ node)
## Usage
### Install
Install from [npm](https://www.npmjs.com/package/mailsplit)
npm install mailsplit --save
### Split message stream
`Splitter` is a transformable stream where input is a byte stream and output is an object stream.
```javascript
let Splitter = require('mailsplit').Splitter;
let splitter = new Splitter(options);
```
Where
- **options** is an optional options object
- **options.ignoreEmbedded** (boolean, defaults to false) if true then treat message/rfc822 node as normal leaf node and do not try to parse it
- **options.maxHeadSize** (number, defaults to Infinity) limits message header size in bytes
#### Events
**'data'** event emits the next parsed object from the message stream.
#### Data objects
- **type**
- `'node'` means that we reached the next mime node and the previous one is completely processed
- `'data'` provides us multipart body parts, including boundaries. This data is not directly related to any specific multipart node, basically it includes everything between the end of one normal node and the header of next node
- `'body'` provides us next chunk for the last seen `'node'` element
- **value** is a buffer value for `'body'` and `'data'` parts
- **getDecoder()** is a function that returns a stream object you can use to decode node contents. Write data from 'body' to decoder and read decoded Buffer value out from it
- **getEncoder()** is a function that returns a stream object you can use to encode node contents. Write buffer data to encoder and read encoded object value out that you can pass to a Joiner
Element with type `'node'` has a bunch of header related methods and properties, see [below](#manipulating-headers).
**Example**
```javascript
let Splitter = require('mailsplit').Splitter;
let splitter = new Splitter();
// handle parsed data
splitter.on('data', data => {
switch (data.type) {
case 'node':
// node header block
process.stdout.write(data.getHeaders());
break;
case 'data':
// multipart message structure
// this is not related to any specific 'node' block as it includes
// everything between the end of some node body and between the next header
process.stdout.write(data.value);
break;
case 'body':
// Leaf element body. Includes the body for the last 'node' block. You might
// have several 'body' calls for a single 'node' block
process.stdout.write(data.value);
break;
}
});
// send data to the parser
someMessagStream.pipe(splitter);
```
### Manipulating headers
If the data object has `type='node'` then you can modify headers for that node (headers can be modified until the data object is passed over to a `Joiner`)
- **node.getHeaders()** returns a Buffer value with generated headers. If you have not modified the headers object in any way then you should get the exact copy of the original. In case you have done something (for example removed a key, or added a new header key), then all linebreaks are forced to <CR><LF> even if the original headers used just <LF>
- **node.setContentType(contentType)** sets or updates mime type for the node
- **node.setCharset(charset)** sets or updates character set in the Content-Type header
- **node.setFilename(filename)** sets or updates filename in the Content-Disposition header (unicode allowed)
You can manipulate specific header keys as well using the `headers` object
- **node.headers.get(key)** returns an array of strings with all header rows for the selected key (these are full header lines, so key name is part of the row string, eg `["Subject: This is subject line"]`)
- **node.headers.getFirst(key)** returns string value of the specified header key (eg `"This is subject line"`)
- **node.headers.add(key, value [,index])** adds a new header value to the specified index or to the top of the header block if index is not specified
- **node.headers.update(key, value)** replaces a header value for the specified key
- **node.headers.delete(key)** remove header value
- **node.headers.mbox** If this is a MBOX formatted message then this value holds the prefix line (eg. "From MAILER-DAEMON Fri Jul 8 12:08:34 2011")
- **node.headers.mbox** If this is a POST form-data then this value holds the HTTP prefix line (eg. "POST /upload.php HTTP/1.1")
Additionally you can check the details of the node with the following properties automatically parsed from the headers:
- **node.root** if true then it means this is the message root, so this node should contain Subject, From, To etc. headers
- **node.contentType** returns the mime type of the node (eg. 'text/html')
- **node.disposition** either `'attachment'`, `'inline'` or `false` if not set
- **node.charset** returns the charset of the node as defined in 'Content-Type' header (eg. 'UTF-8') or false if not defined
- **node.encoding** returns the Transfer-Encoding value (eg. 'base64' or 'quoted-printable') or false if not defined
- **node.multipart** if has value, then this is a multipart node (does not have 'body' parts)
- **node.filename** is set if the headers contain a filename value. This is decoded to unicode, so it is a normal string or false if not found
### Join parsed message stream
`Joiner` is a transformable stream where input is the object stream form `Splitter` and output is a byte stream.
```javascript
let Splitter = require('mailsplit').Splitter;
let Joiner = require('mailsplit').Joiner;
let splitter = new Splitter();
let joiner = new Joiner();
// pipe a message source to splitter, then joiner and finally to stdout
someMessagStream
.pipe(splitter)
.pipe(joiner)
.pipe(process.stdout);
```
### Rewrite specific nodes
`Rewriter` is a simple helper class to modify nodes that match a filter function. You can pipe a Splitter stream directly into a Rewriter and pipe Rewriter output to a Joiner.
Rewriter takes the following argument:
- **filterFunc** gets the current node as argument and starts processing it if `filterFunc` returns true
Once Rewriter finds a matching node, it emits the following event:
- _'node'_ with an object argument `data`
- `data.node` includes the current node with headers
- `data.decoder` is the decoder stream that you can read data from
- `data.encoder` is the encoder stream that you can write data to. Whatever you write into that stream will be encoded properly and inserted as the content of the current node
```javascript
let Splitter = require('mailsplit').Splitter;
let Joiner = require('mailsplit').Joiner;
let Rewriter = require('mailsplit').Rewriter;
let splitter = new Splitter();
let joiner = new Joiner();
let rewriter = new Rewriter(node => node.contentType === 'text/html');
rewriter.on('node', data => {
// manage headers with node.headers
node.headers.add('X-Processed-Time', new Date.toISOString());
// do nothing, just reencode existing data
data.decoder.pipe(data.encoder);
});
// pipe a message source to splitter, then rewriter, then joiner and finally to stdout
someMessagStream
.pipe(splitter)
.pipe(rewriter)
.pipe(joiner)
.pipe(process.stdout);
```
### Stream specific nodes
`Streamer` is a simple helper class to stream nodes that match a filter function. You can pipe a Splitter stream directly into a Streamer and pipe Streamer output to a Joiner.
Streamer takes the following argument:
- **filterFunc** gets the current node as argument and starts processing it if `filterFunc` returns true
Once Streamer finds a matching node, it emits the following event:
- _'node'_ with an object argument `data`
- `data.node` includes the current node with headers (informational only, you can't modify it)
- `data.decoder` is the decoder stream that you can read data from
- `data.done` is a function you must call once you have processed the stream
```javascript
let Splitter = require('mailsplit').Splitter;
let Joiner = require('mailsplit').Joiner;
let Streamer = require('mailsplit').Streamer;
let fs = require('fs');
let splitter = new Splitter();
let joiner = new Joiner();
let streamer = new Streamer(node => node.contentType === 'image/jpeg');
streamer.on('node', data => {
// write to file
data.decoder.pipe(fs.createWriteStream(data.node.filename || 'image.jpg'));
data.done();
});
// pipe a message source to splitter, then streamer, then joiner and finally to stdout
someMessagStream
.pipe(splitter)
.pipe(streamer)
.pipe(joiner)
.pipe(process.stdout);
```
### Benchmark
Parsing and re-building messages is not fast but it isn't slow either. On my Macbook Pro I got around 22 MB/second (single process, single parsing queue) when parsing random messages from my own email archive. Time spent includes file calls to find and load random messages from disk.
```
Streaming 20000 random messages through a plain PassThrough
Done. 20000 messages [1244 MB] processed in 10.095 s. with average of 1981 messages/sec [123 MB/s]
Streaming 20000 random messages through Splitter and Joiner
Done. 20000 messages [1244 MB] processed in 55.882 s. with average of 358 messages/sec [22 MB/s]
```
## License
Dual licensed under **MIT** or **EUPLv1.1+**

15
nodered/rootfs/data/node_modules/mailsplit/index.js generated vendored Normal file
View File

@@ -0,0 +1,15 @@
'use strict';
const MessageSplitter = require('./lib/message-splitter');
const MessageJoiner = require('./lib/message-joiner');
const NodeRewriter = require('./lib/node-rewriter');
const NodeStreamer = require('./lib/node-streamer');
const Headers = require('./lib/headers');
module.exports = {
Splitter: MessageSplitter,
Joiner: MessageJoiner,
Rewriter: NodeRewriter,
Streamer: NodeStreamer,
Headers
};

View File

@@ -0,0 +1,55 @@
'use strict';
// Helper class to rewrite nodes with specific mime type
const Transform = require('stream').Transform;
const libmime = require('libmime');
/**
* Really bad "stream" transform to parse format=flowed content
*
* @constructor
* @param {String} delSp True if delsp option was used
*/
class FlowedDecoder extends Transform {
constructor(config) {
super();
this.config = config || {};
this.chunks = [];
this.chunklen = 0;
this.libmime = new libmime.Libmime({ Iconv: config.Iconv });
}
_transform(chunk, encoding, callback) {
if (!chunk || !chunk.length) {
return callback();
}
if (!encoding !== 'buffer') {
chunk = Buffer.from(chunk, encoding);
}
this.chunks.push(chunk);
this.chunklen += chunk.length;
callback();
}
_flush(callback) {
if (this.chunklen) {
let currentBody = Buffer.concat(this.chunks, this.chunklen);
if (this.config.encoding === 'base64') {
currentBody = Buffer.from(currentBody.toString('binary'), 'base64');
}
let content = this.libmime.decodeFlowed(currentBody.toString('binary'), this.config.delSp);
this.push(Buffer.from(content, 'binary'));
}
return callback();
}
}
module.exports = FlowedDecoder;

View File

@@ -0,0 +1,206 @@
'use strict';
const libmime = require('libmime');
/**
* Class Headers to parse and handle message headers. Headers instance allows to
* check existing, delete or add new headers
*/
class Headers {
constructor(headers, config) {
config = config || {};
if (Array.isArray(headers)) {
// already using parsed headers
this.changed = true;
this.headers = false;
this.parsed = true;
this.lines = headers;
} else {
// using original string/buffer headers
this.changed = false;
this.headers = headers;
this.parsed = false;
this.lines = false;
}
this.mbox = false;
this.http = false;
this.libmime = new libmime.Libmime({ Iconv: config.Iconv });
}
get(key) {
if (!this.parsed) {
this._parseHeaders();
}
key = this._normalizeHeader(key);
let lines = this.lines.filter(line => line.key === key).map(line => line.line);
return lines;
}
getDecoded(key) {
return this.get(key)
.map(line => this.libmime.decodeHeader(line))
.filter(line => line && line.value);
}
getFirst(key) {
if (!this.parsed) {
this._parseHeaders();
}
key = this._normalizeHeader(key);
let header = this.lines.find(line => line.key === key);
if (!header) {
return '';
}
return ((this.libmime.decodeHeader(header.line) || {}).value || '').toString().trim();
}
getList() {
if (!this.parsed) {
this._parseHeaders();
}
return this.lines;
}
add(key, value, index) {
if (typeof value === 'undefined') {
return;
}
if (typeof value === 'number') {
value = value.toString();
}
if (typeof value === 'string') {
value = Buffer.from(value);
}
value = value.toString('binary');
this.addFormatted(key, this.libmime.foldLines(key + ': ' + value.replace(/\r?\n/g, ''), 76, false), index);
}
addFormatted(key, line, index) {
if (!this.parsed) {
this._parseHeaders();
}
index = index || 0;
this.changed = true;
let header = {
key: this._normalizeHeader(key),
line
};
if (index < 1) {
this.lines.unshift(header);
} else if (index >= this.lines.length) {
this.lines.push(header);
} else {
this.lines.splice(index, 0, header);
}
}
remove(key) {
if (!this.parsed) {
this._parseHeaders();
}
key = this._normalizeHeader(key);
for (let i = this.lines.length - 1; i >= 0; i--) {
if (this.lines[i].key === key) {
this.changed = true;
this.lines.splice(i, 1);
}
}
}
update(key, value) {
if (!this.parsed) {
this._parseHeaders();
}
let keyName = key;
let index = 0;
key = this._normalizeHeader(key);
for (let i = this.lines.length - 1; i >= 0; i--) {
if (this.lines[i].key === key) {
index = i;
this.changed = true;
this.lines.splice(i, 1);
}
}
this.add(keyName, value, index);
}
build(lineEnd) {
if (!this.changed && !lineEnd) {
return typeof this.headers === 'string' ? Buffer.from(this.headers, 'binary') : this.headers;
}
if (!this.parsed) {
this._parseHeaders();
}
lineEnd = lineEnd || '\r\n';
let headers = this.lines.map(line => line.line.replace(/\r?\n/g, lineEnd)).join(lineEnd) + `${lineEnd}${lineEnd}`;
if (this.mbox) {
headers = this.mbox + lineEnd + headers;
}
if (this.http) {
headers = this.http + lineEnd + headers;
}
return Buffer.from(headers, 'binary');
}
_normalizeHeader(key) {
return (key || '').toLowerCase().trim();
}
_parseHeaders() {
if (!this.headers) {
this.lines = [];
this.parsed = true;
return;
}
let lines = this.headers
.toString('binary')
.replace(/[\r\n]+$/, '')
.split(/\r?\n/);
for (let i = lines.length - 1; i >= 0; i--) {
let chr = lines[i].charAt(0);
if (i && (chr === ' ' || chr === '\t')) {
lines[i - 1] += '\r\n' + lines[i];
lines.splice(i, 1);
} else {
let line = lines[i];
if (!i && /^From /i.test(line)) {
// mbox file
this.mbox = line;
lines.splice(i, 1);
continue;
} else if (!i && /^POST /i.test(line)) {
// HTTP POST request
this.http = line;
lines.splice(i, 1);
continue;
}
let key = this._normalizeHeader(line.substr(0, line.indexOf(':')));
lines[i] = {
key,
line
};
}
}
this.lines = lines;
this.parsed = true;
}
}
// expose to the world
module.exports = Headers;

View File

@@ -0,0 +1,30 @@
'use strict';
const Transform = require('stream').Transform;
class MessageJoiner extends Transform {
constructor() {
let options = {
readableObjectMode: false,
writableObjectMode: true
};
super(options);
}
_transform(obj, encoding, callback) {
if (Buffer.isBuffer(obj)) {
this.push(obj);
} else if (obj.type === 'node') {
this.push(obj.getHeaders());
} else if (obj.value) {
this.push(obj.value);
}
return callback();
}
_flush(callback) {
return callback();
}
}
module.exports = MessageJoiner;

View File

@@ -0,0 +1,422 @@
'use strict';
const Transform = require('stream').Transform;
const MimeNode = require('./mime-node');
const MAX_HEAD_SIZE = 1 * 1024 * 1024;
const MAX_CHILD_NODES = 1000;
const HEAD = 0x01;
const BODY = 0x02;
class MessageSplitter extends Transform {
constructor(config) {
let options = {
readableObjectMode: true,
writableObjectMode: false
};
super(options);
this.config = config || {};
this.maxHeadSize = this.config.maxHeadSize || MAX_HEAD_SIZE;
this.maxChildNodes = this.config.maxChildNodes || MAX_CHILD_NODES;
this.tree = [];
this.nodeCounter = 0;
this.newNode();
this.tree.push(this.node);
this.line = false;
this.errored = false;
}
_transform(chunk, encoding, callback) {
// process line by line
// find next line ending
let pos = 0;
let i = 0;
let group = {
type: 'none'
};
let groupstart = this.line ? -this.line.length : 0;
let groupend = 0;
let checkTrailingLinebreak = data => {
if (data.type === 'body' && data.node.parentNode && data.value && data.value.length) {
if (data.value[data.value.length - 1] === 0x0a) {
groupstart--;
groupend--;
pos--;
if (data.value.length > 1 && data.value[data.value.length - 2] === 0x0d) {
groupstart--;
groupend--;
pos--;
if (groupstart < 0 && !this.line) {
// store only <CR> as <LF> should be on the positive side
this.line = Buffer.allocUnsafe(1);
this.line[0] = 0x0d;
}
data.value = data.value.slice(0, data.value.length - 2);
} else {
data.value = data.value.slice(0, data.value.length - 1);
}
} else if (data.value[data.value.length - 1] === 0x0d) {
groupstart--;
groupend--;
pos--;
data.value = data.value.slice(0, data.value.length - 1);
}
}
};
let iterateData = () => {
for (let len = chunk.length; i < len; i++) {
// find next <LF>
if (chunk[i] === 0x0a) {
// line end
let start = Math.max(pos, 0);
pos = ++i;
return this.processLine(chunk.slice(start, i), false, (err, data, flush) => {
if (err) {
this.errored = true;
return setImmediate(() => callback(err));
}
if (!data) {
return setImmediate(iterateData);
}
if (flush) {
if (group && group.type !== 'none') {
if (group.type === 'body' && groupend >= groupstart && group.node.parentNode) {
// do not include the last line ending for body
if (chunk[groupend - 1] === 0x0a) {
groupend--;
if (groupend >= groupstart && chunk[groupend - 1] === 0x0d) {
groupend--;
}
}
}
if (groupstart !== groupend) {
group.value = chunk.slice(groupstart, groupend);
if (groupend < i) {
data.value = chunk.slice(groupend, i);
}
}
this.push(group);
group = {
type: 'none'
};
groupstart = groupend = i;
}
this.push(data);
groupend = i;
return setImmediate(iterateData);
}
if (data.type === group.type) {
// shift slice end position forward
groupend = i;
} else {
if (group.type === 'body' && groupend >= groupstart && group.node.parentNode) {
// do not include the last line ending for body
if (chunk[groupend - 1] === 0x0a) {
groupend--;
if (groupend >= groupstart && chunk[groupend - 1] === 0x0d) {
groupend--;
}
}
}
if (group.type !== 'none' && group.type !== 'node') {
// we have a previous data/body chunk to output
if (groupstart !== groupend) {
group.value = chunk.slice(groupstart, groupend);
if (group.value && group.value.length) {
this.push(group);
group = {
type: 'none'
};
}
}
}
if (data.type === 'node') {
this.push(data);
groupstart = i;
groupend = i;
} else if (groupstart < 0) {
groupstart = i;
groupend = i;
checkTrailingLinebreak(data);
if (data.value && data.value.length) {
this.push(data);
}
} else {
// start new body/data chunk
group = data;
groupstart = groupend;
groupend = i;
}
}
return setImmediate(iterateData);
});
}
}
// skip last linebreak for body
if (pos >= groupstart + 1 && group.type === 'body' && group.node.parentNode) {
// do not include the last line ending for body
if (chunk[pos - 1] === 0x0a) {
pos--;
if (pos >= groupstart && chunk[pos - 1] === 0x0d) {
pos--;
}
}
}
if (group.type !== 'none' && group.type !== 'node' && pos > groupstart) {
// we have a leftover data/body chunk to push out
group.value = chunk.slice(groupstart, pos);
if (group.value && group.value.length) {
this.push(group);
group = {
type: 'none'
};
}
}
if (pos < chunk.length) {
if (this.line) {
this.line = Buffer.concat([this.line, chunk.slice(pos)]);
} else {
this.line = chunk.slice(pos);
}
}
callback();
};
setImmediate(iterateData);
}
_flush(callback) {
if (this.errored) {
return callback();
}
this.processLine(false, true, (err, data) => {
if (err) {
return setImmediate(() => callback(err));
}
if (data && (data.type === 'node' || (data.value && data.value.length))) {
this.push(data);
}
callback();
});
}
compareBoundary(line, startpos, boundary) {
// --{boundary}\r\n or --{boundary}--\r\n
if (line.length < boundary.length + 3 + startpos || line.length > boundary.length + 6 + startpos) {
return false;
}
for (let i = 0; i < boundary.length; i++) {
if (line[i + 2 + startpos] !== boundary[i]) {
return false;
}
}
let pos = 0;
for (let i = boundary.length + 2 + startpos; i < line.length; i++) {
let c = line[i];
if (pos === 0 && (c === 0x0d || c === 0x0a)) {
// 1: next node
return 1;
}
if (pos === 0 && c !== 0x2d) {
// expecting "-"
return false;
}
if (pos === 1 && c !== 0x2d) {
// expecting "-"
return false;
}
if (pos === 2 && c !== 0x0d && c !== 0x0a) {
// expecting line terminator, either <CR> or <LF>
return false;
}
if (pos === 3 && c !== 0x0a) {
// expecting line terminator <LF>
return false;
}
pos++;
}
// 2: multipart end
return 2;
}
checkBoundary(line) {
let startpos = 0;
if (line.length >= 1 && (line[0] === 0x0d || line[0] === 0x0a)) {
startpos++;
if (line.length >= 2 && (line[0] === 0x0d || line[1] === 0x0a)) {
startpos++;
}
}
if (line.length < 4 || line[startpos] !== 0x2d || line[startpos + 1] !== 0x2d) {
// defnitely not a boundary
return false;
}
let boundary;
if (this.node._boundary && (boundary = this.compareBoundary(line, startpos, this.node._boundary))) {
// 1: next child
// 2: multipart end
return boundary;
}
if (this.node._parentBoundary && (boundary = this.compareBoundary(line, startpos, this.node._parentBoundary))) {
// 3: next sibling
// 4: parent end
return boundary + 2;
}
return false;
}
processLine(line, final, next) {
let flush = false;
if (this.line && line) {
line = Buffer.concat([this.line, line]);
this.line = false;
} else if (this.line && !line) {
line = this.line;
this.line = false;
}
if (!line) {
line = Buffer.alloc(0);
}
if (this.nodeCounter > this.maxChildNodes) {
let err = new Error('Max allowed child nodes exceeded');
err.code = 'EMAXLEN';
return next(err);
}
// we check boundary outside the HEAD/BODY scope as it may appear anywhere
let boundary = this.checkBoundary(line);
if (boundary) {
// reached boundary, switch context
switch (boundary) {
case 1:
// next child
this.newNode(this.node);
flush = true;
break;
case 2:
// reached end of children, keep current node
break;
case 3: {
// next sibling
let parentNode = this.node.parentNode;
if (parentNode && parentNode.contentType === 'message/rfc822') {
// special case where immediate parent is an inline message block
// move up another step
parentNode = parentNode.parentNode;
}
this.newNode(parentNode);
flush = true;
break;
}
case 4:
// special case when boundary close a node with only header.
if (this.node && this.node._headerlen && !this.node.headers) {
this.node.parseHeaders();
this.push(this.node);
}
// move up
if (this.tree.length) {
this.node = this.tree.pop();
}
this.state = BODY;
break;
}
return next(
null,
{
node: this.node,
type: 'data',
value: line
},
flush
);
}
switch (this.state) {
case HEAD: {
this.node.addHeaderChunk(line);
if (this.node._headerlen > this.maxHeadSize) {
let err = new Error('Max header size for a MIME node exceeded');
err.code = 'EMAXLEN';
return next(err);
}
if (final || (line.length === 1 && line[0] === 0x0a) || (line.length === 2 && line[0] === 0x0d && line[1] === 0x0a)) {
let currentNode = this.node;
currentNode.parseHeaders();
// if the content is attached message then just continue
if (
currentNode.contentType === 'message/rfc822' &&
!this.config.ignoreEmbedded &&
(!currentNode.encoding || ['7bit', '8bit', 'binary'].includes(currentNode.encoding)) &&
currentNode.disposition !== 'attachment'
) {
currentNode.messageNode = true;
this.newNode(currentNode);
if (currentNode.parentNode) {
this.node._parentBoundary = currentNode.parentNode._boundary;
}
} else {
if (currentNode.contentType === 'message/rfc822') {
currentNode.messageNode = false;
}
this.state = BODY;
if (currentNode.multipart && currentNode._boundary) {
this.tree.push(currentNode);
}
}
return next(null, currentNode, flush);
}
return next();
}
case BODY: {
return next(
null,
{
node: this.node,
type: this.node.multipart ? 'data' : 'body',
value: line
},
flush
);
}
}
next(null, false);
}
newNode(parent) {
this.node = new MimeNode(parent || false, this.config);
this.state = HEAD;
this.nodeCounter++;
}
}
module.exports = MessageSplitter;

View File

@@ -0,0 +1,235 @@
'use strict';
const Headers = require('./headers');
const libmime = require('libmime');
const libqp = require('libqp');
const libbase64 = require('libbase64');
const PassThrough = require('stream').PassThrough;
class MimeNode {
constructor(parentNode, config) {
this.type = 'node';
this.root = !parentNode;
this.parentNode = parentNode;
this._parentBoundary = this.parentNode && this.parentNode._boundary;
this._headersLines = [];
this._headerlen = 0;
this._parsedContentType = false;
this._boundary = false;
this.multipart = false;
this.encoding = false;
this.headers = false;
this.contentType = false;
this.flowed = false;
this.delSp = false;
this.config = config || {};
this.libmime = new libmime.Libmime({ Iconv: this.config.Iconv });
this.parentPartNumber = (parentNode && this.partNr) || [];
this.partNr = false; // resolved later
this.childPartNumbers = 0;
}
getPartNr(provided) {
if (provided) {
return []
.concat(this.partNr || [])
.filter(nr => !isNaN(nr))
.concat(provided);
}
let childPartNr = ++this.childPartNumbers;
return []
.concat(this.partNr || [])
.filter(nr => !isNaN(nr))
.concat(childPartNr);
}
addHeaderChunk(line) {
if (!line) {
return;
}
this._headersLines.push(line);
this._headerlen += line.length;
}
parseHeaders() {
if (this.headers) {
return;
}
this.headers = new Headers(Buffer.concat(this._headersLines, this._headerlen), this.config);
this._parsedContentType = this.libmime.parseHeaderValue(this.headers.getFirst('Content-Type'));
this._parsedContentDisposition = this.libmime.parseHeaderValue(this.headers.getFirst('Content-Disposition'));
this.encoding = this.headers
.getFirst('Content-Transfer-Encoding')
.replace(/\(.*\)/g, '')
.toLowerCase()
.trim();
this.contentType = (this._parsedContentType.value || '').toLowerCase().trim() || false;
this.charset = this._parsedContentType.params.charset || false;
this.disposition = (this._parsedContentDisposition.value || '').toLowerCase().trim() || false;
this.filename = this._parsedContentDisposition.params.filename || this._parsedContentType.params.name || false;
if (this._parsedContentType.params.format && this._parsedContentType.params.format.toLowerCase().trim() === 'flowed') {
this.flowed = true;
if (this._parsedContentType.params.delsp && this._parsedContentType.params.delsp.toLowerCase().trim() === 'yes') {
this.delSp = true;
}
}
if (this.filename) {
try {
this.filename = this.libmime.decodeWords(this.filename);
} catch (E) {
// failed to parse filename, keep as is (most probably an unknown charset is used)
}
}
this.multipart =
(this.contentType &&
this.contentType.substr(0, this.contentType.indexOf('/')) === 'multipart' &&
this.contentType.substr(this.contentType.indexOf('/') + 1)) ||
false;
this._boundary = (this._parsedContentType.params.boundary && Buffer.from(this._parsedContentType.params.boundary)) || false;
this.rfc822 = this.contentType === 'message/rfc822';
if (!this.parentNode || this.parentNode.rfc822) {
this.partNr = this.parentNode ? this.parentNode.getPartNr('TEXT') : ['TEXT'];
} else {
this.partNr = this.parentNode ? this.parentNode.getPartNr() : [];
}
}
getHeaders() {
if (!this.headers) {
this.parseHeaders();
}
return this.headers.build();
}
setContentType(contentType) {
if (!this.headers) {
this.parseHeaders();
}
contentType = (contentType || '').toLowerCase().trim();
if (contentType) {
this._parsedContentType.value = contentType;
}
if (!this.flowed && this._parsedContentType.params.format) {
delete this._parsedContentType.params.format;
}
if (!this.delSp && this._parsedContentType.params.delsp) {
delete this._parsedContentType.params.delsp;
}
this.headers.update('Content-Type', this.libmime.buildHeaderValue(this._parsedContentType));
}
setCharset(charset) {
if (!this.headers) {
this.parseHeaders();
}
charset = (charset || '').toLowerCase().trim();
if (charset === 'ascii') {
charset = '';
}
if (!charset) {
if (!this._parsedContentType.value) {
// nothing to set or update
return;
}
delete this._parsedContentType.params.charset;
} else {
this._parsedContentType.params.charset = charset;
}
if (!this._parsedContentType.value) {
this._parsedContentType.value = 'text/plain';
}
this.headers.update('Content-Type', this.libmime.buildHeaderValue(this._parsedContentType));
}
setFilename(filename) {
if (!this.headers) {
this.parseHeaders();
}
this.filename = (filename || '').toLowerCase().trim();
if (this._parsedContentType.params.name) {
delete this._parsedContentType.params.name;
this.headers.update('Content-Type', this.libmime.buildHeaderValue(this._parsedContentType));
}
if (!this.filename) {
if (!this._parsedContentDisposition.value) {
// nothing to set or update
return;
}
delete this._parsedContentDisposition.params.filename;
} else {
this._parsedContentDisposition.params.filename = this.filename;
}
if (!this._parsedContentDisposition.value) {
this._parsedContentDisposition.value = 'attachment';
}
this.headers.update('Content-Disposition', this.libmime.buildHeaderValue(this._parsedContentDisposition));
}
getDecoder() {
if (!this.headers) {
this.parseHeaders();
}
switch (this.encoding) {
case 'base64':
return new libbase64.Decoder();
case 'quoted-printable':
return new libqp.Decoder();
default:
return new PassThrough();
}
}
getEncoder(encoding) {
if (!this.headers) {
this.parseHeaders();
}
encoding = (encoding || '')
.toString()
.toLowerCase()
.trim();
if (encoding && encoding !== this.encoding) {
this.headers.update('Content-Transfer-Encoding', encoding);
} else {
encoding = this.encoding;
}
switch (encoding) {
case 'base64':
return new libbase64.Encoder();
case 'quoted-printable':
return new libqp.Encoder();
default:
return new PassThrough();
}
}
}
module.exports = MimeNode;

View File

@@ -0,0 +1,194 @@
'use strict';
// Helper class to rewrite nodes with specific mime type
const Transform = require('stream').Transform;
const FlowedDecoder = require('./flowed-decoder');
/**
* NodeRewriter Transform stream. Updates content for all nodes with specified mime type
*
* @constructor
* @param {String} mimeType Define the Mime-Type to look for
* @param {Function} rewriteAction Function to run with the node content
*/
class NodeRewriter extends Transform {
constructor(filterFunc, rewriteAction) {
let options = {
readableObjectMode: true,
writableObjectMode: true
};
super(options);
this.filterFunc = filterFunc;
this.rewriteAction = rewriteAction;
this.decoder = false;
this.encoder = false;
this.continue = false;
}
_transform(data, encoding, callback) {
this.processIncoming(data, callback);
}
_flush(callback) {
if (this.decoder) {
// emit an empty node just in case there is pending data to end
return this.processIncoming(
{
type: 'none'
},
callback
);
}
return callback();
}
processIncoming(data, callback) {
if (this.decoder && data.type === 'body') {
// data to parse
if (!this.decoder.write(data.value)) {
return this.decoder.once('drain', callback);
} else {
return callback();
}
} else if (this.decoder && data.type !== 'body') {
// stop decoding.
// we can not process the current data chunk as we need to wait until
// the parsed data is completely processed, so we store a reference to the
// continue callback
this.continue = () => {
this.continue = false;
this.decoder = false;
this.encoder = false;
this.processIncoming(data, callback);
};
return this.decoder.end();
} else if (data.type === 'node' && this.filterFunc(data)) {
// found matching node, create new handler
this.emit('node', this.createDecodePair(data));
} else if (this.readable && data.type !== 'none') {
// we don't care about this data, just pass it over to the joiner
this.push(data);
}
callback();
}
createDecodePair(node) {
this.decoder = node.getDecoder();
if (['base64', 'quoted-printable'].includes(node.encoding)) {
this.encoder = node.getEncoder();
} else {
this.encoder = node.getEncoder('quoted-printable');
}
let lastByte = false;
let decoder = this.decoder;
let encoder = this.encoder;
let firstChunk = true;
decoder.$reading = false;
let readFromEncoder = () => {
decoder.$reading = true;
let data = encoder.read();
if (data === null) {
decoder.$reading = false;
return;
}
if (firstChunk) {
firstChunk = false;
if (this.readable) {
this.push(node);
if (node.type === 'body') {
lastByte = node.value && node.value.length && node.value[node.value.length - 1];
}
}
}
let writeMore = true;
if (this.readable) {
writeMore = this.push({
node,
type: 'body',
value: data
});
lastByte = data && data.length && data[data.length - 1];
}
if (writeMore) {
return setImmediate(readFromEncoder);
} else {
encoder.pause();
// no idea how to catch drain? use timeout for now as poor man's substitute
// this.once('drain', () => encoder.resume());
setTimeout(() => {
encoder.resume();
setImmediate(readFromEncoder);
}, 100);
}
};
encoder.on('readable', () => {
if (!decoder.$reading) {
return readFromEncoder();
}
});
encoder.on('end', () => {
if (firstChunk) {
firstChunk = false;
if (this.readable) {
this.push(node);
if (node.type === 'body') {
lastByte = node.value && node.value.length && node.value[node.value.length - 1];
}
}
}
if (lastByte !== 0x0a) {
// make sure there is a terminating line break
this.push({
node,
type: 'body',
value: Buffer.from([0x0a])
});
}
if (this.continue) {
return this.continue();
}
});
if (/^text\//.test(node.contentType) && node.flowed) {
// text/plain; format=flowed is a special case
let flowDecoder = decoder;
decoder = new FlowedDecoder({
delSp: node.delSp,
encoding: node.encoding
});
flowDecoder.on('error', err => {
decoder.emit('error', err);
});
flowDecoder.pipe(decoder);
// we don't know what kind of data we are going to get, does it comply with the
// requirements of format=flowed, so we just cancel it
node.flowed = false;
node.delSp = false;
node.setContentType();
}
return {
node,
decoder,
encoder
};
}
}
module.exports = NodeRewriter;

View File

@@ -0,0 +1,121 @@
'use strict';
// Helper class to rewrite nodes with specific mime type
const Transform = require('stream').Transform;
const FlowedDecoder = require('./flowed-decoder');
/**
* NodeRewriter Transform stream. Updates content for all nodes with specified mime type
*
* @constructor
* @param {String} mimeType Define the Mime-Type to look for
* @param {Function} streamAction Function to run with the node content
*/
class NodeStreamer extends Transform {
constructor(filterFunc, streamAction) {
let options = {
readableObjectMode: true,
writableObjectMode: true
};
super(options);
this.filterFunc = filterFunc;
this.streamAction = streamAction;
this.decoder = false;
this.canContinue = false;
this.continue = false;
}
_transform(data, encoding, callback) {
this.processIncoming(data, callback);
}
_flush(callback) {
if (this.decoder) {
// emit an empty node just in case there is pending data to end
return this.processIncoming(
{
type: 'none'
},
callback
);
}
return callback();
}
processIncoming(data, callback) {
if (this.decoder && data.type === 'body') {
// data to parse
this.push(data);
if (!this.decoder.write(data.value)) {
return this.decoder.once('drain', callback);
} else {
return callback();
}
} else if (this.decoder && data.type !== 'body') {
// stop decoding.
// we can not process the current data chunk as we need to wait until
// the parsed data is completely processed, so we store a reference to the
// continue callback
let doContinue = () => {
this.continue = false;
this.decoder = false;
this.canContinue = false;
this.processIncoming(data, callback);
};
if (this.canContinue) {
setImmediate(doContinue);
} else {
this.continue = () => doContinue();
}
return this.decoder.end();
} else if (data.type === 'node' && this.filterFunc(data)) {
this.push(data);
// found matching node, create new handler
this.emit('node', this.createDecoder(data));
} else if (this.readable && data.type !== 'none') {
// we don't care about this data, just pass it over to the joiner
this.push(data);
}
callback();
}
createDecoder(node) {
this.decoder = node.getDecoder();
let decoder = this.decoder;
decoder.$reading = false;
if (/^text\//.test(node.contentType) && node.flowed) {
let flowDecoder = decoder;
decoder = new FlowedDecoder({
delSp: node.delSp
});
flowDecoder.on('error', err => {
decoder.emit('error', err);
});
flowDecoder.pipe(decoder);
}
return {
node,
decoder,
done: () => {
if (typeof this.continue === 'function') {
// called once input stream is processed
this.continue();
} else {
// called before input stream is processed
this.canContinue = true;
}
}
};
}
}
module.exports = NodeStreamer;

View File

@@ -0,0 +1,60 @@
{
"_from": "mailsplit@4.6.2",
"_id": "mailsplit@4.6.2",
"_inBundle": false,
"_integrity": "sha512-7Bw2R0QfORXexGGQCEK64EeShHacUNyU5kV5F5sj4jPQB3ITe2v9KRqxD40wpuue6W/sBJlSNBZ0AypIeTGQMQ==",
"_location": "/mailsplit",
"_phantomChildren": {},
"_requested": {
"type": "version",
"registry": true,
"raw": "mailsplit@4.6.2",
"name": "mailsplit",
"escapedName": "mailsplit",
"rawSpec": "4.6.2",
"saveSpec": null,
"fetchSpec": "4.6.2"
},
"_requiredBy": [
"/mailparser"
],
"_resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-4.6.2.tgz",
"_shasum": "ce622cea460406035ff9f7d493ed00ea52a27aaa",
"_spec": "mailsplit@4.6.2",
"_where": "/data/node_modules/mailparser",
"author": {
"name": "Andris Reinman"
},
"bundleDependencies": false,
"dependencies": {
"libbase64": "1.2.1",
"libmime": "4.2.1",
"libqp": "1.1.0"
},
"deprecated": false,
"description": "Split email messages into an object stream",
"devDependencies": {
"eslint": "6.6.0",
"eslint-config-nodemailer": "1.2.0",
"eslint-config-prettier": "6.5.0",
"grunt": "1.0.4",
"grunt-cli": "1.3.2",
"grunt-contrib-nodeunit": "2.0.0",
"grunt-eslint": "22.0.0",
"random-message": "1.1.0"
},
"directories": {
"test": "test"
},
"files": [
"lib",
"index.js"
],
"license": "(MIT OR EUPL-1.1+)",
"main": "index.js",
"name": "mailsplit",
"scripts": {
"test": "grunt"
},
"version": "4.6.2"
}