Author | SHA1 | Message | Date |
---|---|---|---|
|
73fa04a43b | out mean | 5 years ago |
|
9e51565b1d | dump mongo | 5 years ago |
|
dee136cde7 | trouble | 5 years ago |
|
5059f552af | fixed | 5 years ago |
|
6fc2a9d854 | fixed | 5 years ago |
|
af0fa30879 | fixed | 5 years ago |
|
a7760c6ada | Cosas | 5 years ago |
|
09924eb01c | Show dino | 5 years ago |
@ -1 +1,5 @@ | |||
mongodb/ | |||
mongodb.old/ | |||
familyark/app/node_modules | |||
**/*.lock | |||
mean.js/ |
@ -0,0 +1 @@ | |||
{"options":{},"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_","ns":"familyark.animals"}],"uuid":"fe9fa94674044136ae0c9e213c9baac6"} |
@ -0,0 +1 @@ | |||
{"options":{},"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_","ns":"familyark.species"}],"uuid":"061364df003e4d8d9513ffb43100d71e"} |
@ -1,226 +0,0 @@ | |||
{ | |||
"systemParams": "linux-x64-64", | |||
"modulesFolders": [ | |||
"node_modules" | |||
], | |||
"flags": [], | |||
"linkedModules": [], | |||
"topLevelPatterns": [ | |||
"body-parser@*", | |||
"bootstrap@*", | |||
"compression@*", | |||
"connect-ensure-login@*", | |||
"cookie-parser@*", | |||
"datatables@*", | |||
"express-session@*", | |||
"express@*", | |||
"helmet@*", | |||
"jquery@*", | |||
"method-override@*", | |||
"mongoose@^4.x", | |||
"morgan@*", | |||
"nodemailer@*", | |||
"passport-local@*", | |||
"passport@*", | |||
"popper.js@*", | |||
"request@*", | |||
"rotating-file-stream@^1.4.6", | |||
"serve-favicon@*" | |||
], | |||
"lockfileEntries": { | |||
"accepts@~1.3.5": "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd", | |||
"accepts@~1.3.7": "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd", | |||
"ajv@^6.5.5": "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7", | |||
"array-flatten@1.1.1": "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2", | |||
"asn1@~0.2.3": "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136", | |||
"assert-plus@1.0.0": "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525", | |||
"assert-plus@^1.0.0": "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525", | |||
"async@2.6.0": "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4", | |||
"asynckit@^0.4.0": "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79", | |||
"aws-sign2@~0.7.0": "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8", | |||
"aws4@^1.8.0": "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e", | |||
"basic-auth@~2.0.1": "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a", | |||
"bcrypt-pbkdf@^1.0.0": "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e", | |||
"bluebird@3.5.0": "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c", | |||
"body-parser@*": "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a", | |||
"body-parser@1.19.0": "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a", | |||
"bootstrap@*": "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.4.1.tgz#8582960eea0c5cd2bede84d8b0baf3789c3e8b01", | |||
"bowser@2.9.0": "https://registry.yarnpkg.com/bowser/-/bowser-2.9.0.tgz#3bed854233b419b9a7422d9ee3e85504373821c9", | |||
"bson@~1.0.4": "https://registry.yarnpkg.com/bson/-/bson-1.0.9.tgz#12319f8323b1254739b7c6bef8d3e89ae05a2f57", | |||
"buffer-shims@~1.0.0": "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51", | |||
"bytes@3.0.0": "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048", | |||
"bytes@3.1.0": "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6", | |||
"camelize@1.0.0": "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b", | |||
"caseless@~0.12.0": "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc", | |||
"combined-stream@^1.0.6": "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f", | |||
"combined-stream@~1.0.6": "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f", | |||
"compressible@~2.0.16": "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba", | |||
"compression@*": "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f", | |||
"connect-ensure-login@*": "https://registry.yarnpkg.com/connect-ensure-login/-/connect-ensure-login-0.1.1.tgz#174dcc51243b9eac23f8d98215aeb6694e2e8a12", | |||
"content-disposition@0.5.3": "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd", | |||
"content-security-policy-builder@2.1.0": "https://registry.yarnpkg.com/content-security-policy-builder/-/content-security-policy-builder-2.1.0.tgz#0a2364d769a3d7014eec79ff7699804deb8cfcbb", | |||
"content-type@~1.0.4": "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b", | |||
"cookie-parser@*": "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.5.tgz#3e572d4b7c0c80f9c61daf604e4336831b5d1d49", | |||
"cookie-signature@1.0.6": "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c", | |||
"cookie@0.4.0": "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba", | |||
"core-util-is@1.0.2": "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7", | |||
"core-util-is@~1.0.0": "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7", | |||
"dashdash@^1.12.0": "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0", | |||
"dasherize@2.0.0": "https://registry.yarnpkg.com/dasherize/-/dasherize-2.0.0.tgz#6d809c9cd0cf7bb8952d80fc84fa13d47ddb1308", | |||
"datatables@*": "https://registry.yarnpkg.com/datatables/-/datatables-1.10.18.tgz#fee16e82aa70b17c5faf1a6954ac68f404f33a70", | |||
"debug@2.6.9": "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f", | |||
"debug@3.1.0": "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261", | |||
"delayed-stream@~1.0.0": "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619", | |||
"depd@2.0.0": "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df", | |||
"depd@~1.1.2": "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9", | |||
"depd@~2.0.0": "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df", | |||
"destroy@~1.0.4": "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80", | |||
"dns-prefetch-control@0.2.0": "https://registry.yarnpkg.com/dns-prefetch-control/-/dns-prefetch-control-0.2.0.tgz#73988161841f3dcc81f47686d539a2c702c88624", | |||
"dont-sniff-mimetype@1.1.0": "https://registry.yarnpkg.com/dont-sniff-mimetype/-/dont-sniff-mimetype-1.1.0.tgz#c7d0427f8bcb095762751252af59d148b0a623b2", | |||
"ecc-jsbn@~0.1.1": "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9", | |||
"ee-first@1.1.1": "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d", | |||
"encodeurl@~1.0.2": "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59", | |||
"es6-promise@3.2.1": "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.2.1.tgz#ec56233868032909207170c39448e24449dd1fc4", | |||
"escape-html@~1.0.3": "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988", | |||
"etag@~1.8.1": "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887", | |||
"expect-ct@0.2.0": "https://registry.yarnpkg.com/expect-ct/-/expect-ct-0.2.0.tgz#3a54741b6ed34cc7a93305c605f63cd268a54a62", | |||
"express-session@*": "https://registry.yarnpkg.com/express-session/-/express-session-1.17.0.tgz#9b50dbb5e8a03c3537368138f072736150b7f9b3", | |||
"express@*": "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134", | |||
"extend@~3.0.2": "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa", | |||
"extsprintf@1.3.0": "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05", | |||
"extsprintf@^1.2.0": "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f", | |||
"fast-deep-equal@^3.1.1": "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4", | |||
"fast-json-stable-stringify@^2.0.0": "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633", | |||
"feature-policy@0.3.0": "https://registry.yarnpkg.com/feature-policy/-/feature-policy-0.3.0.tgz#7430e8e54a40da01156ca30aaec1a381ce536069", | |||
"finalhandler@~1.1.2": "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d", | |||
"forever-agent@~0.6.1": "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91", | |||
"form-data@~2.3.2": "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6", | |||
"forwarded@~0.1.2": "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84", | |||
"frameguard@3.1.0": "https://registry.yarnpkg.com/frameguard/-/frameguard-3.1.0.tgz#bd1442cca1d67dc346a6751559b6d04502103a22", | |||
"fresh@0.5.2": "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7", | |||
"getpass@^0.1.1": "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa", | |||
"har-schema@^2.0.0": "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92", | |||
"har-validator@~5.1.3": "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080", | |||
"helmet-crossdomain@0.4.0": "https://registry.yarnpkg.com/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz#5f1fe5a836d0325f1da0a78eaa5fd8429078894e", | |||
"helmet-csp@2.10.0": "https://registry.yarnpkg.com/helmet-csp/-/helmet-csp-2.10.0.tgz#685dde1747bc16c5e28ad9d91e229a69f0a85e84", | |||
"helmet@*": "https://registry.yarnpkg.com/helmet/-/helmet-3.22.0.tgz#3a6f11d931799145f0aff15dbc563cff9e13131f", | |||
"hide-powered-by@1.1.0": "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.1.0.tgz#be3ea9cab4bdb16f8744be873755ca663383fa7a", | |||
"hooks-fixed@2.0.2": "https://registry.yarnpkg.com/hooks-fixed/-/hooks-fixed-2.0.2.tgz#20076daa07e77d8a6106883ce3f1722e051140b0", | |||
"hpkp@2.0.0": "https://registry.yarnpkg.com/hpkp/-/hpkp-2.0.0.tgz#10e142264e76215a5d30c44ec43de64dee6d1672", | |||
"hsts@2.2.0": "https://registry.yarnpkg.com/hsts/-/hsts-2.2.0.tgz#09119d42f7a8587035d027dda4522366fe75d964", | |||
"http-errors@1.7.2": "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f", | |||
"http-errors@~1.7.2": "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06", | |||
"http-signature@~1.2.0": "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1", | |||
"iconv-lite@0.4.24": "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b", | |||
"ienoopen@1.1.0": "https://registry.yarnpkg.com/ienoopen/-/ienoopen-1.1.0.tgz#411e5d530c982287dbdc3bb31e7a9c9e32630974", | |||
"inherits@2.0.3": "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de", | |||
"inherits@2.0.4": "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c", | |||
"inherits@~2.0.1": "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c", | |||
"ipaddr.js@1.9.1": "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3", | |||
"is-typedarray@~1.0.0": "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a", | |||
"isarray@~1.0.0": "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11", | |||
"isstream@~0.1.2": "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a", | |||
"jquery@*": "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2", | |||
"jquery@>=1.7": "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2", | |||
"jsbn@~0.1.0": "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513", | |||
"json-schema-traverse@^0.4.1": "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660", | |||
"json-schema@0.2.3": "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13", | |||
"json-stringify-safe@~5.0.1": "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb", | |||
"jsprim@^1.2.2": "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2", | |||
"kareem@1.5.0": "https://registry.yarnpkg.com/kareem/-/kareem-1.5.0.tgz#e3e4101d9dcfde299769daf4b4db64d895d17448", | |||
"lodash.get@4.4.2": "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99", | |||
"lodash@^4.14.0": "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548", | |||
"media-typer@0.3.0": "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748", | |||
"merge-descriptors@1.0.1": "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61", | |||
"method-override@*": "https://registry.yarnpkg.com/method-override/-/method-override-3.0.0.tgz#6ab0d5d574e3208f15b0c9cf45ab52000468d7a2", | |||
"methods@~1.1.2": "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee", | |||
"mime-db@1.43.0": "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58", | |||
"mime-db@>= 1.43.0 < 2": "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58", | |||
"mime-types@^2.1.12": "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06", | |||
"mime-types@~2.1.19": "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06", | |||
"mime-types@~2.1.24": "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06", | |||
"mime@1.6.0": "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1", | |||
"mongodb-core@2.1.18": "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-2.1.18.tgz#4c46139bdf3a1f032ded91db49f38eec01659050", | |||
"mongodb@2.2.34": "https://registry.yarnpkg.com/mongodb/-/mongodb-2.2.34.tgz#a34f59bbeb61754aec432de72c3fe21526a44c1a", | |||
"mongoose@^4.x": "https://registry.yarnpkg.com/mongoose/-/mongoose-4.13.20.tgz#7d6cdc35eca23306bb7388a74a08be1dc1487a7d", | |||
"morgan@*": "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7", | |||
"mpath@0.5.1": "https://registry.yarnpkg.com/mpath/-/mpath-0.5.1.tgz#17131501f1ff9e6e4fbc8ffa875aa7065b5775ab", | |||
"mpromise@0.5.5": "https://registry.yarnpkg.com/mpromise/-/mpromise-0.5.5.tgz#f5b24259d763acc2257b0a0c8c6d866fd51732e6", | |||
"mquery@2.3.3": "https://registry.yarnpkg.com/mquery/-/mquery-2.3.3.tgz#221412e5d4e7290ca5582dd16ea8f190a506b518", | |||
"ms@2.0.0": "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8", | |||
"ms@2.1.1": "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a", | |||
"muri@1.3.0": "https://registry.yarnpkg.com/muri/-/muri-1.3.0.tgz#aeccf3db64c56aa7c5b34e00f95b7878527a4721", | |||
"negotiator@0.6.2": "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb", | |||
"nocache@2.1.0": "https://registry.yarnpkg.com/nocache/-/nocache-2.1.0.tgz#120c9ffec43b5729b1d5de88cd71aa75a0ba491f", | |||
"nodemailer@*": "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.6.tgz#d37f504f6560b36616f646a606894fe18819107f", | |||
"oauth-sign@~0.9.0": "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455", | |||
"on-finished@~2.3.0": "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947", | |||
"on-headers@~1.0.2": "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f", | |||
"parseurl@~1.3.2": "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4", | |||
"parseurl@~1.3.3": "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4", | |||
"passport-local@*": "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee", | |||
"passport-strategy@1.x.x": "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4", | |||
"passport@*": "https://registry.yarnpkg.com/passport/-/passport-0.4.1.tgz#941446a21cb92fc688d97a0861c38ce9f738f270", | |||
"path-to-regexp@0.1.7": "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c", | |||
"pause@0.0.1": "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d", | |||
"performance-now@^2.1.0": "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b", | |||
"popper.js@*": "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b", | |||
"process-nextick-args@~1.0.6": "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3", | |||
"proxy-addr@~2.0.5": "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf", | |||
"psl@^1.1.28": "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24", | |||
"punycode@^2.1.0": "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec", | |||
"punycode@^2.1.1": "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec", | |||
"qs@6.7.0": "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc", | |||
"qs@~6.5.2": "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36", | |||
"random-bytes@~1.0.0": "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b", | |||
"range-parser@~1.2.1": "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031", | |||
"raw-body@2.4.0": "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332", | |||
"readable-stream@2.2.7": "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.7.tgz#07057acbe2467b22042d36f98c5ad507054e95b1", | |||
"referrer-policy@1.2.0": "https://registry.yarnpkg.com/referrer-policy/-/referrer-policy-1.2.0.tgz#b99cfb8b57090dc454895ef897a4cc35ef67a98e", | |||
"regexp-clone@0.0.1": "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-0.0.1.tgz#a7c2e09891fdbf38fbb10d376fb73003e68ac589", | |||
"request@*": "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3", | |||
"require_optional@~1.0.0": "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e", | |||
"resolve-from@^2.0.0": "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57", | |||
"rotating-file-stream@^1.4.6": "https://registry.yarnpkg.com/rotating-file-stream/-/rotating-file-stream-1.4.6.tgz#42725b951835f6c3b5c16f8f6126e65758ef1d61", | |||
"safe-buffer@5.1.1": "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853", | |||
"safe-buffer@5.1.2": "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d", | |||
"safe-buffer@5.2.0": "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519", | |||
"safe-buffer@^5.0.1": "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519", | |||
"safe-buffer@^5.1.2": "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519", | |||
"safe-buffer@~5.1.0": "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d", | |||
"safer-buffer@>= 2.1.2 < 3": "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a", | |||
"safer-buffer@^2.0.2": "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a", | |||
"safer-buffer@^2.1.0": "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a", | |||
"safer-buffer@~2.1.0": "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a", | |||
"semver@^5.1.0": "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7", | |||
"send@0.17.1": "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8", | |||
"serve-favicon@*": "https://registry.yarnpkg.com/serve-favicon/-/serve-favicon-2.5.0.tgz#935d240cdfe0f5805307fdfe967d88942a2cbcf0", | |||
"serve-static@1.14.1": "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9", | |||
"setprototypeof@1.1.1": "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683", | |||
"sliced@0.0.5": "https://registry.yarnpkg.com/sliced/-/sliced-0.0.5.tgz#5edc044ca4eb6f7816d50ba2fc63e25d8fe4707f", | |||
"sliced@1.0.1": "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41", | |||
"sshpk@^1.7.0": "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877", | |||
"statuses@>= 1.5.0 < 2": "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c", | |||
"statuses@~1.5.0": "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c", | |||
"string_decoder@~1.0.0": "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab", | |||
"toidentifier@1.0.0": "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553", | |||
"tough-cookie@~2.5.0": "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2", | |||
"tunnel-agent@^0.6.0": "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd", | |||
"tweetnacl@^0.14.3": "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64", | |||
"tweetnacl@~0.14.0": "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64", | |||
"type-is@~1.6.17": "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131", | |||
"type-is@~1.6.18": "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131", | |||
"uid-safe@~2.1.5": "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a", | |||
"unpipe@1.0.0": "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec", | |||
"unpipe@~1.0.0": "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec", | |||
"uri-js@^4.2.2": "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0", | |||
"util-deprecate@~1.0.1": "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf", | |||
"utils-merge@1.0.1": "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713", | |||
"uuid@^3.3.2": "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee", | |||
"vary@~1.1.2": "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc", | |||
"verror@1.10.0": "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400", | |||
"x-xss-protection@1.3.0": "https://registry.yarnpkg.com/x-xss-protection/-/x-xss-protection-1.3.0.tgz#3e3a8dd638da80421b0e9fff11a2dbe168f6d52c" | |||
}, | |||
"files": [], | |||
"artifacts": {} | |||
} |
@ -1,5 +0,0 @@ | |||
language: "node_js" | |||
node_js: | |||
- "0.4" | |||
- "0.6" | |||
- "0.8" |
@ -1,20 +0,0 @@ | |||
(The MIT License) | |||
Copyright (c) 2012-2013 Jared Hanson | |||
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 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. |
@ -1,75 +0,0 @@ | |||
# connect-ensure-login | |||
This middleware ensures that a user is logged in. If a request is received that | |||
is unauthenticated, the request will be redirected to a login page. The URL | |||
will be saved in the session, so the user can be conveniently returned to the | |||
page that was originally requested. | |||
## Install | |||
$ npm install connect-ensure-login | |||
## Usage | |||
#### Ensure Authentication | |||
In this example, an application has a settings page where preferences can be | |||
configured. A user must be logged in before accessing this page. | |||
app.get('/settings', | |||
ensureLoggedIn('/login'), | |||
function(req, res) { | |||
res.render('settings', { user: req.user }); | |||
}); | |||
If a user is not logged in when attempting to access this page, the request will | |||
be redirected to `/login` and the original request URL (`/settings`) will be | |||
saved to the session at `req.session.returnTo`. | |||
#### Log In and Return To | |||
This middleware integrates seamlessly with [Passport](http://passportjs.org/). | |||
Simply mount Passport's `authenticate()` middleware at the login route. | |||
app.get('/login', function(req, res) { | |||
res.render('login'); | |||
}); | |||
app.post('/login', passport.authenticate('local', { successReturnToOrRedirect: '/', failureRedirect: '/login' })); | |||
Upon log in, Passport will notice the `returnTo` URL saved in the session and | |||
redirect the user back to `/settings`. | |||
#### Step By Step | |||
If the user is not logged in, the sequence of requests and responses that take | |||
place during this process can be confusing. Here is a step-by-step overview of | |||
what happens: | |||
1. User navigates to `GET /settings` | |||
- Middleware sets `session.returnTo` to `/settings` | |||
- Middleware redirects to `/login` | |||
2. User's browser follows redirect to `GET /login` | |||
- Application renders a login form (or, alternatively, offers SSO) | |||
3. User submits credentials to `POST /login` | |||
- Application verifies credentials | |||
- Passport reads `session.returnTo` and redirects to `/settings` | |||
4. User's browser follows redirect to `GET /settings` | |||
- Now authenticated, application renders settings page | |||
## Tests | |||
$ npm install --dev | |||
$ make test | |||
[](http://travis-ci.org/jaredhanson/connect-ensure-login) | |||
## Credits | |||
- [Jared Hanson](http://github.com/jaredhanson) | |||
## License | |||
[The MIT License](http://opensource.org/licenses/MIT) | |||
Copyright (c) 2012-2013 Jared Hanson <[http://jaredhanson.net/](http://jaredhanson.net/)> |
@ -1,52 +0,0 @@ | |||
/** | |||
* Ensure that a user is logged in before proceeding to next route middleware. | |||
* | |||
* This middleware ensures that a user is logged in. If a request is received | |||
* that is unauthenticated, the request will be redirected to a login page (by | |||
* default to `/login`). | |||
* | |||
* Additionally, `returnTo` will be be set in the session to the URL of the | |||
* current request. After authentication, this value can be used to redirect | |||
* the user to the page that was originally requested. | |||
* | |||
* Options: | |||
* - `redirectTo` URL to redirect to for login, defaults to _/login_ | |||
* - `setReturnTo` set redirectTo in session, defaults to _true_ | |||
* | |||
* Examples: | |||
* | |||
* app.get('/profile', | |||
* ensureLoggedIn(), | |||
* function(req, res) { ... }); | |||
* | |||
* app.get('/profile', | |||
* ensureLoggedIn('/signin'), | |||
* function(req, res) { ... }); | |||
* | |||
* app.get('/profile', | |||
* ensureLoggedIn({ redirectTo: '/session/new', setReturnTo: false }), | |||
* function(req, res) { ... }); | |||
* | |||
* @param {Object} options | |||
* @return {Function} | |||
* @api public | |||
*/ | |||
module.exports = function ensureLoggedIn(options) { | |||
if (typeof options == 'string') { | |||
options = { redirectTo: options } | |||
} | |||
options = options || {}; | |||
var url = options.redirectTo || '/login'; | |||
var setReturnTo = (options.setReturnTo === undefined) ? true : options.setReturnTo; | |||
return function(req, res, next) { | |||
if (!req.isAuthenticated || !req.isAuthenticated()) { | |||
if (setReturnTo && req.session) { | |||
req.session.returnTo = req.originalUrl || req.url; | |||
} | |||
return res.redirect(url); | |||
} | |||
next(); | |||
} | |||
} |
@ -1,43 +0,0 @@ | |||
/** | |||
* Ensure that no user is logged in before proceeding to next route middleware. | |||
* | |||
* This middleware ensures that no user is logged in. If a request is received | |||
* that is authenticated, the request will be redirected to another page (by | |||
* default to `/`). | |||
* | |||
* Options: | |||
* - `redirectTo` URL to redirect to in logged in, defaults to _/_ | |||
* | |||
* Examples: | |||
* | |||
* app.get('/login', | |||
* ensureLoggedOut(), | |||
* function(req, res) { ... }); | |||
* | |||
* app.get('/login', | |||
* ensureLoggedOut('/home'), | |||
* function(req, res) { ... }); | |||
* | |||
* app.get('/login', | |||
* ensureLoggedOut({ redirectTo: '/home' }), | |||
* function(req, res) { ... }); | |||
* | |||
* @param {Object} options | |||
* @return {Function} | |||
* @api public | |||
*/ | |||
module.exports = function ensureLoggedOut(options) { | |||
if (typeof options == 'string') { | |||
options = { redirectTo: options } | |||
} | |||
options = options || {}; | |||
var url = options.redirectTo || '/'; | |||
return function(req, res, next) { | |||
if (req.isAuthenticated && req.isAuthenticated()) { | |||
return res.redirect(url); | |||
} | |||
next(); | |||
} | |||
} |
@ -1,10 +0,0 @@ | |||
/** | |||
* Expose middleware. | |||
*/ | |||
exports.ensureAuthenticated = | |||
exports.ensureLoggedIn = require('./ensureLoggedIn'); | |||
exports.ensureUnauthenticated = | |||
exports.ensureNotAuthenticated = | |||
exports.ensureLoggedOut = | |||
exports.ensureNotLoggedIn = require('./ensureLoggedOut'); |
@ -1,32 +0,0 @@ | |||
{ | |||
"name": "connect-ensure-login", | |||
"version": "0.1.1", | |||
"description": "Login session ensuring middleware for Connect.", | |||
"keywords": ["connect", "express", "auth", "authn", "authentication", "login", "session", "passport"], | |||
"repository": { | |||
"type": "git", | |||
"url": "git://github.com/jaredhanson/connect-ensure-login.git" | |||
}, | |||
"bugs": { | |||
"url": "http://github.com/jaredhanson/connect-ensure-login/issues" | |||
}, | |||
"author": { | |||
"name": "Jared Hanson", | |||
"email": "jaredhanson@gmail.com", | |||
"url": "http://www.jaredhanson.net/" | |||
}, | |||
"licenses": [ { | |||
"type": "MIT", | |||
"url": "http://www.opensource.org/licenses/MIT" | |||
} ], | |||
"main": "./lib", | |||
"dependencies": { | |||
}, | |||
"devDependencies": { | |||
"vows": "0.6.x" | |||
}, | |||
"scripts": { | |||
"test": "NODE_PATH=lib node_modules/.bin/vows test/*-test.js" | |||
}, | |||
"engines": { "node": ">= 0.4.0" } | |||
} |
@ -1,424 +0,0 @@ | |||
1.17.0 / 2019-10-10 | |||
=================== | |||
* deps: cookie@0.4.0 | |||
- Add `SameSite=None` support | |||
* deps: safe-buffer@5.2.0 | |||
1.16.2 / 2019-06-12 | |||
=================== | |||
* Fix restoring `cookie.originalMaxAge` when store returns `Date` | |||
* deps: parseurl@~1.3.3 | |||
1.16.1 / 2019-04-11 | |||
=================== | |||
* Fix error passing `data` option to `Cookie` constructor | |||
* Fix uncaught error from bad session data | |||
1.16.0 / 2019-04-10 | |||
=================== | |||
* Catch invalid `cookie.maxAge` value earlier | |||
* Deprecate setting `cookie.maxAge` to a `Date` object | |||
* Fix issue where `resave: false` may not save altered sessions | |||
* Remove `utils-merge` dependency | |||
* Use `safe-buffer` for improved Buffer API | |||
* Use `Set-Cookie` as cookie header name for compatibility | |||
* deps: depd@~2.0.0 | |||
- Replace internal `eval` usage with `Function` constructor | |||
- Use instance methods on `process` to check for listeners | |||
- perf: remove argument reassignment | |||
* deps: on-headers@~1.0.2 | |||
- Fix `res.writeHead` patch missing return value | |||
1.15.6 / 2017-09-26 | |||
=================== | |||
* deps: debug@2.6.9 | |||
* deps: parseurl@~1.3.2 | |||
- perf: reduce overhead for full URLs | |||
- perf: unroll the "fast-path" `RegExp` | |||
* deps: uid-safe@~2.1.5 | |||
- perf: remove only trailing `=` | |||
* deps: utils-merge@1.0.1 | |||
1.15.5 / 2017-08-02 | |||
=================== | |||
* Fix `TypeError` when `req.url` is an empty string | |||
* deps: depd@~1.1.1 | |||
- Remove unnecessary `Buffer` loading | |||
1.15.4 / 2017-07-18 | |||
=================== | |||
* deps: debug@2.6.8 | |||
1.15.3 / 2017-05-17 | |||
=================== | |||
* deps: debug@2.6.7 | |||
- deps: ms@2.0.0 | |||
1.15.2 / 2017-03-26 | |||
=================== | |||
* deps: debug@2.6.3 | |||
- Fix `DEBUG_MAX_ARRAY_LENGTH` | |||
* deps: uid-safe@~2.1.4 | |||
- Remove `base64-url` dependency | |||
1.15.1 / 2017-02-10 | |||
=================== | |||
* deps: debug@2.6.1 | |||
- Fix deprecation messages in WebStorm and other editors | |||
- Undeprecate `DEBUG_FD` set to `1` or `2` | |||
1.15.0 / 2017-01-22 | |||
=================== | |||
* Fix detecting modified session when session contains "cookie" property | |||
* Fix resaving already-saved reloaded session at end of request | |||
* deps: crc@3.4.4 | |||
- perf: use `Buffer.from` when available | |||
* deps: debug@2.6.0 | |||
- Allow colors in workers | |||
- Deprecated `DEBUG_FD` environment variable | |||
- Use same color for same namespace | |||
- Fix error when running under React Native | |||
- deps: ms@0.7.2 | |||
* perf: remove unreachable branch in set-cookie method | |||
1.14.2 / 2016-10-30 | |||
=================== | |||
* deps: crc@3.4.1 | |||
- Fix deprecation warning in Node.js 7.x | |||
* deps: uid-safe@~2.1.3 | |||
- deps: base64-url@1.3.3 | |||
1.14.1 / 2016-08-24 | |||
=================== | |||
* Fix not always resetting session max age before session save | |||
* Fix the cookie `sameSite` option to actually alter the `Set-Cookie` | |||
* deps: uid-safe@~2.1.2 | |||
- deps: base64-url@1.3.2 | |||
1.14.0 / 2016-07-01 | |||
=================== | |||
* Correctly inherit from `EventEmitter` class in `Store` base class | |||
* Fix issue where `Set-Cookie` `Expires` was not always updated | |||
* Methods are no longer enumerable on `req.session` object | |||
* deps: cookie@0.3.1 | |||
- Add `sameSite` option | |||
- Improve error message when `encode` is not a function | |||
- Improve error message when `expires` is not a `Date` | |||
- perf: enable strict mode | |||
- perf: use for loop in parse | |||
- perf: use string concatination for serialization | |||
* deps: parseurl@~1.3.1 | |||
- perf: enable strict mode | |||
* deps: uid-safe@~2.1.1 | |||
- Use `random-bytes` for byte source | |||
- deps: base64-url@1.2.2 | |||
* perf: enable strict mode | |||
* perf: remove argument reassignment | |||
1.13.0 / 2016-01-10 | |||
=================== | |||
* Fix `rolling: true` to not set cookie when no session exists | |||
- Better `saveUninitialized: false` + `rolling: true` behavior | |||
* deps: crc@3.4.0 | |||
1.12.1 / 2015-10-29 | |||
=================== | |||
* deps: cookie@0.2.3 | |||
- Fix cookie `Max-Age` to never be a floating point number | |||
1.12.0 / 2015-10-25 | |||
=================== | |||
* Support the value `'auto'` in the `cookie.secure` option | |||
* deps: cookie@0.2.2 | |||
- Throw on invalid values provided to `serialize` | |||
* deps: depd@~1.1.0 | |||
- Enable strict mode in more places | |||
- Support web browser loading | |||
* deps: on-headers@~1.0.1 | |||
- perf: enable strict mode | |||
1.11.3 / 2015-05-22 | |||
=================== | |||
* deps: cookie@0.1.3 | |||
- Slight optimizations | |||
* deps: crc@3.3.0 | |||
1.11.2 / 2015-05-10 | |||
=================== | |||
* deps: debug@~2.2.0 | |||
- deps: ms@0.7.1 | |||
* deps: uid-safe@~2.0.0 | |||
1.11.1 / 2015-04-08 | |||
=================== | |||
* Fix mutating `options.secret` value | |||
1.11.0 / 2015-04-07 | |||
=================== | |||
* Support an array in `secret` option for key rotation | |||
* deps: depd@~1.0.1 | |||
1.10.4 / 2015-03-15 | |||
=================== | |||
* deps: debug@~2.1.3 | |||
- Fix high intensity foreground color for bold | |||
- deps: ms@0.7.0 | |||
1.10.3 / 2015-02-16 | |||
=================== | |||
* deps: cookie-signature@1.0.6 | |||
* deps: uid-safe@1.1.0 | |||
- Use `crypto.randomBytes`, if available | |||
- deps: base64-url@1.2.1 | |||
1.10.2 / 2015-01-31 | |||
=================== | |||
* deps: uid-safe@1.0.3 | |||
- Fix error branch that would throw | |||
- deps: base64-url@1.2.0 | |||
1.10.1 / 2015-01-08 | |||
=================== | |||
* deps: uid-safe@1.0.2 | |||
- Remove dependency on `mz` | |||
1.10.0 / 2015-01-05 | |||
=================== | |||
* Add `store.touch` interface for session stores | |||
* Fix `MemoryStore` expiration with `resave: false` | |||
* deps: debug@~2.1.1 | |||
1.9.3 / 2014-12-02 | |||
================== | |||
* Fix error when `req.sessionID` contains a non-string value | |||
1.9.2 / 2014-11-22 | |||
================== | |||
* deps: crc@3.2.1 | |||
- Minor fixes | |||
1.9.1 / 2014-10-22 | |||
================== | |||
* Remove unnecessary empty write call | |||
- Fixes Node.js 0.11.14 behavior change | |||
- Helps work-around Node.js 0.10.1 zlib bug | |||
1.9.0 / 2014-09-16 | |||
================== | |||
* deps: debug@~2.1.0 | |||
- Implement `DEBUG_FD` env variable support | |||
* deps: depd@~1.0.0 | |||
1.8.2 / 2014-09-15 | |||
================== | |||
* Use `crc` instead of `buffer-crc32` for speed | |||
* deps: depd@0.4.5 | |||
1.8.1 / 2014-09-08 | |||
================== | |||
* Keep `req.session.save` non-enumerable | |||
* Prevent session prototype methods from being overwritten | |||
1.8.0 / 2014-09-07 | |||
================== | |||
* Do not resave already-saved session at end of request | |||
* deps: cookie-signature@1.0.5 | |||
* deps: debug@~2.0.0 | |||
1.7.6 / 2014-08-18 | |||
================== | |||
* Fix exception on `res.end(null)` calls | |||
1.7.5 / 2014-08-10 | |||
================== | |||
* Fix parsing original URL | |||
* deps: on-headers@~1.0.0 | |||
* deps: parseurl@~1.3.0 | |||
1.7.4 / 2014-08-05 | |||
================== | |||
* Fix response end delay for non-chunked responses | |||
1.7.3 / 2014-08-05 | |||
================== | |||
* Fix `res.end` patch to call correct upstream `res.write` | |||
1.7.2 / 2014-07-27 | |||
================== | |||
* deps: depd@0.4.4 | |||
- Work-around v8 generating empty stack traces | |||
1.7.1 / 2014-07-26 | |||
================== | |||
* deps: depd@0.4.3 | |||
- Fix exception when global `Error.stackTraceLimit` is too low | |||
1.7.0 / 2014-07-22 | |||
================== | |||
* Improve session-ending error handling | |||
- Errors are passed to `next(err)` instead of `console.error` | |||
* deps: debug@1.0.4 | |||
* deps: depd@0.4.2 | |||
- Add `TRACE_DEPRECATION` environment variable | |||
- Remove non-standard grey color from color output | |||
- Support `--no-deprecation` argument | |||
- Support `--trace-deprecation` argument | |||
1.6.5 / 2014-07-11 | |||
================== | |||
* Do not require `req.originalUrl` | |||
* deps: debug@1.0.3 | |||
- Add support for multiple wildcards in namespaces | |||
1.6.4 / 2014-07-07 | |||
================== | |||
* Fix blank responses for stores with synchronous operations | |||
1.6.3 / 2014-07-04 | |||
================== | |||
* Fix resave deprecation message | |||
1.6.2 / 2014-07-04 | |||
================== | |||
* Fix confusing option deprecation messages | |||
1.6.1 / 2014-06-28 | |||
================== | |||
* Fix saveUninitialized deprecation message | |||
1.6.0 / 2014-06-28 | |||
================== | |||
* Add deprecation message to undefined `resave` option | |||
* Add deprecation message to undefined `saveUninitialized` option | |||
* Fix `res.end` patch to return correct value | |||
* Fix `res.end` patch to handle multiple `res.end` calls | |||
* Reject cookies with missing signatures | |||
1.5.2 / 2014-06-26 | |||
================== | |||
* deps: cookie-signature@1.0.4 | |||
- fix for timing attacks | |||
1.5.1 / 2014-06-21 | |||
================== | |||
* Move hard-to-track-down `req.secret` deprecation message | |||
1.5.0 / 2014-06-19 | |||
================== | |||
* Debug name is now "express-session" | |||
* Deprecate integration with `cookie-parser` middleware | |||
* Deprecate looking for secret in `req.secret` | |||
* Directly read cookies; `cookie-parser` no longer required | |||
* Directly set cookies; `res.cookie` no longer required | |||
* Generate session IDs with `uid-safe`, faster and even less collisions | |||
1.4.0 / 2014-06-17 | |||
================== | |||
* Add `genid` option to generate custom session IDs | |||
* Add `saveUninitialized` option to control saving uninitialized sessions | |||
* Add `unset` option to control unsetting `req.session` | |||
* Generate session IDs with `rand-token` by default; reduce collisions | |||
* deps: buffer-crc32@0.2.3 | |||
1.3.1 / 2014-06-14 | |||
================== | |||
* Add description in package for npmjs.org listing | |||
1.3.0 / 2014-06-14 | |||
================== | |||
* Integrate with express "trust proxy" by default | |||
* deps: debug@1.0.2 | |||
1.2.1 / 2014-05-27 | |||
================== | |||
* Fix `resave` such that `resave: true` works | |||
1.2.0 / 2014-05-19 | |||
================== | |||
* Add `resave` option to control saving unmodified sessions | |||
1.1.0 / 2014-05-12 | |||
================== | |||
* Add `name` option; replacement for `key` option | |||
* Use `setImmediate` in MemoryStore for node.js >= 0.10 | |||
1.0.4 / 2014-04-27 | |||
================== | |||
* deps: debug@0.8.1 | |||
1.0.3 / 2014-04-19 | |||
================== | |||
* Use `res.cookie()` instead of `res.setHeader()` | |||
* deps: cookie@0.1.2 | |||
1.0.2 / 2014-02-23 | |||
================== | |||
* Add missing dependency to `package.json` | |||
1.0.1 / 2014-02-15 | |||
================== | |||
* Add missing dependencies to `package.json` | |||
1.0.0 / 2014-02-15 | |||
================== | |||
* Genesis from `connect` |
@ -1,24 +0,0 @@ | |||
(The MIT License) | |||
Copyright (c) 2010 Sencha Inc. | |||
Copyright (c) 2011 TJ Holowaychuk <tj@vision-media.ca> | |||
Copyright (c) 2014-2015 Douglas Christopher Wilson <doug@somethingdoug.com> | |||
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 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. |
@ -1,844 +0,0 @@ | |||
# express-session | |||
[![NPM Version][npm-version-image]][npm-url] | |||
[![NPM Downloads][npm-downloads-image]][node-url] | |||
[![Build Status][travis-image]][travis-url] | |||
[![Test Coverage][coveralls-image]][coveralls-url] | |||
## Installation | |||
This is a [Node.js](https://nodejs.org/en/) module available through the | |||
[npm registry](https://www.npmjs.com/). Installation is done using the | |||
[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): | |||
```sh | |||
$ npm install express-session | |||
``` | |||
## API | |||
```js | |||
var session = require('express-session') | |||
``` | |||
### session(options) | |||
Create a session middleware with the given `options`. | |||
**Note** Session data is _not_ saved in the cookie itself, just the session ID. | |||
Session data is stored server-side. | |||
**Note** Since version 1.5.0, the [`cookie-parser` middleware](https://www.npmjs.com/package/cookie-parser) | |||
no longer needs to be used for this module to work. This module now directly reads | |||
and writes cookies on `req`/`res`. Using `cookie-parser` may result in issues | |||
if the `secret` is not the same between this module and `cookie-parser`. | |||
**Warning** The default server-side session storage, `MemoryStore`, is _purposely_ | |||
not designed for a production environment. It will leak memory under most | |||
conditions, does not scale past a single process, and is meant for debugging and | |||
developing. | |||
For a list of stores, see [compatible session stores](#compatible-session-stores). | |||
#### Options | |||
`express-session` accepts these properties in the options object. | |||
##### cookie | |||
Settings object for the session ID cookie. The default value is | |||
`{ path: '/', httpOnly: true, secure: false, maxAge: null }`. | |||
The following are options that can be set in this object. | |||
##### cookie.domain | |||
Specifies the value for the `Domain` `Set-Cookie` attribute. By default, no domain | |||
is set, and most clients will consider the cookie to apply to only the current | |||
domain. | |||
##### cookie.expires | |||
Specifies the `Date` object to be the value for the `Expires` `Set-Cookie` attribute. | |||
By default, no expiration is set, and most clients will consider this a | |||
"non-persistent cookie" and will delete it on a condition like exiting a web browser | |||
application. | |||
**Note** If both `expires` and `maxAge` are set in the options, then the last one | |||
defined in the object is what is used. | |||
**Note** The `expires` option should not be set directly; instead only use the `maxAge` | |||
option. | |||
##### cookie.httpOnly | |||
Specifies the `boolean` value for the `HttpOnly` `Set-Cookie` attribute. When truthy, | |||
the `HttpOnly` attribute is set, otherwise it is not. By default, the `HttpOnly` | |||
attribute is set. | |||
**Note** be careful when setting this to `true`, as compliant clients will not allow | |||
client-side JavaScript to see the cookie in `document.cookie`. | |||
##### cookie.maxAge | |||
Specifies the `number` (in milliseconds) to use when calculating the `Expires` | |||
`Set-Cookie` attribute. This is done by taking the current server time and adding | |||
`maxAge` milliseconds to the value to calculate an `Expires` datetime. By default, | |||
no maximum age is set. | |||
**Note** If both `expires` and `maxAge` are set in the options, then the last one | |||
defined in the object is what is used. | |||
##### cookie.path | |||
Specifies the value for the `Path` `Set-Cookie`. By default, this is set to `'/'`, which | |||
is the root path of the domain. | |||
##### cookie.sameSite | |||
Specifies the `boolean` or `string` to be the value for the `SameSite` `Set-Cookie` attribute. | |||
- `true` will set the `SameSite` attribute to `Strict` for strict same site enforcement. | |||
- `false` will not set the `SameSite` attribute. | |||
- `'lax'` will set the `SameSite` attribute to `Lax` for lax same site enforcement. | |||
- `'none'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie. | |||
- `'strict'` will set the `SameSite` attribute to `Strict` for strict same site enforcement. | |||
More information about the different enforcement levels can be found in | |||
[the specification][rfc-6265bis-03-4.1.2.7]. | |||
**Note** This is an attribute that has not yet been fully standardized, and may change in | |||
the future. This also means many clients may ignore this attribute until they understand it. | |||
##### cookie.secure | |||
Specifies the `boolean` value for the `Secure` `Set-Cookie` attribute. When truthy, | |||
the `Secure` attribute is set, otherwise it is not. By default, the `Secure` | |||
attribute is not set. | |||
**Note** be careful when setting this to `true`, as compliant clients will not send | |||
the cookie back to the server in the future if the browser does not have an HTTPS | |||
connection. | |||
Please note that `secure: true` is a **recommended** option. However, it requires | |||
an https-enabled website, i.e., HTTPS is necessary for secure cookies. If `secure` | |||
is set, and you access your site over HTTP, the cookie will not be set. If you | |||
have your node.js behind a proxy and are using `secure: true`, you need to set | |||
"trust proxy" in express: | |||
```js | |||
var app = express() | |||
app.set('trust proxy', 1) // trust first proxy | |||
app.use(session({ | |||
secret: 'keyboard cat', | |||
resave: false, | |||
saveUninitialized: true, | |||
cookie: { secure: true } | |||
})) | |||
``` | |||
For using secure cookies in production, but allowing for testing in development, | |||
the following is an example of enabling this setup based on `NODE_ENV` in express: | |||
```js | |||
var app = express() | |||
var sess = { | |||
secret: 'keyboard cat', | |||
cookie: {} | |||
} | |||
if (app.get('env') === 'production') { | |||
app.set('trust proxy', 1) // trust first proxy | |||
sess.cookie.secure = true // serve secure cookies | |||
} | |||
app.use(session(sess)) | |||
``` | |||
The `cookie.secure` option can also be set to the special value `'auto'` to have | |||
this setting automatically match the determined security of the connection. Be | |||
careful when using this setting if the site is available both as HTTP and HTTPS, | |||
as once the cookie is set on HTTPS, it will no longer be visible over HTTP. This | |||
is useful when the Express `"trust proxy"` setting is properly setup to simplify | |||
development vs production configuration. | |||
##### genid | |||
Function to call to generate a new session ID. Provide a function that returns | |||
a string that will be used as a session ID. The function is given `req` as the | |||
first argument if you want to use some value attached to `req` when generating | |||
the ID. | |||
The default value is a function which uses the `uid-safe` library to generate IDs. | |||
**NOTE** be careful to generate unique IDs so your sessions do not conflict. | |||
```js | |||
app.use(session({ | |||
genid: function(req) { | |||
return genuuid() // use UUIDs for session IDs | |||
}, | |||
secret: 'keyboard cat' | |||
})) | |||
``` | |||
##### name | |||
The name of the session ID cookie to set in the response (and read from in the | |||
request). | |||
The default value is `'connect.sid'`. | |||
**Note** if you have multiple apps running on the same hostname (this is just | |||
the name, i.e. `localhost` or `127.0.0.1`; different schemes and ports do not | |||
name a different hostname), then you need to separate the session cookies from | |||
each other. The simplest method is to simply set different `name`s per app. | |||
##### proxy | |||
Trust the reverse proxy when setting secure cookies (via the "X-Forwarded-Proto" | |||
header). | |||
The default value is `undefined`. | |||
- `true` The "X-Forwarded-Proto" header will be used. | |||
- `false` All headers are ignored and the connection is considered secure only | |||
if there is a direct TLS/SSL connection. | |||
- `undefined` Uses the "trust proxy" setting from express | |||
##### resave | |||
Forces the session to be saved back to the session store, even if the session | |||
was never modified during the request. Depending on your store this may be | |||
necessary, but it can also create race conditions where a client makes two | |||
parallel requests to your server and changes made to the session in one | |||
request may get overwritten when the other request ends, even if it made no | |||
changes (this behavior also depends on what store you're using). | |||
The default value is `true`, but using the default has been deprecated, | |||
as the default will change in the future. Please research into this setting | |||
and choose what is appropriate to your use-case. Typically, you'll want | |||
`false`. | |||
How do I know if this is necessary for my store? The best way to know is to | |||
check with your store if it implements the `touch` method. If it does, then | |||
you can safely set `resave: false`. If it does not implement the `touch` | |||
method and your store sets an expiration date on stored sessions, then you | |||
likely need `resave: true`. | |||
##### rolling | |||
Force the session identifier cookie to be set on every response. The expiration | |||
is reset to the original [`maxAge`](#cookiemaxage), resetting the expiration | |||
countdown. | |||
The default value is `false`. | |||
With this enabled, the session identifier cookie will expire in | |||
[`maxAge`](#cookiemaxage) since the last response was sent instead of in | |||
[`maxAge`](#cookiemaxage) since the session was last modified by the server. | |||
This is typically used in conjuction with short, non-session-length | |||
[`maxAge`](#cookiemaxage) values to provide a quick timeout of the session data | |||
with reduced potentional of it occurring during on going server interactions. | |||
**Note** When this option is set to `true` but the `saveUninitialized` option is | |||
set to `false`, the cookie will not be set on a response with an uninitialized | |||
session. This option only modifies the behavior when an existing session was | |||
loaded for the request. | |||
##### saveUninitialized | |||
Forces a session that is "uninitialized" to be saved to the store. A session is | |||
uninitialized when it is new but not modified. Choosing `false` is useful for | |||
implementing login sessions, reducing server storage usage, or complying with | |||
laws that require permission before setting a cookie. Choosing `false` will also | |||
help with race conditions where a client makes multiple parallel requests | |||
without a session. | |||
The default value is `true`, but using the default has been deprecated, as the | |||
default will change in the future. Please research into this setting and | |||
choose what is appropriate to your use-case. | |||
**Note** if you are using Session in conjunction with PassportJS, Passport | |||
will add an empty Passport object to the session for use after a user is | |||
authenticated, which will be treated as a modification to the session, causing | |||
it to be saved. *This has been fixed in PassportJS 0.3.0* | |||
##### secret | |||
**Required option** | |||
This is the secret used to sign the session ID cookie. This can be either a string | |||
for a single secret, or an array of multiple secrets. If an array of secrets is | |||
provided, only the first element will be used to sign the session ID cookie, while | |||
all the elements will be considered when verifying the signature in requests. | |||
##### store | |||
The session store instance, defaults to a new `MemoryStore` instance. | |||
##### unset | |||
Control the result of unsetting `req.session` (through `delete`, setting to `null`, | |||
etc.). | |||
The default value is `'keep'`. | |||
- `'destroy'` The session will be destroyed (deleted) when the response ends. | |||
- `'keep'` The session in the store will be kept, but modifications made during | |||
the request are ignored and not saved. | |||
### req.session | |||
To store or access session data, simply use the request property `req.session`, | |||
which is (generally) serialized as JSON by the store, so nested objects | |||
are typically fine. For example below is a user-specific view counter: | |||
```js | |||
// Use the session middleware | |||
app.use(session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }})) | |||
// Access the session as req.session | |||
app.get('/', function(req, res, next) { | |||
if (req.session.views) { | |||
req.session.views++ | |||
res.setHeader('Content-Type', 'text/html') | |||
res.write('<p>views: ' + req.session.views + '</p>') | |||
res.write('<p>expires in: ' + (req.session.cookie.maxAge / 1000) + 's</p>') | |||
res.end() | |||
} else { | |||
req.session.views = 1 | |||
res.end('welcome to the session demo. refresh!') | |||
} | |||
}) | |||
``` | |||
#### Session.regenerate(callback) | |||
To regenerate the session simply invoke the method. Once complete, | |||
a new SID and `Session` instance will be initialized at `req.session` | |||
and the `callback` will be invoked. | |||
```js | |||
req.session.regenerate(function(err) { | |||
// will have a new session here | |||
}) | |||
``` | |||
#### Session.destroy(callback) | |||
Destroys the session and will unset the `req.session` property. | |||
Once complete, the `callback` will be invoked. | |||
```js | |||
req.session.destroy(function(err) { | |||
// cannot access session here | |||
}) | |||
``` | |||
#### Session.reload(callback) | |||
Reloads the session data from the store and re-populates the | |||
`req.session` object. Once complete, the `callback` will be invoked. | |||
```js | |||
req.session.reload(function(err) { | |||
// session updated | |||
}) | |||
``` | |||
#### Session.save(callback) | |||
Save the session back to the store, replacing the contents on the store with the | |||
contents in memory (though a store may do something else--consult the store's | |||
documentation for exact behavior). | |||
This method is automatically called at the end of the HTTP response if the | |||
session data has been altered (though this behavior can be altered with various | |||
options in the middleware constructor). Because of this, typically this method | |||
does not need to be called. | |||
There are some cases where it is useful to call this method, for example, | |||
redirects, long-lived requests or in WebSockets. | |||
```js | |||
req.session.save(function(err) { | |||
// session saved | |||
}) | |||
``` | |||
#### Session.touch() | |||
Updates the `.maxAge` property. Typically this is | |||
not necessary to call, as the session middleware does this for you. | |||
### req.session.id | |||
Each session has a unique ID associated with it. This property is an | |||
alias of [`req.sessionID`](#reqsessionid-1) and cannot be modified. | |||
It has been added to make the session ID accessible from the `session` | |||
object. | |||
### req.session.cookie | |||
Each session has a unique cookie object accompany it. This allows | |||
you to alter the session cookie per visitor. For example we can | |||
set `req.session.cookie.expires` to `false` to enable the cookie | |||
to remain for only the duration of the user-agent. | |||
#### Cookie.maxAge | |||
Alternatively `req.session.cookie.maxAge` will return the time | |||
remaining in milliseconds, which we may also re-assign a new value | |||
to adjust the `.expires` property appropriately. The following | |||
are essentially equivalent | |||
```js | |||
var hour = 3600000 | |||
req.session.cookie.expires = new Date(Date.now() + hour) | |||
req.session.cookie.maxAge = hour | |||
``` | |||
For example when `maxAge` is set to `60000` (one minute), and 30 seconds | |||
has elapsed it will return `30000` until the current request has completed, | |||
at which time `req.session.touch()` is called to reset | |||
`req.session.cookie.maxAge` to its original value. | |||
```js | |||
req.session.cookie.maxAge // => 30000 | |||
``` | |||
#### Cookie.originalMaxAge | |||
The `req.session.cookie.originalMaxAge` property returns the original | |||
`maxAge` (time-to-live), in milliseconds, of the session cookie. | |||
### req.sessionID | |||
To get the ID of the loaded session, access the request property | |||
`req.sessionID`. This is simply a read-only value set when a session | |||
is loaded/created. | |||
## Session Store Implementation | |||
Every session store _must_ be an `EventEmitter` and implement specific | |||
methods. The following methods are the list of **required**, **recommended**, | |||
and **optional**. | |||
* Required methods are ones that this module will always call on the store. | |||
* Recommended methods are ones that this module will call on the store if | |||
available. | |||
* Optional methods are ones this module does not call at all, but helps | |||
present uniform stores to users. | |||
For an example implementation view the [connect-redis](http://github.com/visionmedia/connect-redis) repo. | |||
### store.all(callback) | |||
**Optional** | |||
This optional method is used to get all sessions in the store as an array. The | |||
`callback` should be called as `callback(error, sessions)`. | |||
### store.destroy(sid, callback) | |||
**Required** | |||
This required method is used to destroy/delete a session from the store given | |||
a session ID (`sid`). The `callback` should be called as `callback(error)` once | |||
the session is destroyed. | |||
### store.clear(callback) | |||
**Optional** | |||
This optional method is used to delete all sessions from the store. The | |||
`callback` should be called as `callback(error)` once the store is cleared. | |||
### store.length(callback) | |||
**Optional** | |||
This optional method is used to get the count of all sessions in the store. | |||
The `callback` should be called as `callback(error, len)`. | |||
### store.get(sid, callback) | |||
**Required** | |||
This required method is used to get a session from the store given a session | |||
ID (`sid`). The `callback` should be called as `callback(error, session)`. | |||
The `session` argument should be a session if found, otherwise `null` or | |||
`undefined` if the session was not found (and there was no error). A special | |||
case is made when `error.code === 'ENOENT'` to act like `callback(null, null)`. | |||
### store.set(sid, session, callback) | |||
**Required** | |||
This required method is used to upsert a session into the store given a | |||
session ID (`sid`) and session (`session`) object. The callback should be | |||
called as `callback(error)` once the session has been set in the store. | |||
### store.touch(sid, session, callback) | |||
**Recommended** | |||
This recommended method is used to "touch" a given session given a | |||
session ID (`sid`) and session (`session`) object. The `callback` should be | |||
called as `callback(error)` once the session has been touched. | |||
This is primarily used when the store will automatically delete idle sessions | |||
and this method is used to signal to the store the given session is active, | |||
potentially resetting the idle timer. | |||
## Compatible Session Stores | |||
The following modules implement a session store that is compatible with this | |||
module. Please make a PR to add additional modules :) | |||
[![★][aerospike-session-store-image] aerospike-session-store][aerospike-session-store-url] A session store using [Aerospike](http://www.aerospike.com/). | |||
[aerospike-session-store-url]: https://www.npmjs.com/package/aerospike-session-store | |||
[aerospike-session-store-image]: https://badgen.net/github/stars/aerospike/aerospike-session-store-expressjs?label=%E2%98%85 | |||
[![★][cassandra-store-image] cassandra-store][cassandra-store-url] An Apache Cassandra-based session store. | |||
[cassandra-store-url]: https://www.npmjs.com/package/cassandra-store | |||
[cassandra-store-image]: https://badgen.net/github/stars/webcc/cassandra-store?label=%E2%98%85 | |||
[![★][cluster-store-image] cluster-store][cluster-store-url] A wrapper for using in-process / embedded | |||
stores - such as SQLite (via knex), leveldb, files, or memory - with node cluster (desirable for Raspberry Pi 2 | |||
and other multi-core embedded devices). | |||
[cluster-store-url]: https://www.npmjs.com/package/cluster-store | |||
[cluster-store-image]: https://badgen.net/github/stars/coolaj86/cluster-store?label=%E2%98%85 | |||
[![★][connect-arango-image] connect-arango][connect-arango-url] An ArangoDB-based session store. | |||
[connect-arango-url]: https://www.npmjs.com/package/connect-arango | |||
[connect-arango-image]: https://badgen.net/github/stars/AlexanderArvidsson/connect-arango?label=%E2%98%85 | |||
[![★][connect-azuretables-image] connect-azuretables][connect-azuretables-url] An [Azure Table Storage](https://azure.microsoft.com/en-gb/services/storage/tables/)-based session store. | |||
[connect-azuretables-url]: https://www.npmjs.com/package/connect-azuretables | |||
[connect-azuretables-image]: https://badgen.net/github/stars/mike-goodwin/connect-azuretables?label=%E2%98%85 | |||
[![★][connect-cloudant-store-image] connect-cloudant-store][connect-cloudant-store-url] An [IBM Cloudant](https://cloudant.com/)-based session store. | |||
[connect-cloudant-store-url]: https://www.npmjs.com/package/connect-cloudant-store | |||
[connect-cloudant-store-image]: https://badgen.net/github/stars/adriantanasa/connect-cloudant-store?label=%E2%98%85 | |||
[![★][connect-couchbase-image] connect-couchbase][connect-couchbase-url] A [couchbase](http://www.couchbase.com/)-based session store. | |||
[connect-couchbase-url]: https://www.npmjs.com/package/connect-couchbase | |||
[connect-couchbase-image]: https://badgen.net/github/stars/christophermina/connect-couchbase?label=%E2%98%85 | |||
[![★][connect-datacache-image] connect-datacache][connect-datacache-url] An [IBM Bluemix Data Cache](http://www.ibm.com/cloud-computing/bluemix/)-based session store. | |||
[connect-datacache-url]: https://www.npmjs.com/package/connect-datacache | |||
[connect-datacache-image]: https://badgen.net/github/stars/adriantanasa/connect-datacache?label=%E2%98%85 | |||
[![★][@google-cloud/connect-datastore-image] @google-cloud/connect-datastore][@google-cloud/connect-datastore-url] A [Google Cloud Datastore](https://cloud.google.com/datastore/docs/concepts/overview)-based session store. | |||
[@google-cloud/connect-datastore-url]: https://www.npmjs.com/package/@google-cloud/connect-datastore | |||
[@google-cloud/connect-datastore-image]: https://badgen.net/github/stars/GoogleCloudPlatform/cloud-datastore-session-node?label=%E2%98%85 | |||
[![★][connect-db2-image] connect-db2][connect-db2-url] An IBM DB2-based session store built using [ibm_db](https://www.npmjs.com/package/ibm_db) module. | |||
[connect-db2-url]: https://www.npmjs.com/package/connect-db2 | |||
[connect-db2-image]: https://badgen.net/github/stars/wallali/connect-db2?label=%E2%98%85 | |||
[![★][connect-dynamodb-image] connect-dynamodb][connect-dynamodb-url] A DynamoDB-based session store. | |||
[connect-dynamodb-url]: https://www.npmjs.com/package/connect-dynamodb | |||
[connect-dynamodb-image]: https://badgen.net/github/stars/ca98am79/connect-dynamodb?label=%E2%98%85 | |||
[![★][@google-cloud/connect-firestore-image] @google-cloud/connect-firestore][@google-cloud/connect-firestore-url] A [Google Cloud Firestore](https://cloud.google.com/firestore/docs/overview)-based session store. | |||
[@google-cloud/connect-firestore-url]: https://www.npmjs.com/package/@google-cloud/connect-firestore | |||
[@google-cloud/connect-firestore-image]: https://badgen.net/github/stars/googleapis/nodejs-firestore-session?label=%E2%98%85 | |||
[![★][connect-hazelcast-image] connect-hazelcast][connect-hazelcast-url] Hazelcast session store for Connect and Express. | |||
[connect-hazelcast-url]: https://www.npmjs.com/package/connect-hazelcast | |||
[connect-hazelcast-image]: https://badgen.net/github/stars/huseyinbabal/connect-hazelcast?label=%E2%98%85 | |||
[![★][connect-loki-image] connect-loki][connect-loki-url] A Loki.js-based session store. | |||
[connect-loki-url]: https://www.npmjs.com/package/connect-loki | |||
[connect-loki-image]: https://badgen.net/github/stars/Requarks/connect-loki?label=%E2%98%85 | |||
[![★][connect-memcached-image] connect-memcached][connect-memcached-url] A memcached-based session store. | |||
[connect-memcached-url]: https://www.npmjs.com/package/connect-memcached | |||
[connect-memcached-image]: https://badgen.net/github/stars/balor/connect-memcached?label=%E2%98%85 | |||
[![★][connect-memjs-image] connect-memjs][connect-memjs-url] A memcached-based session store using | |||
[memjs](https://www.npmjs.com/package/memjs) as the memcached client. | |||
[connect-memjs-url]: https://www.npmjs.com/package/connect-memjs | |||
[connect-memjs-image]: https://badgen.net/github/stars/liamdon/connect-memjs?label=%E2%98%85 | |||
[![★][connect-ml-image] connect-ml][connect-ml-url] A MarkLogic Server-based session store. | |||
[connect-ml-url]: https://www.npmjs.com/package/connect-ml | |||
[connect-ml-image]: https://badgen.net/github/stars/bluetorch/connect-ml?label=%E2%98%85 | |||
[![★][connect-monetdb-image] connect-monetdb][connect-monetdb-url] A MonetDB-based session store. | |||
[connect-monetdb-url]: https://www.npmjs.com/package/connect-monetdb | |||
[connect-monetdb-image]: https://badgen.net/github/stars/MonetDB/npm-connect-monetdb?label=%E2%98%85 | |||
[![★][connect-mongo-image] connect-mongo][connect-mongo-url] A MongoDB-based session store. | |||
[connect-mongo-url]: https://www.npmjs.com/package/connect-mongo | |||
[connect-mongo-image]: https://badgen.net/github/stars/kcbanner/connect-mongo?label=%E2%98%85 | |||
[![★][connect-mongodb-session-image] connect-mongodb-session][connect-mongodb-session-url] Lightweight MongoDB-based session store built and maintained by MongoDB. | |||
[connect-mongodb-session-url]: https://www.npmjs.com/package/connect-mongodb-session | |||
[connect-mongodb-session-image]: https://badgen.net/github/stars/mongodb-js/connect-mongodb-session?label=%E2%98%85 | |||
[![★][connect-mssql-image] connect-mssql][connect-mssql-url] A SQL Server-based session store. | |||
[connect-mssql-url]: https://www.npmjs.com/package/connect-mssql | |||
[connect-mssql-image]: https://badgen.net/github/stars/patriksimek/connect-mssql?label=%E2%98%85 | |||
[![★][connect-pg-simple-image] connect-pg-simple][connect-pg-simple-url] A PostgreSQL-based session store. | |||
[connect-pg-simple-url]: https://www.npmjs.com/package/connect-pg-simple | |||
[connect-pg-simple-image]: https://badgen.net/github/stars/voxpelli/node-connect-pg-simple?label=%E2%98%85 | |||
[![★][connect-redis-image] connect-redis][connect-redis-url] A Redis-based session store. | |||
[connect-redis-url]: https://www.npmjs.com/package/connect-redis | |||
[connect-redis-image]: https://badgen.net/github/stars/tj/connect-redis?label=%E2%98%85 | |||
[![★][connect-session-firebase-image] connect-session-firebase][connect-session-firebase-url] A session store based on the [Firebase Realtime Database](https://firebase.google.com/docs/database/) | |||
[connect-session-firebase-url]: https://www.npmjs.com/package/connect-session-firebase | |||
[connect-session-firebase-image]: https://badgen.net/github/stars/benweier/connect-session-firebase?label=%E2%98%85 | |||
[![★][connect-session-knex-image] connect-session-knex][connect-session-knex-url] A session store using | |||
[Knex.js](http://knexjs.org/), which is a SQL query builder for PostgreSQL, MySQL, MariaDB, SQLite3, and Oracle. | |||
[connect-session-knex-url]: https://www.npmjs.com/package/connect-session-knex | |||
[connect-session-knex-image]: https://badgen.net/github/stars/llambda/connect-session-knex?label=%E2%98%85 | |||
[![★][connect-session-sequelize-image] connect-session-sequelize][connect-session-sequelize-url] A session store using | |||
[Sequelize.js](http://sequelizejs.com/), which is a Node.js / io.js ORM for PostgreSQL, MySQL, SQLite and MSSQL. | |||
[connect-session-sequelize-url]: https://www.npmjs.com/package/connect-session-sequelize | |||
[connect-session-sequelize-image]: https://badgen.net/github/stars/mweibel/connect-session-sequelize?label=%E2%98%85 | |||
[![★][connect-sqlite3-image] connect-sqlite3][connect-sqlite3-url] A [SQLite3](https://github.com/mapbox/node-sqlite3) session store modeled after the TJ's `connect-redis` store. | |||
[connect-sqlite3-url]: https://www.npmjs.com/package/connect-sqlite3 | |||
[connect-sqlite3-image]: https://badgen.net/github/stars/rawberg/connect-sqlite3?label=%E2%98%85 | |||
[![★][connect-typeorm-image] connect-typeorm][connect-typeorm-url] A [TypeORM](https://github.com/typeorm/typeorm)-based session store. | |||
[connect-typeorm-url]: https://www.npmjs.com/package/connect-typeorm | |||
[connect-typeorm-image]: https://badgen.net/github/stars/makepost/connect-typeorm?label=%E2%98%85 | |||
[![★][couchdb-expression-image] couchdb-expression][couchdb-expression-url] A [CouchDB](https://couchdb.apache.org/)-based session store. | |||
[couchdb-expression-url]: https://www.npmjs.com/package/couchdb-expression | |||
[couchdb-expression-image]: https://badgen.net/github/stars/tkshnwesper/couchdb-expression?label=%E2%98%85 | |||
[![★][documentdb-session-image] documentdb-session][documentdb-session-url] A session store for Microsoft Azure's [DocumentDB](https://azure.microsoft.com/en-us/services/documentdb/) NoSQL database service. | |||
[documentdb-session-url]: https://www.npmjs.com/package/documentdb-session | |||
[documentdb-session-image]: https://badgen.net/github/stars/dwhieb/documentdb-session?label=%E2%98%85 | |||
[![★][dynamodb-store-image] dynamodb-store][dynamodb-store-url] A DynamoDB-based session store. | |||
[dynamodb-store-url]: https://www.npmjs.com/package/dynamodb-store | |||
[dynamodb-store-image]: https://badgen.net/github/stars/rafaelrpinto/dynamodb-store?label=%E2%98%85 | |||
[![★][express-etcd-image] express-etcd][express-etcd-url] An [etcd](https://github.com/stianeikeland/node-etcd) based session store. | |||
[express-etcd-url]: https://www.npmjs.com/package/express-etcd | |||
[express-etcd-image]: https://badgen.net/github/stars/gildean/express-etcd?label=%E2%98%85 | |||
[![★][express-mysql-session-image] express-mysql-session][express-mysql-session-url] A session store using native | |||
[MySQL](https://www.mysql.com/) via the [node-mysql](https://github.com/felixge/node-mysql) module. | |||
[express-mysql-session-url]: https://www.npmjs.com/package/express-mysql-session | |||
[express-mysql-session-image]: https://badgen.net/github/stars/chill117/express-mysql-session?label=%E2%98%85 | |||
[![★][express-nedb-session-image] express-nedb-session][express-nedb-session-url] A NeDB-based session store. | |||
[express-nedb-session-url]: https://www.npmjs.com/package/express-nedb-session | |||
[express-nedb-session-image]: https://badgen.net/github/stars/louischatriot/express-nedb-session?label=%E2%98%85 | |||
[![★][express-oracle-session-image] express-oracle-session][express-oracle-session-url] A session store using native | |||
[oracle](https://www.oracle.com/) via the [node-oracledb](https://www.npmjs.com/package/oracledb) module. | |||
[express-oracle-session-url]: https://www.npmjs.com/package/express-oracle-session | |||
[express-oracle-session-image]: https://badgen.net/github/stars/slumber86/express-oracle-session?label=%E2%98%85 | |||
[![★][express-session-cache-manager-image] express-session-cache-manager][express-session-cache-manager-url] | |||
A store that implements [cache-manager](https://www.npmjs.com/package/cache-manager), which supports | |||
a [variety of storage types](https://www.npmjs.com/package/cache-manager#store-engines). | |||
[express-session-cache-manager-url]: https://www.npmjs.com/package/express-session-cache-manager | |||
[express-session-cache-manager-image]: https://badgen.net/github/stars/theogravity/express-session-cache-manager?label=%E2%98%85 | |||
[![★][express-session-etcd3-image] express-session-etcd3][express-session-etcd3-url] An [etcd3](https://github.com/mixer/etcd3) based session store. | |||
[express-session-etcd3-url]: https://www.npmjs.com/package/express-session-etcd3 | |||
[express-session-etcd3-image]: https://badgen.net/github/stars/willgm/express-session-etcd3?label=%E2%98%85 | |||
[![★][express-session-level-image] express-session-level][express-session-level-url] A [LevelDB](https://github.com/Level/levelup) based session store. | |||
[express-session-level-url]: https://www.npmjs.com/package/express-session-level | |||
[express-session-level-image]: https://badgen.net/github/stars/tgohn/express-session-level?label=%E2%98%85 | |||
[![★][express-session-rsdb-image] express-session-rsdb][express-session-rsdb-url] Session store based on Rocket-Store: A very simple, super fast and yet powerfull, flat file database. | |||
[express-session-rsdb-url]: https://www.npmjs.com/package/express-session-rsdb | |||
[express-session-rsdb-image]: https://badgen.net/github/stars/paragi/express-session-rsdb?label=%E2%98%85 | |||
[![★][express-sessions-image] express-sessions][express-sessions-url] A session store supporting both MongoDB and Redis. | |||
[express-sessions-url]: https://www.npmjs.com/package/express-sessions | |||
[express-sessions-image]: https://badgen.net/github/stars/konteck/express-sessions?label=%E2%98%85 | |||
[![★][firestore-store-image] firestore-store][firestore-store-url] A [Firestore](https://github.com/hendrysadrak/firestore-store)-based session store. | |||
[firestore-store-url]: https://www.npmjs.com/package/firestore-store | |||
[firestore-store-image]: https://badgen.net/github/stars/hendrysadrak/firestore-store?label=%E2%98%85 | |||
[![★][fortune-session-image] fortune-session][fortune-session-url] A [Fortune.js](https://github.com/fortunejs/fortune) | |||
based session store. Supports all backends supported by Fortune (MongoDB, Redis, Postgres, NeDB). | |||
[fortune-session-url]: https://www.npmjs.com/package/fortune-session | |||
[fortune-session-image]: https://badgen.net/github/stars/aliceklipper/fortune-session?label=%E2%98%85 | |||
[![★][hazelcast-store-image] hazelcast-store][hazelcast-store-url] A Hazelcast-based session store built on the [Hazelcast Node Client](https://www.npmjs.com/package/hazelcast-client). | |||
[hazelcast-store-url]: https://www.npmjs.com/package/hazelcast-store | |||
[hazelcast-store-image]: https://badgen.net/github/stars/jackspaniel/hazelcast-store?label=%E2%98%85 | |||
[![★][level-session-store-image] level-session-store][level-session-store-url] A LevelDB-based session store. | |||
[level-session-store-url]: https://www.npmjs.com/package/level-session-store | |||
[level-session-store-image]: https://badgen.net/github/stars/toddself/level-session-store?label=%E2%98%85 | |||
[![★][lowdb-session-store-image] lowdb-session-store][lowdb-session-store-url] A [lowdb](https://www.npmjs.com/package/lowdb)-based session store. | |||
[lowdb-session-store-url]: https://www.npmjs.com/package/lowdb-session-store | |||
[lowdb-session-store-image]: https://badgen.net/github/stars/fhellwig/lowdb-session-store?label=%E2%98%85 | |||
[![★][medea-session-store-image] medea-session-store][medea-session-store-url] A Medea-based session store. | |||
[medea-session-store-url]: https://www.npmjs.com/package/medea-session-store | |||
[medea-session-store-image]: https://badgen.net/github/stars/BenjaminVadant/medea-session-store?label=%E2%98%85 | |||
[![★][memorystore-image] memorystore][memorystore-url] A memory session store made for production. | |||
[memorystore-url]: https://www.npmjs.com/package/memorystore | |||
[memorystore-image]: https://badgen.net/github/stars/roccomuso/memorystore?label=%E2%98%85 | |||
[![★][mssql-session-store-image] mssql-session-store][mssql-session-store-url] A SQL Server-based session store. | |||
[mssql-session-store-url]: https://www.npmjs.com/package/mssql-session-store | |||
[mssql-session-store-image]: https://badgen.net/github/stars/jwathen/mssql-session-store?label=%E2%98%85 | |||
[![★][nedb-session-store-image] nedb-session-store][nedb-session-store-url] An alternate NeDB-based (either in-memory or file-persisted) session store. | |||
[nedb-session-store-url]: https://www.npmjs.com/package/nedb-session-store | |||
[nedb-session-store-image]: https://badgen.net/github/stars/JamesMGreene/nedb-session-store?label=%E2%98%85 | |||
[![★][restsession-image] restsession][restsession-url] Store sessions utilizing a RESTful API | |||
[restsession-url]: https://www.npmjs.com/package/restsession | |||
[restsession-image]: https://badgen.net/github/stars/jankal/restsession?label=%E2%98%85 | |||
[![★][sequelstore-connect-image] sequelstore-connect][sequelstore-connect-url] A session store using [Sequelize.js](http://sequelizejs.com/). | |||
[sequelstore-connect-url]: https://www.npmjs.com/package/sequelstore-connect | |||
[sequelstore-connect-image]: https://badgen.net/github/stars/MattMcFarland/sequelstore-connect?label=%E2%98%85 | |||
[![★][session-file-store-image] session-file-store][session-file-store-url] A file system-based session store. | |||
[session-file-store-url]: https://www.npmjs.com/package/session-file-store | |||
[session-file-store-image]: https://badgen.net/github/stars/valery-barysok/session-file-store?label=%E2%98%85 | |||
[![★][session-pouchdb-store-image] session-pouchdb-store][session-pouchdb-store-url] Session store for PouchDB / CouchDB. Accepts embedded, custom, or remote PouchDB instance and realtime synchronization. | |||
[session-pouchdb-store-url]: https://www.npmjs.com/package/session-pouchdb-store | |||
[session-pouchdb-store-image]: https://badgen.net/github/stars/solzimer/session-pouchdb-store?label=%E2%98%85 | |||
[![★][session-rethinkdb-image] session-rethinkdb][session-rethinkdb-url] A [RethinkDB](http://rethinkdb.com/)-based session store. | |||
[session-rethinkdb-url]: https://www.npmjs.com/package/session-rethinkdb | |||
[session-rethinkdb-image]: https://badgen.net/github/stars/llambda/session-rethinkdb?label=%E2%98%85 | |||
[![★][sessionstore-image] sessionstore][sessionstore-url] A session store that works with various databases. | |||
[sessionstore-url]: https://www.npmjs.com/package/sessionstore | |||
[sessionstore-image]: https://badgen.net/github/stars/adrai/sessionstore?label=%E2%98%85 | |||
[![★][tch-nedb-session-image] tch-nedb-session][tch-nedb-session-url] A file system session store based on NeDB. | |||
[tch-nedb-session-url]: https://www.npmjs.com/package/tch-nedb-session | |||
[tch-nedb-session-image]: https://badgen.net/github/stars/tomaschyly/NeDBSession?label=%E2%98%85 | |||
## Example | |||
A simple example using `express-session` to store page views for a user. | |||
```js | |||
var express = require('express') | |||
var parseurl = require('parseurl') | |||
var session = require('express-session') | |||
var app = express() | |||
app.use(session({ | |||
secret: 'keyboard cat', | |||
resave: false, | |||
saveUninitialized: true | |||
})) | |||
app.use(function (req, res, next) { | |||
if (!req.session.views) { | |||
req.session.views = {} | |||
} | |||
// get the url pathname | |||
var pathname = parseurl(req).pathname | |||
// count the views | |||
req.session.views[pathname] = (req.session.views[pathname] || 0) + 1 | |||
next() | |||
}) | |||
app.get('/foo', function (req, res, next) { | |||
res.send('you viewed this page ' + req.session.views['/foo'] + ' times') | |||
}) | |||
app.get('/bar', function (req, res, next) { | |||
res.send('you viewed this page ' + req.session.views['/bar'] + ' times') | |||
}) | |||
``` | |||
## License | |||
[MIT](LICENSE) | |||
[rfc-6265bis-03-4.1.2.7]: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7 | |||
[coveralls-image]: https://badgen.net/coveralls/c/github/expressjs/session/master | |||
[coveralls-url]: https://coveralls.io/r/expressjs/session?branch=master | |||
[node-url]: https://nodejs.org/en/download | |||
[npm-downloads-image]: https://badgen.net/npm/dm/express-session | |||
[npm-url]: https://npmjs.org/package/express-session | |||
[npm-version-image]: https://badgen.net/npm/v/express-session | |||
[travis-image]: https://badgen.net/travis/expressjs/session/master | |||
[travis-url]: https://travis-ci.org/expressjs/session |
@ -1,674 +0,0 @@ | |||
/*! | |||
* express-session | |||
* Copyright(c) 2010 Sencha Inc. | |||
* Copyright(c) 2011 TJ Holowaychuk | |||
* Copyright(c) 2014-2015 Douglas Christopher Wilson | |||
* MIT Licensed | |||
*/ | |||
'use strict'; | |||
/** | |||
* Module dependencies. | |||
* @private | |||
*/ | |||
var Buffer = require('safe-buffer').Buffer | |||
var cookie = require('cookie'); | |||
var crypto = require('crypto') | |||
var debug = require('debug')('express-session'); | |||
var deprecate = require('depd')('express-session'); | |||
var onHeaders = require('on-headers') | |||
var parseUrl = require('parseurl'); | |||
var signature = require('cookie-signature') | |||
var uid = require('uid-safe').sync | |||
var Cookie = require('./session/cookie') | |||
var MemoryStore = require('./session/memory') | |||
var Session = require('./session/session') | |||
var Store = require('./session/store') | |||
// environment | |||
var env = process.env.NODE_ENV; | |||
/** | |||
* Expose the middleware. | |||
*/ | |||
exports = module.exports = session; | |||
/** | |||
* Expose constructors. | |||
*/ | |||
exports.Store = Store; | |||
exports.Cookie = Cookie; | |||
exports.Session = Session; | |||
exports.MemoryStore = MemoryStore; | |||
/** | |||
* Warning message for `MemoryStore` usage in production. | |||
* @private | |||
*/ | |||
var warning = 'Warning: connect.session() MemoryStore is not\n' | |||
+ 'designed for a production environment, as it will leak\n' | |||
+ 'memory, and will not scale past a single process.'; | |||
/** | |||
* Node.js 0.8+ async implementation. | |||
* @private | |||
*/ | |||
/* istanbul ignore next */ | |||
var defer = typeof setImmediate === 'function' | |||
? setImmediate | |||
: function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } | |||
/** | |||
* Setup session store with the given `options`. | |||
* | |||
* @param {Object} [options] | |||
* @param {Object} [options.cookie] Options for cookie | |||
* @param {Function} [options.genid] | |||
* @param {String} [options.name=connect.sid] Session ID cookie name | |||
* @param {Boolean} [options.proxy] | |||
* @param {Boolean} [options.resave] Resave unmodified sessions back to the store | |||
* @param {Boolean} [options.rolling] Enable/disable rolling session expiration | |||
* @param {Boolean} [options.saveUninitialized] Save uninitialized sessions to the store | |||
* @param {String|Array} [options.secret] Secret for signing session ID | |||
* @param {Object} [options.store=MemoryStore] Session store | |||
* @param {String} [options.unset] | |||
* @return {Function} middleware | |||
* @public | |||
*/ | |||
function session(options) { | |||
var opts = options || {} | |||
// get the cookie options | |||
var cookieOptions = opts.cookie || {} | |||
// get the session id generate function | |||
var generateId = opts.genid || generateSessionId | |||
// get the session cookie name | |||
var name = opts.name || opts.key || 'connect.sid' | |||
// get the session store | |||
var store = opts.store || new MemoryStore() | |||
// get the trust proxy setting | |||
var trustProxy = opts.proxy | |||
// get the resave session option | |||
var resaveSession = opts.resave; | |||
// get the rolling session option | |||
var rollingSessions = Boolean(opts.rolling) | |||
// get the save uninitialized session option | |||
var saveUninitializedSession = opts.saveUninitialized | |||
// get the cookie signing secret | |||
var secret = opts.secret | |||
if (typeof generateId !== 'function') { | |||
throw new TypeError('genid option must be a function'); | |||
} | |||
if (resaveSession === undefined) { | |||
deprecate('undefined resave option; provide resave option'); | |||
resaveSession = true; | |||
} | |||
if (saveUninitializedSession === undefined) { | |||
deprecate('undefined saveUninitialized option; provide saveUninitialized option'); | |||
saveUninitializedSession = true; | |||
} | |||
if (opts.unset && opts.unset !== 'destroy' && opts.unset !== 'keep') { | |||
throw new TypeError('unset option must be "destroy" or "keep"'); | |||
} | |||
// TODO: switch to "destroy" on next major | |||
var unsetDestroy = opts.unset === 'destroy' | |||
if (Array.isArray(secret) && secret.length === 0) { | |||
throw new TypeError('secret option array must contain one or more strings'); | |||
} | |||
if (secret && !Array.isArray(secret)) { | |||
secret = [secret]; | |||
} | |||
if (!secret) { | |||
deprecate('req.secret; provide secret option'); | |||
} | |||
// notify user that this store is not | |||
// meant for a production environment | |||
/* istanbul ignore next: not tested */ | |||
if (env === 'production' && store instanceof MemoryStore) { | |||
console.warn(warning); | |||
} | |||
// generates the new session | |||
store.generate = function(req){ | |||
req.sessionID = generateId(req); | |||
req.session = new Session(req); | |||
req.session.cookie = new Cookie(cookieOptions); | |||
if (cookieOptions.secure === 'auto') { | |||
req.session.cookie.secure = issecure(req, trustProxy); | |||
} | |||
}; | |||
var storeImplementsTouch = typeof store.touch === 'function'; | |||
// register event listeners for the store to track readiness | |||
var storeReady = true | |||
store.on('disconnect', function ondisconnect() { | |||
storeReady = false | |||
}) | |||
store.on('connect', function onconnect() { | |||
storeReady = true | |||
}) | |||
return function session(req, res, next) { | |||
// self-awareness | |||
if (req.session) { | |||
next() | |||
return | |||
} | |||
// Handle connection as if there is no session if | |||
// the store has temporarily disconnected etc | |||
if (!storeReady) { | |||
debug('store is disconnected') | |||
next() | |||
return | |||
} | |||
// pathname mismatch | |||
var originalPath = parseUrl.original(req).pathname || '/' | |||
if (originalPath.indexOf(cookieOptions.path || '/') !== 0) return next(); | |||
// ensure a secret is available or bail | |||
if (!secret && !req.secret) { | |||
next(new Error('secret option required for sessions')); | |||
return; | |||
} | |||
// backwards compatibility for signed cookies | |||
// req.secret is passed from the cookie parser middleware | |||
var secrets = secret || [req.secret]; | |||
var originalHash; | |||
var originalId; | |||
var savedHash; | |||
var touched = false | |||
// expose store | |||
req.sessionStore = store; | |||
// get the session ID from the cookie | |||
var cookieId = req.sessionID = getcookie(req, name, secrets); | |||
// set-cookie | |||
onHeaders(res, function(){ | |||
if (!req.session) { | |||
debug('no session'); | |||
return; | |||
} | |||
if (!shouldSetCookie(req)) { | |||
return; | |||
} | |||
// only send secure cookies via https | |||
if (req.session.cookie.secure && !issecure(req, trustProxy)) { | |||
debug('not secured'); | |||
return; | |||
} | |||
if (!touched) { | |||
// touch session | |||
req.session.touch() | |||
touched = true | |||
} | |||
// set cookie | |||
setcookie(res, name, req.sessionID, secrets[0], req.session.cookie.data); | |||
}); | |||
// proxy end() to commit the session | |||
var _end = res.end; | |||
var _write = res.write; | |||
var ended = false; | |||
res.end = function end(chunk, encoding) { | |||
if (ended) { | |||
return false; | |||
} | |||
ended = true; | |||
var ret; | |||
var sync = true; | |||
function writeend() { | |||
if (sync) { | |||
ret = _end.call(res, chunk, encoding); | |||
sync = false; | |||
return; | |||
} | |||
_end.call(res); | |||
} | |||
function writetop() { | |||
if (!sync) { | |||
return ret; | |||
} | |||
if (chunk == null) { | |||
ret = true; | |||
return ret; | |||
} | |||
var contentLength = Number(res.getHeader('Content-Length')); | |||
if (!isNaN(contentLength) && contentLength > 0) { | |||
// measure chunk | |||
chunk = !Buffer.isBuffer(chunk) | |||
? Buffer.from(chunk, encoding) | |||
: chunk; | |||
encoding = undefined; | |||
if (chunk.length !== 0) { | |||
debug('split response'); | |||
ret = _write.call(res, chunk.slice(0, chunk.length - 1)); | |||
chunk = chunk.slice(chunk.length - 1, chunk.length); | |||
return ret; | |||
} | |||
} | |||
ret = _write.call(res, chunk, encoding); | |||
sync = false; | |||
return ret; | |||
} | |||
if (shouldDestroy(req)) { | |||
// destroy session | |||
debug('destroying'); | |||
store.destroy(req.sessionID, function ondestroy(err) { | |||
if (err) { | |||
defer(next, err); | |||
} | |||
debug('destroyed'); | |||
writeend(); | |||
}); | |||
return writetop(); | |||
} | |||
// no session to save | |||
if (!req.session) { | |||
debug('no session'); | |||
return _end.call(res, chunk, encoding); | |||
} | |||
if (!touched) { | |||
// touch session | |||
req.session.touch() | |||
touched = true | |||
} | |||
if (shouldSave(req)) { | |||
req.session.save(function onsave(err) { | |||
if (err) { | |||
defer(next, err); | |||
} | |||
writeend(); | |||
}); | |||
return writetop(); | |||
} else if (storeImplementsTouch && shouldTouch(req)) { | |||
// store implements touch method | |||
debug('touching'); | |||
store.touch(req.sessionID, req.session, function ontouch(err) { | |||
if (err) { | |||
defer(next, err); | |||
} | |||
debug('touched'); | |||
writeend(); | |||
}); | |||
return writetop(); | |||
} | |||
return _end.call(res, chunk, encoding); | |||
}; | |||
// generate the session | |||
function generate() { | |||
store.generate(req); | |||
originalId = req.sessionID; | |||
originalHash = hash(req.session); | |||
wrapmethods(req.session); | |||
} | |||
// inflate the session | |||
function inflate (req, sess) { | |||
store.createSession(req, sess) | |||
originalId = req.sessionID | |||
originalHash = hash(sess) | |||
if (!resaveSession) { | |||
savedHash = originalHash | |||
} | |||
wrapmethods(req.session) | |||
} | |||
// wrap session methods | |||
function wrapmethods(sess) { | |||
var _reload = sess.reload | |||
var _save = sess.save; | |||
function reload(callback) { | |||
debug('reloading %s', this.id) | |||
_reload.call(this, function () { | |||
wrapmethods(req.session) | |||
callback.apply(this, arguments) | |||
}) | |||
} | |||
function save() { | |||
debug('saving %s', this.id); | |||
savedHash = hash(this); | |||
_save.apply(this, arguments); | |||
} | |||
Object.defineProperty(sess, 'reload', { | |||
configurable: true, | |||
enumerable: false, | |||
value: reload, | |||
writable: true | |||
}) | |||
Object.defineProperty(sess, 'save', { | |||
configurable: true, | |||
enumerable: false, | |||
value: save, | |||
writable: true | |||
}); | |||
} | |||
// check if session has been modified | |||
function isModified(sess) { | |||
return originalId !== sess.id || originalHash !== hash(sess); | |||
} | |||
// check if session has been saved | |||
function isSaved(sess) { | |||
return originalId === sess.id && savedHash === hash(sess); | |||
} | |||
// determine if session should be destroyed | |||
function shouldDestroy(req) { | |||
return req.sessionID && unsetDestroy && req.session == null; | |||
} | |||
// determine if session should be saved to store | |||
function shouldSave(req) { | |||
// cannot set cookie without a session ID | |||
if (typeof req.sessionID !== 'string') { | |||
debug('session ignored because of bogus req.sessionID %o', req.sessionID); | |||
return false; | |||
} | |||
return !saveUninitializedSession && cookieId !== req.sessionID | |||
? isModified(req.session) | |||
: !isSaved(req.session) | |||
} | |||
// determine if session should be touched | |||
function shouldTouch(req) { | |||
// cannot set cookie without a session ID | |||
if (typeof req.sessionID !== 'string') { | |||
debug('session ignored because of bogus req.sessionID %o', req.sessionID); | |||
return false; | |||
} | |||
return cookieId === req.sessionID && !shouldSave(req); | |||
} | |||
// determine if cookie should be set on response | |||
function shouldSetCookie(req) { | |||
// cannot set cookie without a session ID | |||
if (typeof req.sessionID !== 'string') { | |||
return false; | |||
} | |||
return cookieId !== req.sessionID | |||
? saveUninitializedSession || isModified(req.session) | |||
: rollingSessions || req.session.cookie.expires != null && isModified(req.session); | |||
} | |||
// generate a session if the browser doesn't send a sessionID | |||
if (!req.sessionID) { | |||
debug('no SID sent, generating session'); | |||
generate(); | |||
next(); | |||
return; | |||
} | |||
// generate the session object | |||
debug('fetching %s', req.sessionID); | |||
store.get(req.sessionID, function(err, sess){ | |||
// error handling | |||
if (err && err.code !== 'ENOENT') { | |||
debug('error %j', err); | |||
next(err) | |||
return | |||
} | |||
try { | |||
if (err || !sess) { | |||
debug('no session found') | |||
generate() | |||
} else { | |||
debug('session found') | |||
inflate(req, sess) | |||
} | |||
} catch (e) { | |||
next(e) | |||
return | |||
} | |||
next() | |||
}); | |||
}; | |||
}; | |||
/** | |||
* Generate a session ID for a new session. | |||
* | |||
* @return {String} | |||
* @private | |||
*/ | |||
function generateSessionId(sess) { | |||
return uid(24); | |||
} | |||
/** | |||
* Get the session ID cookie from request. | |||
* | |||
* @return {string} | |||
* @private | |||
*/ | |||
function getcookie(req, name, secrets) { | |||
var header = req.headers.cookie; | |||
var raw; | |||
var val; | |||
// read from cookie header | |||
if (header) { | |||
var cookies = cookie.parse(header); | |||
raw = cookies[name]; | |||
if (raw) { | |||
if (raw.substr(0, 2) === 's:') { | |||
val = unsigncookie(raw.slice(2), secrets); | |||
if (val === false) { | |||
debug('cookie signature invalid'); | |||
val = undefined; | |||
} | |||
} else { | |||
debug('cookie unsigned') | |||
} | |||
} | |||
} | |||
// back-compat read from cookieParser() signedCookies data | |||
if (!val && req.signedCookies) { | |||
val = req.signedCookies[name]; | |||
if (val) { | |||
deprecate('cookie should be available in req.headers.cookie'); | |||
} | |||
} | |||
// back-compat read from cookieParser() cookies data | |||
if (!val && req.cookies) { | |||
raw = req.cookies[name]; | |||
if (raw) { | |||
if (raw.substr(0, 2) === 's:') { | |||
val = unsigncookie(raw.slice(2), secrets); | |||
if (val) { | |||
deprecate('cookie should be available in req.headers.cookie'); | |||
} | |||
if (val === false) { | |||
debug('cookie signature invalid'); | |||
val = undefined; | |||
} | |||
} else { | |||
debug('cookie unsigned') | |||
} | |||
} | |||
} | |||
return val; | |||
} | |||
/** | |||
* Hash the given `sess` object omitting changes to `.cookie`. | |||
* | |||
* @param {Object} sess | |||
* @return {String} | |||
* @private | |||
*/ | |||
function hash(sess) { | |||
// serialize | |||
var str = JSON.stringify(sess, function (key, val) { | |||
// ignore sess.cookie property | |||
if (this === sess && key === 'cookie') { | |||
return | |||
} | |||
return val | |||
}) | |||
// hash | |||
return crypto | |||
.createHash('sha1') | |||
.update(str, 'utf8') | |||
.digest('hex') | |||
} | |||
/** | |||
* Determine if request is secure. | |||
* | |||
* @param {Object} req | |||
* @param {Boolean} [trustProxy] | |||
* @return {Boolean} | |||
* @private | |||
*/ | |||
function issecure(req, trustProxy) { | |||
// socket is https server | |||
if (req.connection && req.connection.encrypted) { | |||
return true; | |||
} | |||
// do not trust proxy | |||
if (trustProxy === false) { | |||
return false; | |||
} | |||
// no explicit trust; try req.secure from express | |||
if (trustProxy !== true) { | |||
return req.secure === true | |||
} | |||
// read the proto from x-forwarded-proto header | |||
var header = req.headers['x-forwarded-proto'] || ''; | |||
var index = header.indexOf(','); | |||
var proto = index !== -1 | |||
? header.substr(0, index).toLowerCase().trim() | |||
: header.toLowerCase().trim() | |||
return proto === 'https'; | |||
} | |||
/** | |||
* Set cookie on response. | |||
* | |||
* @private | |||
*/ | |||
function setcookie(res, name, val, secret, options) { | |||
var signed = 's:' + signature.sign(val, secret); | |||
var data = cookie.serialize(name, signed, options); | |||
debug('set-cookie %s', data); | |||
var prev = res.getHeader('Set-Cookie') || [] | |||
var header = Array.isArray(prev) ? prev.concat(data) : [prev, data]; | |||
res.setHeader('Set-Cookie', header) | |||
} | |||
/** | |||
* Verify and decode the given `val` with `secrets`. | |||
* | |||
* @param {String} val | |||
* @param {Array} secrets | |||
* @returns {String|Boolean} | |||
* @private | |||
*/ | |||
function unsigncookie(val, secrets) { | |||
for (var i = 0; i < secrets.length; i++) { | |||
var result = signature.unsign(val, secrets[i]); | |||
if (result !== false) { | |||
return result; | |||
} | |||
} | |||
return false; | |||
} |
@ -1,103 +0,0 @@ | |||
2.0.0 / 2018-10-26 | |||
================== | |||
* Drop support for Node.js 0.6 | |||
* Replace internal `eval` usage with `Function` constructor | |||
* Use instance methods on `process` to check for listeners | |||
1.1.2 / 2018-01-11 | |||
================== | |||
* perf: remove argument reassignment | |||
* Support Node.js 0.6 to 9.x | |||
1.1.1 / 2017-07-27 | |||
================== | |||
* Remove unnecessary `Buffer` loading | |||
* Support Node.js 0.6 to 8.x | |||
1.1.0 / 2015-09-14 | |||
================== | |||
* Enable strict mode in more places | |||
* Support io.js 3.x | |||
* Support io.js 2.x | |||
* Support web browser loading | |||
- Requires bundler like Browserify or webpack | |||
1.0.1 / 2015-04-07 | |||
================== | |||
* Fix `TypeError`s when under `'use strict'` code | |||
* Fix useless type name on auto-generated messages | |||
* Support io.js 1.x | |||
* Support Node.js 0.12 | |||
1.0.0 / 2014-09-17 | |||
================== | |||
* No changes | |||
0.4.5 / 2014-09-09 | |||
================== | |||
* Improve call speed to functions using the function wrapper | |||
* Support Node.js 0.6 | |||
0.4.4 / 2014-07-27 | |||
================== | |||
* Work-around v8 generating empty stack traces | |||
0.4.3 / 2014-07-26 | |||
================== | |||
* Fix exception when global `Error.stackTraceLimit` is too low | |||
0.4.2 / 2014-07-19 | |||
================== | |||
* Correct call site for wrapped functions and properties | |||
0.4.1 / 2014-07-19 | |||
================== | |||
* Improve automatic message generation for function properties | |||
0.4.0 / 2014-07-19 | |||
================== | |||
* Add `TRACE_DEPRECATION` environment variable | |||
* Remove non-standard grey color from color output | |||
* Support `--no-deprecation` argument | |||
* Support `--trace-deprecation` argument | |||
* Support `deprecate.property(fn, prop, message)` | |||
0.3.0 / 2014-06-16 | |||
================== | |||
* Add `NO_DEPRECATION` environment variable | |||
0.2.0 / 2014-06-15 | |||
================== | |||
* Add `deprecate.property(obj, prop, message)` | |||
* Remove `supports-color` dependency for node.js 0.8 | |||
0.1.0 / 2014-06-15 | |||
================== | |||
* Add `deprecate.function(fn, message)` | |||
* Add `process.on('deprecation', fn)` emitter | |||
* Automatically generate message when omitted from `deprecate()` | |||
0.0.1 / 2014-06-15 | |||
================== | |||
* Fix warning for dynamic calls at singe call site | |||
0.0.0 / 2014-06-15 | |||
================== | |||
* Initial implementation |
@ -1,22 +0,0 @@ | |||
(The MIT License) | |||
Copyright (c) 2014-2018 Douglas Christopher Wilson | |||
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 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. |
@ -1,280 +0,0 @@ | |||
# depd | |||
[![NPM Version][npm-version-image]][npm-url] | |||
[![NPM Downloads][npm-downloads-image]][npm-url] | |||
[![Node.js Version][node-image]][node-url] | |||
[![Linux Build][travis-image]][travis-url] | |||
[![Windows Build][appveyor-image]][appveyor-url] | |||
[![Coverage Status][coveralls-image]][coveralls-url] | |||
Deprecate all the things | |||
> With great modules comes great responsibility; mark things deprecated! | |||
## Install | |||
This module is installed directly using `npm`: | |||
```sh | |||
$ npm install depd | |||
``` | |||
This module can also be bundled with systems like | |||
[Browserify](http://browserify.org/) or [webpack](https://webpack.github.io/), | |||
though by default this module will alter it's API to no longer display or | |||
track deprecations. | |||
## API | |||
<!-- eslint-disable no-unused-vars --> | |||
```js | |||
var deprecate = require('depd')('my-module') | |||
``` | |||
This library allows you to display deprecation messages to your users. | |||
This library goes above and beyond with deprecation warnings by | |||
introspection of the call stack (but only the bits that it is interested | |||
in). | |||
Instead of just warning on the first invocation of a deprecated | |||
function and never again, this module will warn on the first invocation | |||
of a deprecated function per unique call site, making it ideal to alert | |||
users of all deprecated uses across the code base, rather than just | |||
whatever happens to execute first. | |||
The deprecation warnings from this module also include the file and line | |||
information for the call into the module that the deprecated function was | |||
in. | |||
**NOTE** this library has a similar interface to the `debug` module, and | |||
this module uses the calling file to get the boundary for the call stacks, | |||
so you should always create a new `deprecate` object in each file and not | |||
within some central file. | |||
### depd(namespace) | |||
Create a new deprecate function that uses the given namespace name in the | |||
messages and will display the call site prior to the stack entering the | |||
file this function was called from. It is highly suggested you use the | |||
name of your module as the namespace. | |||
### deprecate(message) | |||
Call this function from deprecated code to display a deprecation message. | |||
This message will appear once per unique caller site. Caller site is the | |||
first call site in the stack in a different file from the caller of this | |||
function. | |||
If the message is omitted, a message is generated for you based on the site | |||
of the `deprecate()` call and will display the name of the function called, | |||
similar to the name displayed in a stack trace. | |||
### deprecate.function(fn, message) | |||
Call this function to wrap a given function in a deprecation message on any | |||
call to the function. An optional message can be supplied to provide a custom | |||
message. | |||
### deprecate.property(obj, prop, message) | |||
Call this function to wrap a given property on object in a deprecation message | |||
on any accessing or setting of the property. An optional message can be supplied | |||
to provide a custom message. | |||
The method must be called on the object where the property belongs (not | |||
inherited from the prototype). | |||
If the property is a data descriptor, it will be converted to an accessor | |||
descriptor in order to display the deprecation message. | |||
### process.on('deprecation', fn) | |||
This module will allow easy capturing of deprecation errors by emitting the | |||
errors as the type "deprecation" on the global `process`. If there are no | |||
listeners for this type, the errors are written to STDERR as normal, but if | |||
there are any listeners, nothing will be written to STDERR and instead only | |||
emitted. From there, you can write the errors in a different format or to a | |||
logging source. | |||
The error represents the deprecation and is emitted only once with the same | |||
rules as writing to STDERR. The error has the following properties: | |||
- `message` - This is the message given by the library | |||
- `name` - This is always `'DeprecationError'` | |||
- `namespace` - This is the namespace the deprecation came from | |||
- `stack` - This is the stack of the call to the deprecated thing | |||
Example `error.stack` output: | |||
``` | |||
DeprecationError: my-cool-module deprecated oldfunction | |||
at Object.<anonymous> ([eval]-wrapper:6:22) | |||
at Module._compile (module.js:456:26) | |||
at evalScript (node.js:532:25) | |||
at startup (node.js:80:7) | |||
at node.js:902:3 | |||
``` | |||
### process.env.NO_DEPRECATION | |||
As a user of modules that are deprecated, the environment variable `NO_DEPRECATION` | |||
is provided as a quick solution to silencing deprecation warnings from being | |||
output. The format of this is similar to that of `DEBUG`: | |||
```sh | |||
$ NO_DEPRECATION=my-module,othermod node app.js | |||
``` | |||
This will suppress deprecations from being output for "my-module" and "othermod". | |||
The value is a list of comma-separated namespaces. To suppress every warning | |||
across all namespaces, use the value `*` for a namespace. | |||
Providing the argument `--no-deprecation` to the `node` executable will suppress | |||
all deprecations (only available in Node.js 0.8 or higher). | |||
**NOTE** This will not suppress the deperecations given to any "deprecation" | |||
event listeners, just the output to STDERR. | |||
### process.env.TRACE_DEPRECATION | |||
As a user of modules that are deprecated, the environment variable `TRACE_DEPRECATION` | |||
is provided as a solution to getting more detailed location information in deprecation | |||
warnings by including the entire stack trace. The format of this is the same as | |||
`NO_DEPRECATION`: | |||
```sh | |||
$ TRACE_DEPRECATION=my-module,othermod node app.js | |||
``` | |||
This will include stack traces for deprecations being output for "my-module" and | |||
"othermod". The value is a list of comma-separated namespaces. To trace every | |||
warning across all namespaces, use the value `*` for a namespace. | |||
Providing the argument `--trace-deprecation` to the `node` executable will trace | |||
all deprecations (only available in Node.js 0.8 or higher). | |||
**NOTE** This will not trace the deperecations silenced by `NO_DEPRECATION`. | |||
## Display | |||
 | |||
When a user calls a function in your library that you mark deprecated, they | |||
will see the following written to STDERR (in the given colors, similar colors | |||
and layout to the `debug` module): | |||
``` | |||
bright cyan bright yellow | |||
| | reset cyan | |||
| | | | | |||
▼ ▼ ▼ ▼ | |||
my-cool-module deprecated oldfunction [eval]-wrapper:6:22 | |||
▲ ▲ ▲ ▲ | |||
| | | | | |||
namespace | | location of mycoolmod.oldfunction() call | |||
| deprecation message | |||
the word "deprecated" | |||
``` | |||
If the user redirects their STDERR to a file or somewhere that does not support | |||
colors, they see (similar layout to the `debug` module): | |||
``` | |||
Sun, 15 Jun 2014 05:21:37 GMT my-cool-module deprecated oldfunction at [eval]-wrapper:6:22 | |||
▲ ▲ ▲ ▲ ▲ | |||
| | | | | | |||
timestamp of message namespace | | location of mycoolmod.oldfunction() call | |||
| deprecation message | |||
the word "deprecated" | |||
``` | |||
## Examples | |||
### Deprecating all calls to a function | |||
This will display a deprecated message about "oldfunction" being deprecated | |||
from "my-module" on STDERR. | |||
```js | |||
var deprecate = require('depd')('my-cool-module') | |||
// message automatically derived from function name | |||
// Object.oldfunction | |||
exports.oldfunction = deprecate.function(function oldfunction () { | |||
// all calls to function are deprecated | |||
}) | |||
// specific message | |||
exports.oldfunction = deprecate.function(function () { | |||
// all calls to function are deprecated | |||
}, 'oldfunction') | |||
``` | |||
### Conditionally deprecating a function call | |||
This will display a deprecated message about "weirdfunction" being deprecated | |||
from "my-module" on STDERR when called with less than 2 arguments. | |||
```js | |||
var deprecate = require('depd')('my-cool-module') | |||
exports.weirdfunction = function () { | |||
if (arguments.length < 2) { | |||
// calls with 0 or 1 args are deprecated | |||
deprecate('weirdfunction args < 2') | |||
} | |||
} | |||
``` | |||
When calling `deprecate` as a function, the warning is counted per call site | |||
within your own module, so you can display different deprecations depending | |||
on different situations and the users will still get all the warnings: | |||
```js | |||
var deprecate = require('depd')('my-cool-module') | |||
exports.weirdfunction = function () { | |||
if (arguments.length < 2) { | |||
// calls with 0 or 1 args are deprecated | |||
deprecate('weirdfunction args < 2') | |||
} else if (typeof arguments[0] !== 'string') { | |||
// calls with non-string first argument are deprecated | |||
deprecate('weirdfunction non-string first arg') | |||
} | |||
} | |||
``` | |||
### Deprecating property access | |||
This will display a deprecated message about "oldprop" being deprecated | |||
from "my-module" on STDERR when accessed. A deprecation will be displayed | |||
when setting the value and when getting the value. | |||
```js | |||
var deprecate = require('depd')('my-cool-module') | |||
exports.oldprop = 'something' | |||
// message automatically derives from property name | |||
deprecate.property(exports, 'oldprop') | |||
// explicit message | |||
deprecate.property(exports, 'oldprop', 'oldprop >= 0.10') | |||
``` | |||
## License | |||
[MIT](LICENSE) | |||
[appveyor-image]: https://badgen.net/appveyor/ci/dougwilson/nodejs-depd/master?label=windows | |||
[appveyor-url]: https://ci.appveyor.com/project/dougwilson/nodejs-depd | |||
[coveralls-image]: https://badgen.net/coveralls/c/github/dougwilson/nodejs-depd/master | |||
[coveralls-url]: https://coveralls.io/r/dougwilson/nodejs-depd?branch=master | |||
[node-image]: https://badgen.net/npm/node/depd | |||
[node-url]: https://nodejs.org/en/download/ | |||
[npm-downloads-image]: https://badgen.net/npm/dm/depd | |||
[npm-url]: https://npmjs.org/package/depd | |||
[npm-version-image]: https://badgen.net/npm/v/depd | |||
[travis-image]: https://badgen.net/travis/dougwilson/nodejs-depd/master?label=linux | |||
[travis-url]: https://travis-ci.org/dougwilson/nodejs-depd |
@ -1,538 +0,0 @@ | |||
/*! | |||
* depd | |||
* Copyright(c) 2014-2018 Douglas Christopher Wilson | |||
* MIT Licensed | |||
*/ | |||
/** | |||
* Module dependencies. | |||
*/ | |||
var relative = require('path').relative | |||
/** | |||
* Module exports. | |||
*/ | |||
module.exports = depd | |||
/** | |||
* Get the path to base files on. | |||
*/ | |||
var basePath = process.cwd() | |||
/** | |||
* Determine if namespace is contained in the string. | |||
*/ | |||
function containsNamespace (str, namespace) { | |||
var vals = str.split(/[ ,]+/) | |||
var ns = String(namespace).toLowerCase() | |||
for (var i = 0; i < vals.length; i++) { | |||
var val = vals[i] | |||
// namespace contained | |||
if (val && (val === '*' || val.toLowerCase() === ns)) { | |||
return true | |||
} | |||
} | |||
return false | |||
} | |||
/** | |||
* Convert a data descriptor to accessor descriptor. | |||
*/ | |||
function convertDataDescriptorToAccessor (obj, prop, message) { | |||
var descriptor = Object.getOwnPropertyDescriptor(obj, prop) | |||
var value = descriptor.value | |||
descriptor.get = function getter () { return value } | |||
if (descriptor.writable) { | |||
descriptor.set = function setter (val) { return (value = val) } | |||
} | |||
delete descriptor.value | |||
delete descriptor.writable | |||
Object.defineProperty(obj, prop, descriptor) | |||
return descriptor | |||
} | |||
/** | |||
* Create arguments string to keep arity. | |||
*/ | |||
function createArgumentsString (arity) { | |||
var str = '' | |||
for (var i = 0; i < arity; i++) { | |||
str += ', arg' + i | |||
} | |||
return str.substr(2) | |||
} | |||
/** | |||
* Create stack string from stack. | |||
*/ | |||
function createStackString (stack) { | |||
var str = this.name + ': ' + this.namespace | |||
if (this.message) { | |||
str += ' deprecated ' + this.message | |||
} | |||
for (var i = 0; i < stack.length; i++) { | |||
str += '\n at ' + stack[i].toString() | |||
} | |||
return str | |||
} | |||
/** | |||
* Create deprecate for namespace in caller. | |||
*/ | |||
function depd (namespace) { | |||
if (!namespace) { | |||
throw new TypeError('argument namespace is required') | |||
} | |||
var stack = getStack() | |||
var site = callSiteLocation(stack[1]) | |||
var file = site[0] | |||
function deprecate (message) { | |||
// call to self as log | |||
log.call(deprecate, message) | |||
} | |||
deprecate._file = file | |||
deprecate._ignored = isignored(namespace) | |||
deprecate._namespace = namespace | |||
deprecate._traced = istraced(namespace) | |||
deprecate._warned = Object.create(null) | |||
deprecate.function = wrapfunction | |||
deprecate.property = wrapproperty | |||
return deprecate | |||
} | |||
/** | |||
* Determine if event emitter has listeners of a given type. | |||
* | |||
* The way to do this check is done three different ways in Node.js >= 0.8 | |||
* so this consolidates them into a minimal set using instance methods. | |||
* | |||
* @param {EventEmitter} emitter | |||
* @param {string} type | |||
* @returns {boolean} | |||
* @private | |||
*/ | |||
function eehaslisteners (emitter, type) { | |||
var count = typeof emitter.listenerCount !== 'function' | |||
? emitter.listeners(type).length | |||
: emitter.listenerCount(type) | |||
return count > 0 | |||
} | |||
/** | |||
* Determine if namespace is ignored. | |||
*/ | |||
function isignored (namespace) { | |||
if (process.noDeprecation) { | |||
// --no-deprecation support | |||
return true | |||
} | |||
var str = process.env.NO_DEPRECATION || '' | |||
// namespace ignored | |||
return containsNamespace(str, namespace) | |||
} | |||
/** | |||
* Determine if namespace is traced. | |||
*/ | |||
function istraced (namespace) { | |||
if (process.traceDeprecation) { | |||
// --trace-deprecation support | |||
return true | |||
} | |||
var str = process.env.TRACE_DEPRECATION || '' | |||
// namespace traced | |||
return containsNamespace(str, namespace) | |||
} | |||
/** | |||
* Display deprecation message. | |||
*/ | |||
function log (message, site) { | |||
var haslisteners = eehaslisteners(process, 'deprecation') | |||
// abort early if no destination | |||
if (!haslisteners && this._ignored) { | |||
return | |||
} | |||
var caller | |||
var callFile | |||
var callSite | |||
var depSite | |||
var i = 0 | |||
var seen = false | |||
var stack = getStack() | |||
var file = this._file | |||
if (site) { | |||
// provided site | |||
depSite = site | |||
callSite = callSiteLocation(stack[1]) | |||
callSite.name = depSite.name | |||
file = callSite[0] | |||
} else { | |||
// get call site | |||
i = 2 | |||
depSite = callSiteLocation(stack[i]) | |||
callSite = depSite | |||
} | |||
// get caller of deprecated thing in relation to file | |||
for (; i < stack.length; i++) { | |||
caller = callSiteLocation(stack[i]) | |||
callFile = caller[0] | |||
if (callFile === file) { | |||
seen = true | |||
} else if (callFile === this._file) { | |||
file = this._file | |||
} else if (seen) { | |||
break | |||
} | |||
} | |||
var key = caller | |||
? depSite.join(':') + '__' + caller.join(':') | |||
: undefined | |||
if (key !== undefined && key in this._warned) { | |||
// already warned | |||
return | |||
} | |||
this._warned[key] = true | |||
// generate automatic message from call site | |||
var msg = message | |||
if (!msg) { | |||
msg = callSite === depSite || !callSite.name | |||
? defaultMessage(depSite) | |||
: defaultMessage(callSite) | |||
} | |||
// emit deprecation if listeners exist | |||
if (haslisteners) { | |||
var err = DeprecationError(this._namespace, msg, stack.slice(i)) | |||
process.emit('deprecation', err) | |||
return | |||
} | |||
// format and write message | |||
var format = process.stderr.isTTY | |||
? formatColor | |||
: formatPlain | |||
var output = format.call(this, msg, caller, stack.slice(i)) | |||
process.stderr.write(output + '\n', 'utf8') | |||
} | |||
/** | |||
* Get call site location as array. | |||
*/ | |||
function callSiteLocation (callSite) { | |||
var file = callSite.getFileName() || '<anonymous>' | |||
var line = callSite.getLineNumber() | |||
var colm = callSite.getColumnNumber() | |||
if (callSite.isEval()) { | |||
file = callSite.getEvalOrigin() + ', ' + file | |||
} | |||
var site = [file, line, colm] | |||
site.callSite = callSite | |||
site.name = callSite.getFunctionName() | |||
return site | |||
} | |||
/** | |||
* Generate a default message from the site. | |||
*/ | |||
function defaultMessage (site) { | |||
var callSite = site.callSite | |||
var funcName = site.name | |||
// make useful anonymous name | |||
if (!funcName) { | |||
funcName = '<anonymous@' + formatLocation(site) + '>' | |||
} | |||
var context = callSite.getThis() | |||
var typeName = context && callSite.getTypeName() | |||
// ignore useless type name | |||
if (typeName === 'Object') { | |||
typeName = undefined | |||
} | |||
// make useful type name | |||
if (typeName === 'Function') { | |||
typeName = context.name || typeName | |||
} | |||
return typeName && callSite.getMethodName() | |||
? typeName + '.' + funcName | |||
: funcName | |||
} | |||
/** | |||
* Format deprecation message without color. | |||
*/ | |||
function formatPlain (msg, caller, stack) { | |||
var timestamp = new Date().toUTCString() | |||
var formatted = timestamp + | |||
' ' + this._namespace + | |||
' deprecated ' + msg | |||
// add stack trace | |||
if (this._traced) { | |||
for (var i = 0; i < stack.length; i++) { | |||
formatted += '\n at ' + stack[i].toString() | |||
} | |||
return formatted | |||
} | |||
if (caller) { | |||
formatted += ' at ' + formatLocation(caller) | |||
} | |||
return formatted | |||
} | |||
/** | |||
* Format deprecation message with color. | |||
*/ | |||
function formatColor (msg, caller, stack) { | |||
var formatted = '\x1b[36;1m' + this._namespace + '\x1b[22;39m' + // bold cyan | |||
' \x1b[33;1mdeprecated\x1b[22;39m' + // bold yellow | |||
' \x1b[0m' + msg + '\x1b[39m' // reset | |||
// add stack trace | |||
if (this._traced) { | |||
for (var i = 0; i < stack.length; i++) { | |||
formatted += '\n \x1b[36mat ' + stack[i].toString() + '\x1b[39m' // cyan | |||
} | |||
return formatted | |||
} | |||
if (caller) { | |||
formatted += ' \x1b[36m' + formatLocation(caller) + '\x1b[39m' // cyan | |||
} | |||
return formatted | |||
} | |||
/** | |||
* Format call site location. | |||
*/ | |||
function formatLocation (callSite) { | |||
return relative(basePath, callSite[0]) + | |||
':' + callSite[1] + | |||
':' + callSite[2] | |||
} | |||
/** | |||
* Get the stack as array of call sites. | |||
*/ | |||
function getStack () { | |||
var limit = Error.stackTraceLimit | |||
var obj = {} | |||
var prep = Error.prepareStackTrace | |||
Error.prepareStackTrace = prepareObjectStackTrace | |||
Error.stackTraceLimit = Math.max(10, limit) | |||
// capture the stack | |||
Error.captureStackTrace(obj) | |||
// slice this function off the top | |||
var stack = obj.stack.slice(1) | |||
Error.prepareStackTrace = prep | |||
Error.stackTraceLimit = limit | |||
return stack | |||
} | |||
/** | |||
* Capture call site stack from v8. | |||
*/ | |||
function prepareObjectStackTrace (obj, stack) { | |||
return stack | |||
} | |||
/** | |||
* Return a wrapped function in a deprecation message. | |||
*/ | |||
function wrapfunction (fn, message) { | |||
if (typeof fn !== 'function') { | |||
throw new TypeError('argument fn must be a function') | |||
} | |||
var args = createArgumentsString(fn.length) | |||
var stack = getStack() | |||
var site = callSiteLocation(stack[1]) | |||
site.name = fn.name | |||
// eslint-disable-next-line no-new-func | |||
var deprecatedfn = new Function('fn', 'log', 'deprecate', 'message', 'site', | |||
'"use strict"\n' + | |||
'return function (' + args + ') {' + | |||
'log.call(deprecate, message, site)\n' + | |||
'return fn.apply(this, arguments)\n' + | |||
'}')(fn, log, this, message, site) | |||
return deprecatedfn | |||
} | |||
/** | |||
* Wrap property in a deprecation message. | |||
*/ | |||
function wrapproperty (obj, prop, message) { | |||
if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) { | |||
throw new TypeError('argument obj must be object') | |||
} | |||
var descriptor = Object.getOwnPropertyDescriptor(obj, prop) | |||
if (!descriptor) { | |||
throw new TypeError('must call property on owner object') | |||
} | |||
if (!descriptor.configurable) { | |||
throw new TypeError('property must be configurable') | |||
} | |||
var deprecate = this | |||
var stack = getStack() | |||
var site = callSiteLocation(stack[1]) | |||
// set site name | |||
site.name = prop | |||
// convert data descriptor | |||
if ('value' in descriptor) { | |||
descriptor = convertDataDescriptorToAccessor(obj, prop, message) | |||
} | |||
var get = descriptor.get | |||
var set = descriptor.set | |||
// wrap getter | |||
if (typeof get === 'function') { | |||
descriptor.get = function getter () { | |||
log.call(deprecate, message, site) | |||
return get.apply(this, arguments) | |||
} | |||
} | |||
// wrap setter | |||
if (typeof set === 'function') { | |||
descriptor.set = function setter () { | |||
log.call(deprecate, message, site) | |||
return set.apply(this, arguments) | |||
} | |||
} | |||
Object.defineProperty(obj, prop, descriptor) | |||
} | |||
/** | |||
* Create DeprecationError for deprecation | |||
*/ | |||
function DeprecationError (namespace, message, stack) { | |||
var error = new Error() | |||
var stackString | |||
Object.defineProperty(error, 'constructor', { | |||
value: DeprecationError | |||
}) | |||
Object.defineProperty(error, 'message', { | |||
configurable: true, | |||
enumerable: false, | |||
value: message, | |||
writable: true | |||
}) | |||
Object.defineProperty(error, 'name', { | |||
enumerable: false, | |||
configurable: true, | |||
value: 'DeprecationError', | |||
writable: true | |||
}) | |||
Object.defineProperty(error, 'namespace', { | |||
configurable: true, | |||
enumerable: false, | |||
value: namespace, | |||
writable: true | |||
}) | |||
Object.defineProperty(error, 'stack', { | |||
configurable: true, | |||
enumerable: false, | |||
get: function () { | |||
if (stackString !== undefined) { | |||
return stackString | |||
} | |||
// prepare stack trace | |||
return (stackString = createStackString.call(this, stack)) | |||
}, | |||
set: function setter (val) { | |||
stackString = val | |||
} | |||
}) | |||
return error | |||
} |
@ -1,77 +0,0 @@ | |||
/*! | |||
* depd | |||
* Copyright(c) 2015 Douglas Christopher Wilson | |||
* MIT Licensed | |||
*/ | |||
'use strict' | |||
/** | |||
* Module exports. | |||
* @public | |||
*/ | |||
module.exports = depd | |||
/** | |||
* Create deprecate for namespace in caller. | |||
*/ | |||
function depd (namespace) { | |||
if (!namespace) { | |||
throw new TypeError('argument namespace is required') | |||
} | |||
function deprecate (message) { | |||
// no-op in browser | |||
} | |||
deprecate._file = undefined | |||
deprecate._ignored = true | |||
deprecate._namespace = namespace | |||
deprecate._traced = false | |||
deprecate._warned = Object.create(null) | |||
deprecate.function = wrapfunction | |||
deprecate.property = wrapproperty | |||
return deprecate | |||
} | |||
/** | |||
* Return a wrapped function in a deprecation message. | |||
* | |||
* This is a no-op version of the wrapper, which does nothing but call | |||
* validation. | |||
*/ | |||
function wrapfunction (fn, message) { | |||
if (typeof fn !== 'function') { | |||
throw new TypeError('argument fn must be a function') | |||
} | |||
return fn | |||
} | |||
/** | |||
* Wrap property in a deprecation message. | |||
* | |||
* This is a no-op version of the wrapper, which does nothing but call | |||
* validation. | |||
*/ | |||
function wrapproperty (obj, prop, message) { | |||
if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) { | |||
throw new TypeError('argument obj must be object') | |||
} | |||
var descriptor = Object.getOwnPropertyDescriptor(obj, prop) | |||
if (!descriptor) { | |||
throw new TypeError('must call property on owner object') | |||
} | |||
if (!descriptor.configurable) { | |||
throw new TypeError('property must be configurable') | |||
} | |||
} |
@ -1,45 +0,0 @@ | |||
{ | |||
"name": "depd", | |||
"description": "Deprecate all the things", | |||
"version": "2.0.0", | |||
"author": "Douglas Christopher Wilson <doug@somethingdoug.com>", | |||
"license": "MIT", | |||
"keywords": [ | |||
"deprecate", | |||
"deprecated" | |||
], | |||
"repository": "dougwilson/nodejs-depd", | |||
"browser": "lib/browser/index.js", | |||
"devDependencies": { | |||
"benchmark": "2.1.4", | |||
"beautify-benchmark": "0.2.4", | |||
"eslint": "5.7.0", | |||
"eslint-config-standard": "12.0.0", | |||
"eslint-plugin-import": "2.14.0", | |||
"eslint-plugin-markdown": "1.0.0-beta.7", | |||
"eslint-plugin-node": "7.0.1", | |||
"eslint-plugin-promise": "4.0.1", | |||
"eslint-plugin-standard": "4.0.0", | |||
"istanbul": "0.4.5", | |||
"mocha": "5.2.0", | |||
"safe-buffer": "5.1.2", | |||
"uid-safe": "2.1.5" | |||
}, | |||
"files": [ | |||
"lib/", | |||
"History.md", | |||
"LICENSE", | |||
"index.js", | |||
"Readme.md" | |||
], | |||
"engines": { | |||
"node": ">= 0.8" | |||
}, | |||
"scripts": { | |||
"bench": "node benchmark/index.js", | |||
"lint": "eslint --plugin markdown --ext js,md .", | |||
"test": "mocha --reporter spec --bail test/", | |||
"test-ci": "istanbul cover --print=none node_modules/mocha/bin/_mocha -- --reporter spec test/ && istanbul report lcovonly text-summary", | |||
"test-cov": "istanbul cover --print=none node_modules/mocha/bin/_mocha -- --reporter dot test/ && istanbul report lcov text-summary" | |||
} | |||
} |
@ -1,21 +0,0 @@ | |||
The MIT License (MIT) | |||
Copyright (c) Feross Aboukhadijeh | |||
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 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. |
@ -1,586 +0,0 @@ | |||
# safe-buffer [![travis][travis-image]][travis-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url] | |||
[travis-image]: https://img.shields.io/travis/feross/safe-buffer/master.svg | |||
[travis-url]: https://travis-ci.org/feross/safe-buffer | |||
[npm-image]: https://img.shields.io/npm/v/safe-buffer.svg | |||
[npm-url]: https://npmjs.org/package/safe-buffer | |||
[downloads-image]: https://img.shields.io/npm/dm/safe-buffer.svg | |||
[downloads-url]: https://npmjs.org/package/safe-buffer | |||
[standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg | |||
[standard-url]: https://standardjs.com | |||
#### Safer Node.js Buffer API | |||
**Use the new Node.js Buffer APIs (`Buffer.from`, `Buffer.alloc`, | |||
`Buffer.allocUnsafe`, `Buffer.allocUnsafeSlow`) in all versions of Node.js.** | |||
**Uses the built-in implementation when available.** | |||
## install | |||
``` | |||
npm install safe-buffer | |||
``` | |||
[Get supported safe-buffer with the Tidelift Subscription](https://tidelift.com/subscription/pkg/npm-safe-buffer?utm_source=npm-safe-buffer&utm_medium=referral&utm_campaign=readme) | |||
## usage | |||
The goal of this package is to provide a safe replacement for the node.js `Buffer`. | |||
It's a drop-in replacement for `Buffer`. You can use it by adding one `require` line to | |||
the top of your node.js modules: | |||
```js | |||
var Buffer = require('safe-buffer').Buffer | |||
// Existing buffer code will continue to work without issues: | |||
new Buffer('hey', 'utf8') | |||
new Buffer([1, 2, 3], 'utf8') | |||
new Buffer(obj) | |||
new Buffer(16) // create an uninitialized buffer (potentially unsafe) | |||
// But you can use these new explicit APIs to make clear what you want: | |||
Buffer.from('hey', 'utf8') // convert from many types to a Buffer | |||
Buffer.alloc(16) // create a zero-filled buffer (safe) | |||
Buffer.allocUnsafe(16) // create an uninitialized buffer (potentially unsafe) | |||
``` | |||
## api | |||
### Class Method: Buffer.from(array) | |||
<!-- YAML | |||
added: v3.0.0 | |||
--> | |||
* `array` {Array} | |||
Allocates a new `Buffer` using an `array` of octets. | |||
```js | |||
const buf = Buffer.from([0x62,0x75,0x66,0x66,0x65,0x72]); | |||
// creates a new Buffer containing ASCII bytes | |||
// ['b','u','f','f','e','r'] | |||
``` | |||
A `TypeError` will be thrown if `array` is not an `Array`. | |||
### Class Method: Buffer.from(arrayBuffer[, byteOffset[, length]]) | |||
<!-- YAML | |||
added: v5.10.0 | |||
--> | |||
* `arrayBuffer` {ArrayBuffer} The `.buffer` property of a `TypedArray` or | |||
a `new ArrayBuffer()` | |||
* `byteOffset` {Number} Default: `0` | |||
* `length` {Number} Default: `arrayBuffer.length - byteOffset` | |||
When passed a reference to the `.buffer` property of a `TypedArray` instance, | |||
the newly created `Buffer` will share the same allocated memory as the | |||
TypedArray. | |||
```js | |||
const arr = new Uint16Array(2); | |||
arr[0] = 5000; | |||
arr[1] = 4000; | |||
const buf = Buffer.from(arr.buffer); // shares the memory with arr; | |||
console.log(buf); | |||
// Prints: <Buffer 88 13 a0 0f> | |||
// changing the TypedArray changes the Buffer also | |||
arr[1] = 6000; | |||
console.log(buf); | |||
// Prints: <Buffer 88 13 70 17> | |||
``` | |||
The optional `byteOffset` and `length` arguments specify a memory range within | |||
the `arrayBuffer` that will be shared by the `Buffer`. | |||
```js | |||
const ab = new ArrayBuffer(10); | |||
const buf = Buffer.from(ab, 0, 2); | |||
console.log(buf.length); | |||
// Prints: 2 | |||
``` | |||
A `TypeError` will be thrown if `arrayBuffer` is not an `ArrayBuffer`. | |||
### Class Method: Buffer.from(buffer) | |||
<!-- YAML | |||
added: v3.0.0 | |||
--> | |||
* `buffer` {Buffer} | |||
Copies the passed `buffer` data onto a new `Buffer` instance. | |||
```js | |||
const buf1 = Buffer.from('buffer'); | |||
const buf2 = Buffer.from(buf1); | |||
buf1[0] = 0x61; | |||
console.log(buf1.toString()); | |||
// 'auffer' | |||
console.log(buf2.toString()); | |||
// 'buffer' (copy is not changed) | |||
``` | |||
A `TypeError` will be thrown if `buffer` is not a `Buffer`. | |||
### Class Method: Buffer.from(str[, encoding]) | |||
<!-- YAML | |||
added: v5.10.0 | |||
--> | |||
* `str` {String} String to encode. | |||
* `encoding` {String} Encoding to use, Default: `'utf8'` | |||
Creates a new `Buffer` containing the given JavaScript string `str`. If | |||
provided, the `encoding` parameter identifies the character encoding. | |||
If not provided, `encoding` defaults to `'utf8'`. | |||
```js | |||
const buf1 = Buffer.from('this is a tést'); | |||
console.log(buf1.toString()); | |||
// prints: this is a tést | |||
console.log(buf1.toString('ascii')); | |||
// prints: this is a tC)st | |||
const buf2 = Buffer.from('7468697320697320612074c3a97374', 'hex'); | |||
console.log(buf2.toString()); | |||
// prints: this is a tést | |||
``` | |||
A `TypeError` will be thrown if `str` is not a string. | |||
### Class Method: Buffer.alloc(size[, fill[, encoding]]) | |||
<!-- YAML | |||
added: v5.10.0 | |||
--> | |||
* `size` {Number} | |||
* `fill` {Value} Default: `undefined` | |||
* `encoding` {String} Default: `utf8` | |||
Allocates a new `Buffer` of `size` bytes. If `fill` is `undefined`, the | |||
`Buffer` will be *zero-filled*. | |||
```js | |||
const buf = Buffer.alloc(5); | |||
console.log(buf); | |||
// <Buffer 00 00 00 00 00> | |||
``` | |||
The `size` must be less than or equal to the value of | |||
`require('buffer').kMaxLength` (on 64-bit architectures, `kMaxLength` is | |||
`(2^31)-1`). Otherwise, a [`RangeError`][] is thrown. A zero-length Buffer will | |||
be created if a `size` less than or equal to 0 is specified. | |||
If `fill` is specified, the allocated `Buffer` will be initialized by calling | |||
`buf.fill(fill)`. See [`buf.fill()`][] for more information. | |||
```js | |||
const buf = Buffer.alloc(5, 'a'); | |||
console.log(buf); | |||
// <Buffer 61 61 61 61 61> | |||
``` | |||
If both `fill` and `encoding` are specified, the allocated `Buffer` will be | |||
initialized by calling `buf.fill(fill, encoding)`. For example: | |||
```js | |||
const buf = Buffer.alloc(11, 'aGVsbG8gd29ybGQ=', 'base64'); | |||
console.log(buf); | |||
// <Buffer 68 65 6c 6c 6f 20 77 6f 72 6c 64> | |||
``` | |||
Calling `Buffer.alloc(size)` can be significantly slower than the alternative | |||
`Buffer.allocUnsafe(size)` but ensures that the newly created `Buffer` instance | |||
contents will *never contain sensitive data*. | |||
A `TypeError` will be thrown if `size` is not a number. | |||
### Class Method: Buffer.allocUnsafe(size) | |||
<!-- YAML | |||
added: v5.10.0 | |||
--> | |||
* `size` {Number} | |||
Allocates a new *non-zero-filled* `Buffer` of `size` bytes. The `size` must | |||
be less than or equal to the value of `require('buffer').kMaxLength` (on 64-bit | |||
architectures, `kMaxLength` is `(2^31)-1`). Otherwise, a [`RangeError`][] is | |||
thrown. A zero-length Buffer will be created if a `size` less than or equal to | |||
0 is specified. | |||
The underlying memory for `Buffer` instances created in this way is *not | |||
initialized*. The contents of the newly created `Buffer` are unknown and | |||
*may contain sensitive data*. Use [`buf.fill(0)`][] to initialize such | |||
`Buffer` instances to zeroes. | |||
```js | |||
const buf = Buffer.allocUnsafe(5); | |||
console.log(buf); | |||
// <Buffer 78 e0 82 02 01> | |||
// (octets will be different, every time) | |||
buf.fill(0); | |||
console.log(buf); | |||
// <Buffer 00 00 00 00 00> | |||
``` | |||
A `TypeError` will be thrown if `size` is not a number. | |||
Note that the `Buffer` module pre-allocates an internal `Buffer` instance of | |||
size `Buffer.poolSize` that is used as a pool for the fast allocation of new | |||
`Buffer` instances created using `Buffer.allocUnsafe(size)` (and the deprecated | |||
`new Buffer(size)` constructor) only when `size` is less than or equal to | |||
`Buffer.poolSize >> 1` (floor of `Buffer.poolSize` divided by two). The default | |||
value of `Buffer.poolSize` is `8192` but can be modified. | |||
Use of this pre-allocated internal memory pool is a key difference between | |||
calling `Buffer.alloc(size, fill)` vs. `Buffer.allocUnsafe(size).fill(fill)`. | |||
Specifically, `Buffer.alloc(size, fill)` will *never* use the internal Buffer | |||
pool, while `Buffer.allocUnsafe(size).fill(fill)` *will* use the internal | |||
Buffer pool if `size` is less than or equal to half `Buffer.poolSize`. The | |||
difference is subtle but can be important when an application requires the | |||
additional performance that `Buffer.allocUnsafe(size)` provides. | |||
### Class Method: Buffer.allocUnsafeSlow(size) | |||
<!-- YAML | |||
added: v5.10.0 | |||
--> | |||
* `size` {Number} | |||
Allocates a new *non-zero-filled* and non-pooled `Buffer` of `size` bytes. The | |||
`size` must be less than or equal to the value of | |||
`require('buffer').kMaxLength` (on 64-bit architectures, `kMaxLength` is | |||
`(2^31)-1`). Otherwise, a [`RangeError`][] is thrown. A zero-length Buffer will | |||
be created if a `size` less than or equal to 0 is specified. | |||
The underlying memory for `Buffer` instances created in this way is *not | |||
initialized*. The contents of the newly created `Buffer` are unknown and | |||
*may contain sensitive data*. Use [`buf.fill(0)`][] to initialize such | |||
`Buffer` instances to zeroes. | |||
When using `Buffer.allocUnsafe()` to allocate new `Buffer` instances, | |||
allocations under 4KB are, by default, sliced from a single pre-allocated | |||
`Buffer`. This allows applications to avoid the garbage collection overhead of | |||
creating many individually allocated Buffers. This approach improves both | |||
performance and memory usage by eliminating the need to track and cleanup as | |||
many `Persistent` objects. | |||
However, in the case where a developer may need to retain a small chunk of | |||
memory from a pool for an indeterminate amount of time, it may be appropriate | |||
to create an un-pooled Buffer instance using `Buffer.allocUnsafeSlow()` then | |||
copy out the relevant bits. | |||
```js | |||
// need to keep around a few small chunks of memory | |||
const store = []; | |||
socket.on('readable', () => { | |||
const data = socket.read(); | |||
// allocate for retained data | |||
const sb = Buffer.allocUnsafeSlow(10); | |||
// copy the data into the new allocation | |||
data.copy(sb, 0, 0, 10); | |||
store.push(sb); | |||
}); | |||
``` | |||
Use of `Buffer.allocUnsafeSlow()` should be used only as a last resort *after* | |||
a developer has observed undue memory retention in their applications. | |||
A `TypeError` will be thrown if `size` is not a number. | |||
### All the Rest | |||
The rest of the `Buffer` API is exactly the same as in node.js. | |||
[See the docs](https://nodejs.org/api/buffer.html). | |||
## Related links | |||
- [Node.js issue: Buffer(number) is unsafe](https://github.com/nodejs/node/issues/4660) | |||
- [Node.js Enhancement Proposal: Buffer.from/Buffer.alloc/Buffer.zalloc/Buffer() soft-deprecate](https://github.com/nodejs/node-eps/pull/4) | |||
## Why is `Buffer` unsafe? | |||
Today, the node.js `Buffer` constructor is overloaded to handle many different argument | |||
types like `String`, `Array`, `Object`, `TypedArrayView` (`Uint8Array`, etc.), | |||
`ArrayBuffer`, and also `Number`. | |||
The API is optimized for convenience: you can throw any type at it, and it will try to do | |||
what you want. | |||
Because the Buffer constructor is so powerful, you often see code like this: | |||
```js | |||
// Convert UTF-8 strings to hex | |||
function toHex (str) { | |||
return new Buffer(str).toString('hex') | |||
} | |||
``` | |||
***But what happens if `toHex` is called with a `Number` argument?*** | |||
### Remote Memory Disclosure | |||
If an attacker can make your program call the `Buffer` constructor with a `Number` | |||
argument, then they can make it allocate uninitialized memory from the node.js process. | |||
This could potentially disclose TLS private keys, user data, or database passwords. | |||
When the `Buffer` constructor is passed a `Number` argument, it returns an | |||
**UNINITIALIZED** block of memory of the specified `size`. When you create a `Buffer` like | |||
this, you **MUST** overwrite the contents before returning it to the user. | |||
From the [node.js docs](https://nodejs.org/api/buffer.html#buffer_new_buffer_size): | |||
> `new Buffer(size)` | |||
> | |||
> - `size` Number | |||
> | |||
> The underlying memory for `Buffer` instances created in this way is not initialized. | |||
> **The contents of a newly created `Buffer` are unknown and could contain sensitive | |||
> data.** Use `buf.fill(0)` to initialize a Buffer to zeroes. | |||
(Emphasis our own.) | |||
Whenever the programmer intended to create an uninitialized `Buffer` you often see code | |||
like this: | |||
```js | |||
var buf = new Buffer(16) | |||
// Immediately overwrite the uninitialized buffer with data from another buffer | |||
for (var i = 0; i < buf.length; i++) { | |||
buf[i] = otherBuf[i] | |||
} | |||
``` | |||
### Would this ever be a problem in real code? | |||
Yes. It's surprisingly common to forget to check the type of your variables in a | |||
dynamically-typed language like JavaScript. | |||
Usually the consequences of assuming the wrong type is that your program crashes with an | |||
uncaught exception. But the failure mode for forgetting to check the type of arguments to | |||
the `Buffer` constructor is more catastrophic. | |||
Here's an example of a vulnerable service that takes a JSON payload and converts it to | |||
hex: | |||
```js | |||
// Take a JSON payload {str: "some string"} and convert it to hex | |||
var server = http.createServer(function (req, res) { | |||
var data = '' | |||
req.setEncoding('utf8') | |||
req.on('data', function (chunk) { | |||
data += chunk | |||
}) | |||
req.on('end', function () { | |||
var body = JSON.parse(data) | |||
res.end(new Buffer(body.str).toString('hex')) | |||
}) | |||
}) | |||
server.listen(8080) | |||
``` | |||
In this example, an http client just has to send: | |||
```json | |||
{ | |||
"str": 1000 | |||
} | |||
``` | |||
and it will get back 1,000 bytes of uninitialized memory from the server. | |||
This is a very serious bug. It's similar in severity to the | |||
[the Heartbleed bug](http://heartbleed.com/) that allowed disclosure of OpenSSL process | |||
memory by remote attackers. | |||
### Which real-world packages were vulnerable? | |||
#### [`bittorrent-dht`](https://www.npmjs.com/package/bittorrent-dht) | |||
[Mathias Buus](https://github.com/mafintosh) and I | |||
([Feross Aboukhadijeh](http://feross.org/)) found this issue in one of our own packages, | |||
[`bittorrent-dht`](https://www.npmjs.com/package/bittorrent-dht). The bug would allow | |||
anyone on the internet to send a series of messages to a user of `bittorrent-dht` and get | |||
them to reveal 20 bytes at a time of uninitialized memory from the node.js process. | |||
Here's | |||
[the commit](https://github.com/feross/bittorrent-dht/commit/6c7da04025d5633699800a99ec3fbadf70ad35b8) | |||
that fixed it. We released a new fixed version, created a | |||
[Node Security Project disclosure](https://nodesecurity.io/advisories/68), and deprecated all | |||
vulnerable versions on npm so users will get a warning to upgrade to a newer version. | |||
#### [`ws`](https://www.npmjs.com/package/ws) | |||
That got us wondering if there were other vulnerable packages. Sure enough, within a short | |||
period of time, we found the same issue in [`ws`](https://www.npmjs.com/package/ws), the | |||
most popular WebSocket implementation in node.js. | |||
If certain APIs were called with `Number` parameters instead of `String` or `Buffer` as | |||
expected, then uninitialized server memory would be disclosed to the remote peer. | |||
These were the vulnerable methods: | |||
```js | |||
socket.send(number) | |||
socket.ping(number) | |||
socket.pong(number) | |||
``` | |||
Here's a vulnerable socket server with some echo functionality: | |||
```js | |||
server.on('connection', function (socket) { | |||
socket.on('message', function (message) { | |||
message = JSON.parse(message) | |||
if (message.type === 'echo') { | |||
socket.send(message.data) // send back the user's message | |||
} | |||
}) | |||
}) | |||
``` | |||
`socket.send(number)` called on the server, will disclose server memory. | |||
Here's [the release](https://github.com/websockets/ws/releases/tag/1.0.1) where the issue | |||
was fixed, with a more detailed explanation. Props to | |||
[Arnout Kazemier](https://github.com/3rd-Eden) for the quick fix. Here's the | |||
[Node Security Project disclosure](https://nodesecurity.io/advisories/67). | |||
### What's the solution? | |||
It's important that node.js offers a fast way to get memory otherwise performance-critical | |||
applications would needlessly get a lot slower. | |||
But we need a better way to *signal our intent* as programmers. **When we want | |||
uninitialized memory, we should request it explicitly.** | |||
Sensitive functionality should not be packed into a developer-friendly API that loosely | |||
accepts many different types. This type of API encourages the lazy practice of passing | |||
variables in without checking the type very carefully. | |||
#### A new API: `Buffer.allocUnsafe(number)` | |||
The functionality of creating buffers with uninitialized memory should be part of another | |||
API. We propose `Buffer.allocUnsafe(number)`. This way, it's not part of an API that | |||
frequently gets user input of all sorts of different types passed into it. | |||
```js | |||
var buf = Buffer.allocUnsafe(16) // careful, uninitialized memory! | |||
// Immediately overwrite the uninitialized buffer with data from another buffer | |||
for (var i = 0; i < buf.length; i++) { | |||
buf[i] = otherBuf[i] | |||
} | |||
``` | |||
### How do we fix node.js core? | |||
We sent [a PR to node.js core](https://github.com/nodejs/node/pull/4514) (merged as | |||
`semver-major`) which defends against one case: | |||
```js | |||
var str = 16 | |||
new Buffer(str, 'utf8') | |||
``` | |||
In this situation, it's implied that the programmer intended the first argument to be a | |||
string, since they passed an encoding as a second argument. Today, node.js will allocate | |||
uninitialized memory in the case of `new Buffer(number, encoding)`, which is probably not | |||
what the programmer intended. | |||
But this is only a partial solution, since if the programmer does `new Buffer(variable)` | |||
(without an `encoding` parameter) there's no way to know what they intended. If `variable` | |||
is sometimes a number, then uninitialized memory will sometimes be returned. | |||
### What's the real long-term fix? | |||
We could deprecate and remove `new Buffer(number)` and use `Buffer.allocUnsafe(number)` when | |||
we need uninitialized memory. But that would break 1000s of packages. | |||
~~We believe the best solution is to:~~ | |||
~~1. Change `new Buffer(number)` to return safe, zeroed-out memory~~ | |||
~~2. Create a new API for creating uninitialized Buffers. We propose: `Buffer.allocUnsafe(number)`~~ | |||
#### Update | |||
We now support adding three new APIs: | |||
- `Buffer.from(value)` - convert from any type to a buffer | |||
- `Buffer.alloc(size)` - create a zero-filled buffer | |||
- `Buffer.allocUnsafe(size)` - create an uninitialized buffer with given size | |||
This solves the core problem that affected `ws` and `bittorrent-dht` which is | |||
`Buffer(variable)` getting tricked into taking a number argument. | |||
This way, existing code continues working and the impact on the npm ecosystem will be | |||
minimal. Over time, npm maintainers can migrate performance-critical code to use | |||
`Buffer.allocUnsafe(number)` instead of `new Buffer(number)`. | |||
### Conclusion | |||
We think there's a serious design issue with the `Buffer` API as it exists today. It | |||
promotes insecure software by putting high-risk functionality into a convenient API | |||
with friendly "developer ergonomics". | |||
This wasn't merely a theoretical exercise because we found the issue in some of the | |||
most popular npm packages. | |||
Fortunately, there's an easy fix that can be applied today. Use `safe-buffer` in place of | |||
`buffer`. | |||
```js | |||
var Buffer = require('safe-buffer').Buffer | |||
``` | |||
Eventually, we hope that node.js core can switch to this new, safer behavior. We believe | |||
the impact on the ecosystem would be minimal since it's not a breaking change. | |||
Well-maintained, popular packages would be updated to use `Buffer.alloc` quickly, while | |||
older, insecure packages would magically become safe from this attack vector. | |||
## links | |||
- [Node.js PR: buffer: throw if both length and enc are passed](https://github.com/nodejs/node/pull/4514) | |||
- [Node Security Project disclosure for `ws`](https://nodesecurity.io/advisories/67) | |||
- [Node Security Project disclosure for`bittorrent-dht`](https://nodesecurity.io/advisories/68) | |||
## credit | |||
The original issues in `bittorrent-dht` | |||
([disclosure](https://nodesecurity.io/advisories/68)) and | |||
`ws` ([disclosure](https://nodesecurity.io/advisories/67)) were discovered by | |||
[Mathias Buus](https://github.com/mafintosh) and | |||
[Feross Aboukhadijeh](http://feross.org/). | |||
Thanks to [Adam Baldwin](https://github.com/evilpacket) for helping disclose these issues | |||
and for his work running the [Node Security Project](https://nodesecurity.io/). | |||
Thanks to [John Hiesey](https://github.com/jhiesey) for proofreading this README and | |||
auditing the code. | |||
## license | |||
MIT. Copyright (C) [Feross Aboukhadijeh](http://feross.org) |
@ -1,187 +0,0 @@ | |||
declare module "safe-buffer" { | |||
export class Buffer { | |||
length: number | |||
write(string: string, offset?: number, length?: number, encoding?: string): number; | |||
toString(encoding?: string, start?: number, end?: number): string; | |||
toJSON(): { type: 'Buffer', data: any[] }; | |||
equals(otherBuffer: Buffer): boolean; | |||
compare(otherBuffer: Buffer, targetStart?: number, targetEnd?: number, sourceStart?: number, sourceEnd?: number): number; | |||
copy(targetBuffer: Buffer, targetStart?: number, sourceStart?: number, sourceEnd?: number): number; | |||
slice(start?: number, end?: number): Buffer; | |||
writeUIntLE(value: number, offset: number, byteLength: number, noAssert?: boolean): number; | |||
writeUIntBE(value: number, offset: number, byteLength: number, noAssert?: boolean): number; | |||
writeIntLE(value: number, offset: number, byteLength: number, noAssert?: boolean): number; | |||
writeIntBE(value: number, offset: number, byteLength: number, noAssert?: boolean): number; | |||
readUIntLE(offset: number, byteLength: number, noAssert?: boolean): number; | |||
readUIntBE(offset: number, byteLength: number, noAssert?: boolean): number; | |||
readIntLE(offset: number, byteLength: number, noAssert?: boolean): number; | |||
readIntBE(offset: number, byteLength: number, noAssert?: boolean): number; | |||
readUInt8(offset: number, noAssert?: boolean): number; | |||
readUInt16LE(offset: number, noAssert?: boolean): number; | |||
readUInt16BE(offset: number, noAssert?: boolean): number; | |||
readUInt32LE(offset: number, noAssert?: boolean): number; | |||
readUInt32BE(offset: number, noAssert?: boolean): number; | |||
readInt8(offset: number, noAssert?: boolean): number; | |||
readInt16LE(offset: number, noAssert?: boolean): number; | |||
readInt16BE(offset: number, noAssert?: boolean): number; | |||
readInt32LE(offset: number, noAssert?: boolean): number; | |||
readInt32BE(offset: number, noAssert?: boolean): number; | |||
readFloatLE(offset: number, noAssert?: boolean): number; | |||
readFloatBE(offset: number, noAssert?: boolean): number; | |||
readDoubleLE(offset: number, noAssert?: boolean): number; | |||
readDoubleBE(offset: number, noAssert?: boolean): number; | |||
swap16(): Buffer; | |||
swap32(): Buffer; | |||
swap64(): Buffer; | |||
writeUInt8(value: number, offset: number, noAssert?: boolean): number; | |||
writeUInt16LE(value: number, offset: number, noAssert?: boolean): number; | |||
writeUInt16BE(value: number, offset: number, noAssert?: boolean): number; | |||
writeUInt32LE(value: number, offset: number, noAssert?: boolean): number; | |||
writeUInt32BE(value: number, offset: number, noAssert?: boolean): number; | |||
writeInt8(value: number, offset: number, noAssert?: boolean): number; | |||
writeInt16LE(value: number, offset: number, noAssert?: boolean): number; | |||
writeInt16BE(value: number, offset: number, noAssert?: boolean): number; | |||
writeInt32LE(value: number, offset: number, noAssert?: boolean): number; | |||
writeInt32BE(value: number, offset: number, noAssert?: boolean): number; | |||
writeFloatLE(value: number, offset: number, noAssert?: boolean): number; | |||
writeFloatBE(value: number, offset: number, noAssert?: boolean): number; | |||
writeDoubleLE(value: number, offset: number, noAssert?: boolean): number; | |||
writeDoubleBE(value: number, offset: number, noAssert?: boolean): number; | |||
fill(value: any, offset?: number, end?: number): this; | |||
indexOf(value: string | number | Buffer, byteOffset?: number, encoding?: string): number; | |||
lastIndexOf(value: string | number | Buffer, byteOffset?: number, encoding?: string): number; | |||
includes(value: string | number | Buffer, byteOffset?: number, encoding?: string): boolean; | |||
/** | |||
* Allocates a new buffer containing the given {str}. | |||
* | |||
* @param str String to store in buffer. | |||
* @param encoding encoding to use, optional. Default is 'utf8' | |||
*/ | |||
constructor (str: string, encoding?: string); | |||
/** | |||
* Allocates a new buffer of {size} octets. | |||
* | |||
* @param size count of octets to allocate. | |||
*/ | |||
constructor (size: number); | |||
/** | |||
* Allocates a new buffer containing the given {array} of octets. | |||
* | |||
* @param array The octets to store. | |||
*/ | |||
constructor (array: Uint8Array); | |||
/** | |||
* Produces a Buffer backed by the same allocated memory as | |||
* the given {ArrayBuffer}. | |||
* | |||
* | |||
* @param arrayBuffer The ArrayBuffer with which to share memory. | |||
*/ | |||
constructor (arrayBuffer: ArrayBuffer); | |||
/** | |||
* Allocates a new buffer containing the given {array} of octets. | |||
* | |||
* @param array The octets to store. | |||
*/ | |||
constructor (array: any[]); | |||
/** | |||
* Copies the passed {buffer} data onto a new {Buffer} instance. | |||
* | |||
* @param buffer The buffer to copy. | |||
*/ | |||
constructor (buffer: Buffer); | |||
prototype: Buffer; | |||
/** | |||
* Allocates a new Buffer using an {array} of octets. | |||
* | |||
* @param array | |||
*/ | |||
static from(array: any[]): Buffer; | |||
/** | |||
* When passed a reference to the .buffer property of a TypedArray instance, | |||
* the newly created Buffer will share the same allocated memory as the TypedArray. | |||
* The optional {byteOffset} and {length} arguments specify a memory range | |||
* within the {arrayBuffer} that will be shared by the Buffer. | |||
* | |||
* @param arrayBuffer The .buffer property of a TypedArray or a new ArrayBuffer() | |||
* @param byteOffset | |||
* @param length | |||
*/ | |||
static from(arrayBuffer: ArrayBuffer, byteOffset?: number, length?: number): Buffer; | |||
/** | |||
* Copies the passed {buffer} data onto a new Buffer instance. | |||
* | |||
* @param buffer | |||
*/ | |||
static from(buffer: Buffer): Buffer; | |||
/** | |||
* Creates a new Buffer containing the given JavaScript string {str}. | |||
* If provided, the {encoding} parameter identifies the character encoding. | |||
* If not provided, {encoding} defaults to 'utf8'. | |||
* | |||
* @param str | |||
*/ | |||
static from(str: string, encoding?: string): Buffer; | |||
/** | |||
* Returns true if {obj} is a Buffer | |||
* | |||
* @param obj object to test. | |||
*/ | |||
static isBuffer(obj: any): obj is Buffer; | |||
/** | |||
* Returns true if {encoding} is a valid encoding argument. | |||
* Valid string encodings in Node 0.12: 'ascii'|'utf8'|'utf16le'|'ucs2'(alias of 'utf16le')|'base64'|'binary'(deprecated)|'hex' | |||
* | |||
* @param encoding string to test. | |||
*/ | |||
static isEncoding(encoding: string): boolean; | |||
/** | |||
* Gives the actual byte length of a string. encoding defaults to 'utf8'. | |||
* This is not the same as String.prototype.length since that returns the number of characters in a string. | |||
* | |||
* @param string string to test. | |||
* @param encoding encoding used to evaluate (defaults to 'utf8') | |||
*/ | |||
static byteLength(string: string, encoding?: string): number; | |||
/** | |||
* Returns a buffer which is the result of concatenating all the buffers in the list together. | |||
* | |||
* If the list has no items, or if the totalLength is 0, then it returns a zero-length buffer. | |||
* If the list has exactly one item, then the first item of the list is returned. | |||
* If the list has more than one item, then a new Buffer is created. | |||
* | |||
* @param list An array of Buffer objects to concatenate | |||
* @param totalLength Total length of the buffers when concatenated. | |||
* If totalLength is not provided, it is read from the buffers in the list. However, this adds an additional loop to the function, so it is faster to provide the length explicitly. | |||
*/ | |||
static concat(list: Buffer[], totalLength?: number): Buffer; | |||
/** | |||
* The same as buf1.compare(buf2). | |||
*/ | |||
static compare(buf1: Buffer, buf2: Buffer): number; | |||
/** | |||
* Allocates a new buffer of {size} octets. | |||
* | |||
* @param size count of octets to allocate. | |||
* @param fill if specified, buffer will be initialized by calling buf.fill(fill). | |||
* If parameter is omitted, buffer will be filled with zeros. | |||
* @param encoding encoding used for call to buf.fill while initalizing | |||
*/ | |||
static alloc(size: number, fill?: string | Buffer | number, encoding?: string): Buffer; | |||
/** | |||
* Allocates a new buffer of {size} octets, leaving memory not initialized, so the contents | |||
* of the newly created Buffer are unknown and may contain sensitive data. | |||
* | |||
* @param size count of octets to allocate | |||
*/ | |||
static allocUnsafe(size: number): Buffer; | |||
/** | |||
* Allocates a new non-pooled buffer of {size} octets, leaving memory not initialized, so the contents | |||
* of the newly created Buffer are unknown and may contain sensitive data. | |||
* | |||
* @param size count of octets to allocate | |||
*/ | |||
static allocUnsafeSlow(size: number): Buffer; | |||
} | |||
} |
@ -1,64 +0,0 @@ | |||
/* eslint-disable node/no-deprecated-api */ | |||
var buffer = require('buffer') | |||
var Buffer = buffer.Buffer | |||
// alternative to using Object.keys for old browsers | |||
function copyProps (src, dst) { | |||
for (var key in src) { | |||
dst[key] = src[key] | |||
} | |||
} | |||
if (Buffer.from && Buffer.alloc && Buffer.allocUnsafe && Buffer.allocUnsafeSlow) { | |||
module.exports = buffer | |||
} else { | |||
// Copy properties from require('buffer') | |||
copyProps(buffer, exports) | |||
exports.Buffer = SafeBuffer | |||
} | |||
function SafeBuffer (arg, encodingOrOffset, length) { | |||
return Buffer(arg, encodingOrOffset, length) | |||
} | |||
SafeBuffer.prototype = Object.create(Buffer.prototype) | |||
// Copy static methods from Buffer | |||
copyProps(Buffer, SafeBuffer) | |||
SafeBuffer.from = function (arg, encodingOrOffset, length) { | |||
if (typeof arg === 'number') { | |||
throw new TypeError('Argument must not be a number') | |||
} | |||
return Buffer(arg, encodingOrOffset, length) | |||
} | |||
SafeBuffer.alloc = function (size, fill, encoding) { | |||
if (typeof size !== 'number') { | |||
throw new TypeError('Argument must be a number') | |||
} | |||
var buf = Buffer(size) | |||
if (fill !== undefined) { | |||
if (typeof encoding === 'string') { | |||
buf.fill(fill, encoding) | |||
} else { | |||
buf.fill(fill) | |||
} | |||
} else { | |||
buf.fill(0) | |||
} | |||
return buf | |||
} | |||
SafeBuffer.allocUnsafe = function (size) { | |||
if (typeof size !== 'number') { | |||
throw new TypeError('Argument must be a number') | |||
} | |||
return Buffer(size) | |||
} | |||
SafeBuffer.allocUnsafeSlow = function (size) { | |||
if (typeof size !== 'number') { | |||
throw new TypeError('Argument must be a number') | |||
} | |||
return buffer.SlowBuffer(size) | |||
} |
@ -1,37 +0,0 @@ | |||
{ | |||
"name": "safe-buffer", | |||
"description": "Safer Node.js Buffer API", | |||
"version": "5.2.0", | |||
"author": { | |||
"name": "Feross Aboukhadijeh", | |||
"email": "feross@feross.org", | |||
"url": "http://feross.org" | |||
}, | |||
"bugs": { | |||
"url": "https://github.com/feross/safe-buffer/issues" | |||
}, | |||
"devDependencies": { | |||
"standard": "*", | |||
"tape": "^4.0.0" | |||
}, | |||
"homepage": "https://github.com/feross/safe-buffer", | |||
"keywords": [ | |||
"buffer", | |||
"buffer allocate", | |||
"node security", | |||
"safe", | |||
"safe-buffer", | |||
"security", | |||
"uninitialized" | |||
], | |||
"license": "MIT", | |||
"main": "index.js", | |||
"types": "index.d.ts", | |||
"repository": { | |||
"type": "git", | |||
"url": "git://github.com/feross/safe-buffer.git" | |||
}, | |||
"scripts": { | |||
"test": "standard && tape test/*.js" | |||
} | |||
} |
@ -1,48 +0,0 @@ | |||
{ | |||
"name": "express-session", | |||
"version": "1.17.0", | |||
"description": "Simple session middleware for Express", | |||
"author": "TJ Holowaychuk <tj@vision-media.ca> (http://tjholowaychuk.com)", | |||
"contributors": [ | |||
"Douglas Christopher Wilson <doug@somethingdoug.com>", | |||
"Joe Wagner <njwjs722@gmail.com>" | |||
], | |||
"repository": "expressjs/session", | |||
"license": "MIT", | |||
"dependencies": { | |||
"cookie": "0.4.0", | |||
"cookie-signature": "1.0.6", | |||
"debug": "2.6.9", | |||
"depd": "~2.0.0", | |||
"on-headers": "~1.0.2", | |||
"parseurl": "~1.3.3", | |||
"safe-buffer": "5.2.0", | |||
"uid-safe": "~2.1.5" | |||
}, | |||
"devDependencies": { | |||
"after": "0.8.2", | |||
"cookie-parser": "1.4.4", | |||
"eslint": "3.19.0", | |||
"eslint-plugin-markdown": "1.0.0", | |||
"express": "4.17.1", | |||
"mocha": "6.2.1", | |||
"nyc": "14.1.1", | |||
"supertest": "4.0.2" | |||
}, | |||
"files": [ | |||
"session/", | |||
"HISTORY.md", | |||
"LICENSE", | |||
"index.js" | |||
], | |||
"engines": { | |||
"node": ">= 0.8.0" | |||
}, | |||
"scripts": { | |||
"lint": "eslint --plugin markdown --ext js,md . && node ./scripts/lint-readme.js", | |||
"test": "mocha --require test/support/env --check-leaks --bail --no-exit --reporter spec test/", | |||
"test-cov": "nyc npm test", | |||
"test-travis": "nyc npm test -- --no-exit", | |||
"version": "node scripts/version-history.js && git add HISTORY.md" | |||
} | |||
} |
@ -1,150 +0,0 @@ | |||
/*! | |||
* Connect - session - Cookie | |||
* Copyright(c) 2010 Sencha Inc. | |||
* Copyright(c) 2011 TJ Holowaychuk | |||
* MIT Licensed | |||
*/ | |||
'use strict'; | |||
/** | |||
* Module dependencies. | |||
*/ | |||
var cookie = require('cookie') | |||
var deprecate = require('depd')('express-session') | |||
/** | |||
* Initialize a new `Cookie` with the given `options`. | |||
* | |||
* @param {IncomingMessage} req | |||
* @param {Object} options | |||
* @api private | |||
*/ | |||
var Cookie = module.exports = function Cookie(options) { | |||
this.path = '/'; | |||
this.maxAge = null; | |||
this.httpOnly = true; | |||
if (options) { | |||
if (typeof options !== 'object') { | |||
throw new TypeError('argument options must be a object') | |||
} | |||
for (var key in options) { | |||
if (key !== 'data') { | |||
this[key] = options[key] | |||
} | |||
} | |||
} | |||
if (this.originalMaxAge === undefined || this.originalMaxAge === null) { | |||
this.originalMaxAge = this.maxAge | |||
} | |||
}; | |||
/*! | |||
* Prototype. | |||
*/ | |||
Cookie.prototype = { | |||
/** | |||
* Set expires `date`. | |||
* | |||
* @param {Date} date | |||
* @api public | |||
*/ | |||
set expires(date) { | |||
this._expires = date; | |||
this.originalMaxAge = this.maxAge; | |||
}, | |||
/** | |||
* Get expires `date`. | |||
* | |||
* @return {Date} | |||
* @api public | |||
*/ | |||
get expires() { | |||
return this._expires; | |||
}, | |||
/** | |||
* Set expires via max-age in `ms`. | |||
* | |||
* @param {Number} ms | |||
* @api public | |||
*/ | |||
set maxAge(ms) { | |||
if (ms && typeof ms !== 'number' && !(ms instanceof Date)) { | |||
throw new TypeError('maxAge must be a number or Date') | |||
} | |||
if (ms instanceof Date) { | |||
deprecate('maxAge as Date; pass number of milliseconds instead') | |||
} | |||
this.expires = typeof ms === 'number' | |||
? new Date(Date.now() + ms) | |||
: ms; | |||
}, | |||
/** | |||
* Get expires max-age in `ms`. | |||
* | |||
* @return {Number} | |||
* @api public | |||
*/ | |||
get maxAge() { | |||
return this.expires instanceof Date | |||
? this.expires.valueOf() - Date.now() | |||
: this.expires; | |||
}, | |||
/** | |||
* Return cookie data object. | |||
* | |||
* @return {Object} | |||
* @api private | |||
*/ | |||
get data() { | |||
return { | |||
originalMaxAge: this.originalMaxAge | |||
, expires: this._expires | |||
, secure: this.secure | |||
, httpOnly: this.httpOnly | |||
, domain: this.domain | |||
, path: this.path | |||
, sameSite: this.sameSite | |||
} | |||
}, | |||
/** | |||
* Return a serialized cookie string. | |||
* | |||
* @return {String} | |||
* @api public | |||
*/ | |||
serialize: function(name, val){ | |||
return cookie.serialize(name, val, this.data); | |||
}, | |||
/** | |||
* Return JSON representation of this cookie. | |||
* | |||
* @return {Object} | |||
* @api private | |||
*/ | |||
toJSON: function(){ | |||
return this.data; | |||
} | |||
}; |
@ -1,187 +0,0 @@ | |||
/*! | |||
* express-session | |||
* Copyright(c) 2010 Sencha Inc. | |||
* Copyright(c) 2011 TJ Holowaychuk | |||
* Copyright(c) 2015 Douglas Christopher Wilson | |||
* MIT Licensed | |||
*/ | |||
'use strict'; | |||
/** | |||
* Module dependencies. | |||
* @private | |||
*/ | |||
var Store = require('./store') | |||
var util = require('util') | |||
/** | |||
* Shim setImmediate for node.js < 0.10 | |||
* @private | |||
*/ | |||
/* istanbul ignore next */ | |||
var defer = typeof setImmediate === 'function' | |||
? setImmediate | |||
: function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } | |||
/** | |||
* Module exports. | |||
*/ | |||
module.exports = MemoryStore | |||
/** | |||
* A session store in memory. | |||
* @public | |||
*/ | |||
function MemoryStore() { | |||
Store.call(this) | |||
this.sessions = Object.create(null) | |||
} | |||
/** | |||
* Inherit from Store. | |||
*/ | |||
util.inherits(MemoryStore, Store) | |||
/** | |||
* Get all active sessions. | |||
* | |||
* @param {function} callback | |||
* @public | |||
*/ | |||
MemoryStore.prototype.all = function all(callback) { | |||
var sessionIds = Object.keys(this.sessions) | |||
var sessions = Object.create(null) | |||
for (var i = 0; i < sessionIds.length; i++) { | |||
var sessionId = sessionIds[i] | |||
var session = getSession.call(this, sessionId) | |||
if (session) { | |||
sessions[sessionId] = session; | |||
} | |||
} | |||
callback && defer(callback, null, sessions) | |||
} | |||
/** | |||
* Clear all sessions. | |||
* | |||
* @param {function} callback | |||
* @public | |||
*/ | |||
MemoryStore.prototype.clear = function clear(callback) { | |||
this.sessions = Object.create(null) | |||
callback && defer(callback) | |||
} | |||
/** | |||
* Destroy the session associated with the given session ID. | |||
* | |||
* @param {string} sessionId | |||
* @public | |||
*/ | |||
MemoryStore.prototype.destroy = function destroy(sessionId, callback) { | |||
delete this.sessions[sessionId] | |||
callback && defer(callback) | |||
} | |||
/** | |||
* Fetch session by the given session ID. | |||
* | |||
* @param {string} sessionId | |||
* @param {function} callback | |||
* @public | |||
*/ | |||
MemoryStore.prototype.get = function get(sessionId, callback) { | |||
defer(callback, null, getSession.call(this, sessionId)) | |||
} | |||
/** | |||
* Commit the given session associated with the given sessionId to the store. | |||
* | |||
* @param {string} sessionId | |||
* @param {object} session | |||
* @param {function} callback | |||
* @public | |||
*/ | |||
MemoryStore.prototype.set = function set(sessionId, session, callback) { | |||
this.sessions[sessionId] = JSON.stringify(session) | |||
callback && defer(callback) | |||
} | |||
/** | |||
* Get number of active sessions. | |||
* | |||
* @param {function} callback | |||
* @public | |||
*/ | |||
MemoryStore.prototype.length = function length(callback) { | |||
this.all(function (err, sessions) { | |||
if (err) return callback(err) | |||
callback(null, Object.keys(sessions).length) | |||
}) | |||
} | |||
/** | |||
* Touch the given session object associated with the given session ID. | |||
* | |||
* @param {string} sessionId | |||
* @param {object} session | |||
* @param {function} callback | |||
* @public | |||
*/ | |||
MemoryStore.prototype.touch = function touch(sessionId, session, callback) { | |||
var currentSession = getSession.call(this, sessionId) | |||
if (currentSession) { | |||
// update expiration | |||
currentSession.cookie = session.cookie | |||
this.sessions[sessionId] = JSON.stringify(currentSession) | |||
} | |||
callback && defer(callback) | |||
} | |||
/** | |||
* Get session from the store. | |||
* @private | |||
*/ | |||
function getSession(sessionId) { | |||
var sess = this.sessions[sessionId] | |||
if (!sess) { | |||
return | |||
} | |||
// parse | |||
sess = JSON.parse(sess) | |||
if (sess.cookie) { | |||
var expires = typeof sess.cookie.expires === 'string' | |||
? new Date(sess.cookie.expires) | |||
: sess.cookie.expires | |||
// destroy expired session | |||
if (expires && expires <= Date.now()) { | |||
delete this.sessions[sessionId] | |||
return | |||
} | |||
} | |||
return sess | |||
} |
@ -1,143 +0,0 @@ | |||
/*! | |||
* Connect - session - Session | |||
* Copyright(c) 2010 Sencha Inc. | |||
* Copyright(c) 2011 TJ Holowaychuk | |||
* MIT Licensed | |||
*/ | |||
'use strict'; | |||
/** | |||
* Expose Session. | |||
*/ | |||
module.exports = Session; | |||
/** | |||
* Create a new `Session` with the given request and `data`. | |||
* | |||
* @param {IncomingRequest} req | |||
* @param {Object} data | |||
* @api private | |||
*/ | |||
function Session(req, data) { | |||
Object.defineProperty(this, 'req', { value: req }); | |||
Object.defineProperty(this, 'id', { value: req.sessionID }); | |||
if (typeof data === 'object' && data !== null) { | |||
// merge data into this, ignoring prototype properties | |||
for (var prop in data) { | |||
if (!(prop in this)) { | |||
this[prop] = data[prop] | |||
} | |||
} | |||
} | |||
} | |||
/** | |||
* Update reset `.cookie.maxAge` to prevent | |||
* the cookie from expiring when the | |||
* session is still active. | |||
* | |||
* @return {Session} for chaining | |||
* @api public | |||
*/ | |||
defineMethod(Session.prototype, 'touch', function touch() { | |||
return this.resetMaxAge(); | |||
}); | |||
/** | |||
* Reset `.maxAge` to `.originalMaxAge`. | |||
* | |||
* @return {Session} for chaining | |||
* @api public | |||
*/ | |||
defineMethod(Session.prototype, 'resetMaxAge', function resetMaxAge() { | |||
this.cookie.maxAge = this.cookie.originalMaxAge; | |||
return this; | |||
}); | |||
/** | |||
* Save the session data with optional callback `fn(err)`. | |||
* | |||
* @param {Function} fn | |||
* @return {Session} for chaining | |||
* @api public | |||
*/ | |||
defineMethod(Session.prototype, 'save', function save(fn) { | |||
this.req.sessionStore.set(this.id, this, fn || function(){}); | |||
return this; | |||
}); | |||
/** | |||
* Re-loads the session data _without_ altering | |||
* the maxAge properties. Invokes the callback `fn(err)`, | |||
* after which time if no exception has occurred the | |||
* `req.session` property will be a new `Session` object, | |||
* although representing the same session. | |||
* | |||
* @param {Function} fn | |||
* @return {Session} for chaining | |||
* @api public | |||
*/ | |||
defineMethod(Session.prototype, 'reload', function reload(fn) { | |||
var req = this.req | |||
var store = this.req.sessionStore | |||
store.get(this.id, function(err, sess){ | |||
if (err) return fn(err); | |||
if (!sess) return fn(new Error('failed to load session')); | |||
store.createSession(req, sess); | |||
fn(); | |||
}); | |||
return this; | |||
}); | |||
/** | |||
* Destroy `this` session. | |||
* | |||
* @param {Function} fn | |||
* @return {Session} for chaining | |||
* @api public | |||
*/ | |||
defineMethod(Session.prototype, 'destroy', function destroy(fn) { | |||
delete this.req.session; | |||
this.req.sessionStore.destroy(this.id, fn); | |||
return this; | |||
}); | |||
/** | |||
* Regenerate this request's session. | |||
* | |||
* @param {Function} fn | |||
* @return {Session} for chaining | |||
* @api public | |||
*/ | |||
defineMethod(Session.prototype, 'regenerate', function regenerate(fn) { | |||
this.req.sessionStore.regenerate(this.req, fn); | |||
return this; | |||
}); | |||
/** | |||
* Helper function for creating a method on a prototype. | |||
* | |||
* @param {Object} obj | |||
* @param {String} name | |||
* @param {Function} fn | |||
* @private | |||
*/ | |||
function defineMethod(obj, name, fn) { | |||
Object.defineProperty(obj, name, { | |||
configurable: true, | |||
enumerable: false, | |||
value: fn, | |||
writable: true | |||
}); | |||
}; |
@ -1,102 +0,0 @@ | |||
/*! | |||
* Connect - session - Store | |||
* Copyright(c) 2010 Sencha Inc. | |||
* Copyright(c) 2011 TJ Holowaychuk | |||
* MIT Licensed | |||
*/ | |||
'use strict'; | |||
/** | |||
* Module dependencies. | |||
* @private | |||
*/ | |||
var Cookie = require('./cookie') | |||
var EventEmitter = require('events').EventEmitter | |||
var Session = require('./session') | |||
var util = require('util') | |||
/** | |||
* Module exports. | |||
* @public | |||
*/ | |||
module.exports = Store | |||
/** | |||
* Abstract base class for session stores. | |||
* @public | |||
*/ | |||
function Store () { | |||
EventEmitter.call(this) | |||
} | |||
/** | |||
* Inherit from EventEmitter. | |||
*/ | |||
util.inherits(Store, EventEmitter) | |||
/** | |||
* Re-generate the given requests's session. | |||
* | |||
* @param {IncomingRequest} req | |||
* @return {Function} fn | |||
* @api public | |||
*/ | |||
Store.prototype.regenerate = function(req, fn){ | |||
var self = this; | |||
this.destroy(req.sessionID, function(err){ | |||
self.generate(req); | |||
fn(err); | |||
}); | |||
}; | |||
/** | |||
* Load a `Session` instance via the given `sid` | |||
* and invoke the callback `fn(err, sess)`. | |||
* | |||
* @param {String} sid | |||
* @param {Function} fn | |||
* @api public | |||
*/ | |||
Store.prototype.load = function(sid, fn){ | |||
var self = this; | |||
this.get(sid, function(err, sess){ | |||
if (err) return fn(err); | |||
if (!sess) return fn(); | |||
var req = { sessionID: sid, sessionStore: self }; | |||
fn(null, self.createSession(req, sess)) | |||
}); | |||
}; | |||
/** | |||
* Create session from JSON `sess` data. | |||
* | |||
* @param {IncomingRequest} req | |||
* @param {Object} sess | |||
* @return {Session} | |||
* @api private | |||
*/ | |||
Store.prototype.createSession = function(req, sess){ | |||
var expires = sess.cookie.expires | |||
var originalMaxAge = sess.cookie.originalMaxAge | |||
sess.cookie = new Cookie(sess.cookie); | |||
if (typeof expires === 'string') { | |||
// convert expires to a Date object | |||
sess.cookie.expires = new Date(expires) | |||
} | |||
// keep originalMaxAge intact | |||
sess.cookie.originalMaxAge = originalMaxAge | |||
req.session = new Session(req, sess); | |||
return req.session; | |||
}; |
@ -1,16 +0,0 @@ | |||
The ISC License | |||
Copyright (c) Isaac Z. Schlueter | |||
Permission to use, copy, modify, and/or distribute this software for any | |||
purpose with or without fee is hereby granted, provided that the above | |||
copyright notice and this permission notice appear in all copies. | |||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH | |||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND | |||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, | |||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM | |||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR | |||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR | |||
PERFORMANCE OF THIS SOFTWARE. | |||
@ -1,42 +0,0 @@ | |||
Browser-friendly inheritance fully compatible with standard node.js | |||
[inherits](http://nodejs.org/api/util.html#util_util_inherits_constructor_superconstructor). | |||
This package exports standard `inherits` from node.js `util` module in | |||
node environment, but also provides alternative browser-friendly | |||
implementation through [browser | |||
field](https://gist.github.com/shtylman/4339901). Alternative | |||
implementation is a literal copy of standard one located in standalone | |||
module to avoid requiring of `util`. It also has a shim for old | |||
browsers with no `Object.create` support. | |||
While keeping you sure you are using standard `inherits` | |||
implementation in node.js environment, it allows bundlers such as | |||
[browserify](https://github.com/substack/node-browserify) to not | |||
include full `util` package to your client code if all you need is | |||
just `inherits` function. It worth, because browser shim for `util` | |||
package is large and `inherits` is often the single function you need | |||
from it. | |||
It's recommended to use this package instead of | |||
`require('util').inherits` for any code that has chances to be used | |||
not only in node.js but in browser too. | |||
## usage | |||
```js | |||
var inherits = require('inherits'); | |||
// then use exactly as the standard one | |||
``` | |||
## note on version ~1.0 | |||
Version ~1.0 had completely different motivation and is not compatible | |||
neither with 2.0 nor with standard node.js `inherits`. | |||
If you are using version ~1.0 and planning to switch to ~2.0, be | |||
careful: | |||
* new version uses `super_` instead of `super` for referencing | |||
superclass | |||
* new version overwrites current prototype while old one preserves any | |||
existing fields on it |
@ -1,7 +0,0 @@ | |||
try { | |||
var util = require('util'); | |||
if (typeof util.inherits !== 'function') throw ''; | |||
module.exports = util.inherits; | |||
} catch (e) { | |||
module.exports = require('./inherits_browser.js'); | |||
} |
@ -1,23 +0,0 @@ | |||
if (typeof Object.create === 'function') { | |||
// implementation from standard node.js 'util' module | |||
module.exports = function inherits(ctor, superCtor) { | |||
ctor.super_ = superCtor | |||
ctor.prototype = Object.create(superCtor.prototype, { | |||
constructor: { | |||
value: ctor, | |||
enumerable: false, | |||
writable: true, | |||
configurable: true | |||
} | |||
}); | |||
}; | |||
} else { | |||
// old school shim for old browsers | |||
module.exports = function inherits(ctor, superCtor) { | |||
ctor.super_ = superCtor | |||
var TempCtor = function () {} | |||
TempCtor.prototype = superCtor.prototype | |||
ctor.prototype = new TempCtor() | |||
ctor.prototype.constructor = ctor | |||
} | |||
} |
@ -1,29 +0,0 @@ | |||
{ | |||
"name": "inherits", | |||
"description": "Browser-friendly inheritance fully compatible with standard node.js inherits()", | |||
"version": "2.0.3", | |||
"keywords": [ | |||
"inheritance", | |||
"class", | |||
"klass", | |||
"oop", | |||
"object-oriented", | |||
"inherits", | |||
"browser", | |||
"browserify" | |||
], | |||
"main": "./inherits.js", | |||
"browser": "./inherits_browser.js", | |||
"repository": "git://github.com/isaacs/inherits", | |||
"license": "ISC", | |||
"scripts": { | |||
"test": "node test" | |||
}, | |||
"devDependencies": { | |||
"tap": "^7.1.0" | |||
}, | |||
"files": [ | |||
"inherits.js", | |||
"inherits_browser.js" | |||
] | |||
} |
@ -1 +0,0 @@ | |||
../../../sshpk/bin/sshpk-conv |
@ -1 +0,0 @@ | |||
../../../sshpk/bin/sshpk-sign |
@ -1 +0,0 @@ | |||
../../../sshpk/bin/sshpk-verify |
@ -1 +0,0 @@ | |||
repo_token: SIAeZjKYlHK74rbcFvNHMUzjRiMpflxve |
@ -1,14 +0,0 @@ | |||
{ | |||
"env": { | |||
"browser": true, | |||
"node": true | |||
}, | |||
"globals": { | |||
"chrome": true | |||
}, | |||
"rules": { | |||
"no-console": 0, | |||
"no-empty": [1, { "allowEmptyCatch": true }] | |||
}, | |||
"extends": "eslint:recommended" | |||
} |
@ -1,9 +0,0 @@ | |||
support | |||
test | |||
examples | |||
example | |||
*.sock | |||
dist | |||
yarn.lock | |||
coverage | |||
bower.json |
@ -1,20 +0,0 @@ | |||
sudo: false | |||
language: node_js | |||
node_js: | |||
- "4" | |||
- "6" | |||
- "8" | |||
install: | |||
- make install | |||
script: | |||
- make lint | |||
- make test | |||
matrix: | |||
include: | |||
- node_js: '8' | |||
env: BROWSER=1 |
@ -1,395 +0,0 @@ | |||
3.1.0 / 2017-09-26 | |||
================== | |||
* Add `DEBUG_HIDE_DATE` env var (#486) | |||
* Remove ReDoS regexp in %o formatter (#504) | |||
* Remove "component" from package.json | |||
* Remove `component.json` | |||
* Ignore package-lock.json | |||
* Examples: fix colors printout | |||
* Fix: browser detection | |||
* Fix: spelling mistake (#496, @EdwardBetts) | |||
3.0.1 / 2017-08-24 | |||
================== | |||
* Fix: Disable colors in Edge and Internet Explorer (#489) | |||
3.0.0 / 2017-08-08 | |||
================== | |||
* Breaking: Remove DEBUG_FD (#406) | |||
* Breaking: Use `Date#toISOString()` instead to `Date#toUTCString()` when output is not a TTY (#418) | |||
* Breaking: Make millisecond timer namespace specific and allow 'always enabled' output (#408) | |||
* Addition: document `enabled` flag (#465) | |||
* Addition: add 256 colors mode (#481) | |||
* Addition: `enabled()` updates existing debug instances, add `destroy()` function (#440) | |||
* Update: component: update "ms" to v2.0.0 | |||
* Update: separate the Node and Browser tests in Travis-CI | |||
* Update: refactor Readme, fixed documentation, added "Namespace Colors" section, redid screenshots | |||
* Update: separate Node.js and web browser examples for organization | |||
* Update: update "browserify" to v14.4.0 | |||
* Fix: fix Readme typo (#473) | |||
2.6.9 / 2017-09-22 | |||
================== | |||
* remove ReDoS regexp in %o formatter (#504) | |||
2.6.8 / 2017-05-18 | |||
================== | |||
* Fix: Check for undefined on browser globals (#462, @marbemac) | |||
2.6.7 / 2017-05-16 | |||
================== | |||
* Fix: Update ms to 2.0.0 to fix regular expression denial of service vulnerability (#458, @hubdotcom) | |||
* Fix: Inline extend function in node implementation (#452, @dougwilson) | |||
* Docs: Fix typo (#455, @msasad) | |||
2.6.5 / 2017-04-27 | |||
================== | |||
* Fix: null reference check on window.documentElement.style.WebkitAppearance (#447, @thebigredgeek) | |||
* Misc: clean up browser reference checks (#447, @thebigredgeek) | |||
* Misc: add npm-debug.log to .gitignore (@thebigredgeek) | |||
2.6.4 / 2017-04-20 | |||
================== | |||
* Fix: bug that would occur if process.env.DEBUG is a non-string value. (#444, @LucianBuzzo) | |||
* Chore: ignore bower.json in npm installations. (#437, @joaovieira) | |||
* Misc: update "ms" to v0.7.3 (@tootallnate) | |||
2.6.3 / 2017-03-13 | |||
================== | |||
* Fix: Electron reference to `process.env.DEBUG` (#431, @paulcbetts) | |||
* Docs: Changelog fix (@thebigredgeek) | |||
2.6.2 / 2017-03-10 | |||
================== | |||
* Fix: DEBUG_MAX_ARRAY_LENGTH (#420, @slavaGanzin) | |||
* Docs: Add backers and sponsors from Open Collective (#422, @piamancini) | |||
* Docs: Add Slackin invite badge (@tootallnate) | |||
2.6.1 / 2017-02-10 | |||
================== | |||
* Fix: Module's `export default` syntax fix for IE8 `Expected identifier` error | |||
* Fix: Whitelist DEBUG_FD for values 1 and 2 only (#415, @pi0) | |||
* Fix: IE8 "Expected identifier" error (#414, @vgoma) | |||
* Fix: Namespaces would not disable once enabled (#409, @musikov) | |||
2.6.0 / 2016-12-28 | |||
================== | |||
* Fix: added better null pointer checks for browser useColors (@thebigredgeek) | |||
* Improvement: removed explicit `window.debug` export (#404, @tootallnate) | |||
* Improvement: deprecated `DEBUG_FD` environment variable (#405, @tootallnate) | |||
2.5.2 / 2016-12-25 | |||
================== | |||
* Fix: reference error on window within webworkers (#393, @KlausTrainer) | |||
* Docs: fixed README typo (#391, @lurch) | |||
* Docs: added notice about v3 api discussion (@thebigredgeek) | |||
2.5.1 / 2016-12-20 | |||
================== | |||
* Fix: babel-core compatibility | |||
2.5.0 / 2016-12-20 | |||
================== | |||
* Fix: wrong reference in bower file (@thebigredgeek) | |||
* Fix: webworker compatibility (@thebigredgeek) | |||
* Fix: output formatting issue (#388, @kribblo) | |||
* Fix: babel-loader compatibility (#383, @escwald) | |||
* Misc: removed built asset from repo and publications (@thebigredgeek) | |||
* Misc: moved source files to /src (#378, @yamikuronue) | |||
* Test: added karma integration and replaced babel with browserify for browser tests (#378, @yamikuronue) | |||
* Test: coveralls integration (#378, @yamikuronue) | |||
* Docs: simplified language in the opening paragraph (#373, @yamikuronue) | |||
2.4.5 / 2016-12-17 | |||
================== | |||
* Fix: `navigator` undefined in Rhino (#376, @jochenberger) | |||
* Fix: custom log function (#379, @hsiliev) | |||
* Improvement: bit of cleanup + linting fixes (@thebigredgeek) | |||
* Improvement: rm non-maintainted `dist/` dir (#375, @freewil) | |||
* Docs: simplified language in the opening paragraph. (#373, @yamikuronue) | |||
2.4.4 / 2016-12-14 | |||
================== | |||
* Fix: work around debug being loaded in preload scripts for electron (#368, @paulcbetts) | |||
2.4.3 / 2016-12-14 | |||
================== | |||
* Fix: navigation.userAgent error for react native (#364, @escwald) | |||
2.4.2 / 2016-12-14 | |||
================== | |||
* Fix: browser colors (#367, @tootallnate) | |||
* Misc: travis ci integration (@thebigredgeek) | |||
* Misc: added linting and testing boilerplate with sanity check (@thebigredgeek) | |||
2.4.1 / 2016-12-13 | |||
================== | |||
* Fix: typo that broke the package (#356) | |||
2.4.0 / 2016-12-13 | |||
================== | |||
* Fix: bower.json references unbuilt src entry point (#342, @justmatt) | |||
* Fix: revert "handle regex special characters" (@tootallnate) | |||
* Feature: configurable util.inspect()`options for NodeJS (#327, @tootallnate) | |||
* Feature: %O`(big O) pretty-prints objects (#322, @tootallnate) | |||
* Improvement: allow colors in workers (#335, @botverse) | |||
* Improvement: use same color for same namespace. (#338, @lchenay) | |||
2.3.3 / 2016-11-09 | |||
================== | |||
* Fix: Catch `JSON.stringify()` errors (#195, Jovan Alleyne) | |||
* Fix: Returning `localStorage` saved values (#331, Levi Thomason) | |||
* Improvement: Don't create an empty object when no `process` (Nathan Rajlich) | |||
2.3.2 / 2016-11-09 | |||
================== | |||
* Fix: be super-safe in index.js as well (@TooTallNate) | |||
* Fix: should check whether process exists (Tom Newby) | |||
2.3.1 / 2016-11-09 | |||
================== | |||
* Fix: Added electron compatibility (#324, @paulcbetts) | |||
* Improvement: Added performance optimizations (@tootallnate) | |||
* Readme: Corrected PowerShell environment variable example (#252, @gimre) | |||
* Misc: Removed yarn lock file from source control (#321, @fengmk2) | |||
2.3.0 / 2016-11-07 | |||
================== | |||
* Fix: Consistent placement of ms diff at end of output (#215, @gorangajic) | |||
* Fix: Escaping of regex special characters in namespace strings (#250, @zacronos) | |||
* Fix: Fixed bug causing crash on react-native (#282, @vkarpov15) | |||
* Feature: Enabled ES6+ compatible import via default export (#212 @bucaran) | |||
* Feature: Added %O formatter to reflect Chrome's console.log capability (#279, @oncletom) | |||
* Package: Update "ms" to 0.7.2 (#315, @DevSide) | |||
* Package: removed superfluous version property from bower.json (#207 @kkirsche) | |||
* Readme: fix USE_COLORS to DEBUG_COLORS | |||
* Readme: Doc fixes for format string sugar (#269, @mlucool) | |||
* Readme: Updated docs for DEBUG_FD and DEBUG_COLORS environment variables (#232, @mattlyons0) | |||
* Readme: doc fixes for PowerShell (#271 #243, @exoticknight @unreadable) | |||
* Readme: better docs for browser support (#224, @matthewmueller) | |||
* Tooling: Added yarn integration for development (#317, @thebigredgeek) | |||
* Misc: Renamed History.md to CHANGELOG.md (@thebigredgeek) | |||
* Misc: Added license file (#226 #274, @CantemoInternal @sdaitzman) | |||
* Misc: Updated contributors (@thebigredgeek) | |||
2.2.0 / 2015-05-09 | |||
================== | |||
* package: update "ms" to v0.7.1 (#202, @dougwilson) | |||
* README: add logging to file example (#193, @DanielOchoa) | |||
* README: fixed a typo (#191, @amir-s) | |||
* browser: expose `storage` (#190, @stephenmathieson) | |||
* Makefile: add a `distclean` target (#189, @stephenmathieson) | |||
2.1.3 / 2015-03-13 | |||
================== | |||
* Updated stdout/stderr example (#186) | |||
* Updated example/stdout.js to match debug current behaviour | |||
* Renamed example/stderr.js to stdout.js | |||
* Update Readme.md (#184) | |||
* replace high intensity foreground color for bold (#182, #183) | |||
2.1.2 / 2015-03-01 | |||
================== | |||
* dist: recompile | |||
* update "ms" to v0.7.0 | |||
* package: update "browserify" to v9.0.3 | |||
* component: fix "ms.js" repo location | |||
* changed bower package name | |||
* updated documentation about using debug in a browser | |||
* fix: security error on safari (#167, #168, @yields) | |||
2.1.1 / 2014-12-29 | |||
================== | |||
* browser: use `typeof` to check for `console` existence | |||
* browser: check for `console.log` truthiness (fix IE 8/9) | |||
* browser: add support for Chrome apps | |||
* Readme: added Windows usage remarks | |||
* Add `bower.json` to properly support bower install | |||
2.1.0 / 2014-10-15 | |||
================== | |||
* node: implement `DEBUG_FD` env variable support | |||
* package: update "browserify" to v6.1.0 | |||
* package: add "license" field to package.json (#135, @panuhorsmalahti) | |||
2.0.0 / 2014-09-01 | |||
================== | |||
* package: update "browserify" to v5.11.0 | |||
* node: use stderr rather than stdout for logging (#29, @stephenmathieson) | |||
1.0.4 / 2014-07-15 | |||
================== | |||
* dist: recompile | |||
* example: remove `console.info()` log usage | |||
* example: add "Content-Type" UTF-8 header to browser example | |||
* browser: place %c marker after the space character | |||
* browser: reset the "content" color via `color: inherit` | |||
* browser: add colors support for Firefox >= v31 | |||
* debug: prefer an instance `log()` function over the global one (#119) | |||
* Readme: update documentation about styled console logs for FF v31 (#116, @wryk) | |||
1.0.3 / 2014-07-09 | |||
================== | |||
* Add support for multiple wildcards in namespaces (#122, @seegno) | |||
* browser: fix lint | |||
1.0.2 / 2014-06-10 | |||
================== | |||
* browser: update color palette (#113, @gscottolson) | |||
* common: make console logging function configurable (#108, @timoxley) | |||
* node: fix %o colors on old node <= 0.8.x | |||
* Makefile: find node path using shell/which (#109, @timoxley) | |||
1.0.1 / 2014-06-06 | |||
================== | |||
* browser: use `removeItem()` to clear localStorage | |||
* browser, node: don't set DEBUG if namespaces is undefined (#107, @leedm777) | |||
* package: add "contributors" section | |||
* node: fix comment typo | |||
* README: list authors | |||
1.0.0 / 2014-06-04 | |||
================== | |||
* make ms diff be global, not be scope | |||
* debug: ignore empty strings in enable() | |||
* node: make DEBUG_COLORS able to disable coloring | |||
* *: export the `colors` array | |||
* npmignore: don't publish the `dist` dir | |||
* Makefile: refactor to use browserify | |||
* package: add "browserify" as a dev dependency | |||
* Readme: add Web Inspector Colors section | |||
* node: reset terminal color for the debug content | |||
* node: map "%o" to `util.inspect()` | |||
* browser: map "%j" to `JSON.stringify()` | |||
* debug: add custom "formatters" | |||
* debug: use "ms" module for humanizing the diff | |||
* Readme: add "bash" syntax highlighting | |||
* browser: add Firebug color support | |||
* browser: add colors for WebKit browsers | |||
* node: apply log to `console` | |||
* rewrite: abstract common logic for Node & browsers | |||
* add .jshintrc file | |||
0.8.1 / 2014-04-14 | |||
================== | |||
* package: re-add the "component" section | |||
0.8.0 / 2014-03-30 | |||
================== | |||
* add `enable()` method for nodejs. Closes #27 | |||
* change from stderr to stdout | |||
* remove unnecessary index.js file | |||
0.7.4 / 2013-11-13 | |||
================== | |||
* remove "browserify" key from package.json (fixes something in browserify) | |||
0.7.3 / 2013-10-30 | |||
================== | |||
* fix: catch localStorage security error when cookies are blocked (Chrome) | |||
* add debug(err) support. Closes #46 | |||
* add .browser prop to package.json. Closes #42 | |||
0.7.2 / 2013-02-06 | |||
================== | |||
* fix package.json | |||
* fix: Mobile Safari (private mode) is broken with debug | |||
* fix: Use unicode to send escape character to shell instead of octal to work with strict mode javascript | |||
0.7.1 / 2013-02-05 | |||
================== | |||
* add repository URL to package.json | |||
* add DEBUG_COLORED to force colored output | |||
* add browserify support | |||
* fix component. Closes #24 | |||
0.7.0 / 2012-05-04 | |||
================== | |||
* Added .component to package.json | |||
* Added debug.component.js build | |||
0.6.0 / 2012-03-16 | |||
================== | |||
* Added support for "-" prefix in DEBUG [Vinay Pulim] | |||
* Added `.enabled` flag to the node version [TooTallNate] | |||
0.5.0 / 2012-02-02 | |||
================== | |||
* Added: humanize diffs. Closes #8 | |||
* Added `debug.disable()` to the CS variant | |||
* Removed padding. Closes #10 | |||
* Fixed: persist client-side variant again. Closes #9 | |||
0.4.0 / 2012-02-01 | |||
================== | |||
* Added browser variant support for older browsers [TooTallNate] | |||
* Added `debug.enable('project:*')` to browser variant [TooTallNate] | |||
* Added padding to diff (moved it to the right) | |||
0.3.0 / 2012-01-26 | |||
================== | |||
* Added millisecond diff when isatty, otherwise UTC string | |||
0.2.0 / 2012-01-22 | |||
================== | |||
* Added wildcard support | |||
0.1.0 / 2011-12-02 | |||
================== | |||
* Added: remove colors unless stderr isatty [TooTallNate] | |||
0.0.1 / 2010-01-03 | |||
================== | |||
* Initial release |
@ -1,19 +0,0 @@ | |||
(The MIT License) | |||
Copyright (c) 2014 TJ Holowaychuk <tj@vision-media.ca> | |||
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 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. | |||
@ -1,58 +0,0 @@ | |||
# get Makefile directory name: http://stackoverflow.com/a/5982798/376773 | |||
THIS_MAKEFILE_PATH:=$(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST)) | |||
THIS_DIR:=$(shell cd $(dir $(THIS_MAKEFILE_PATH));pwd) | |||
# BIN directory | |||
BIN := $(THIS_DIR)/node_modules/.bin | |||
# Path | |||
PATH := node_modules/.bin:$(PATH) | |||
SHELL := /bin/bash | |||
# applications | |||
NODE ?= $(shell which node) | |||
YARN ?= $(shell which yarn) | |||
PKG ?= $(if $(YARN),$(YARN),$(NODE) $(shell which npm)) | |||
BROWSERIFY ?= $(NODE) $(BIN)/browserify | |||
install: node_modules | |||
browser: dist/debug.js | |||
node_modules: package.json | |||
@NODE_ENV= $(PKG) install | |||
@touch node_modules | |||
dist/debug.js: src/*.js node_modules | |||
@mkdir -p dist | |||
@$(BROWSERIFY) \ | |||
--standalone debug \ | |||
. > dist/debug.js | |||
lint: | |||
@eslint *.js src/*.js | |||
test-node: | |||
@istanbul cover node_modules/mocha/bin/_mocha -- test/**.js | |||
@cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js | |||
test-browser: | |||
@$(MAKE) browser | |||
@karma start --single-run | |||
test-all: | |||
@concurrently \ | |||
"make test-node" \ | |||
"make test-browser" | |||
test: | |||
@if [ "x$(BROWSER)" = "x" ]; then \ | |||
$(MAKE) test-node; \ | |||
else \ | |||
$(MAKE) test-browser; \ | |||
fi | |||
clean: | |||
rimraf dist coverage | |||
.PHONY: browser install clean lint test test-all test-node test-browser |
@ -1,368 +0,0 @@ | |||
# debug | |||
[](https://travis-ci.org/visionmedia/debug) [](https://coveralls.io/github/visionmedia/debug?branch=master) [](https://visionmedia-community-slackin.now.sh/) [](#backers) | |||
[](#sponsors) | |||
<img width="647" src="https://user-images.githubusercontent.com/71256/29091486-fa38524c-7c37-11e7-895f-e7ec8e1039b6.png"> | |||
A tiny JavaScript debugging utility modelled after Node.js core's debugging | |||
technique. Works in Node.js and web browsers. | |||
## Installation | |||
```bash | |||
$ npm install debug | |||
``` | |||
## Usage | |||
`debug` exposes a function; simply pass this function the name of your module, and it will return a decorated version of `console.error` for you to pass debug statements to. This will allow you to toggle the debug output for different parts of your module as well as the module as a whole. | |||
Example [_app.js_](./examples/node/app.js): | |||
```js | |||
var debug = require('debug')('http') | |||
, http = require('http') | |||
, name = 'My App'; | |||
// fake app | |||
debug('booting %o', name); | |||
http.createServer(function(req, res){ | |||
debug(req.method + ' ' + req.url); | |||
res.end('hello\n'); | |||
}).listen(3000, function(){ | |||
debug('listening'); | |||
}); | |||
// fake worker of some kind | |||
require('./worker'); | |||
``` | |||
Example [_worker.js_](./examples/node/worker.js): | |||
```js | |||
var a = require('debug')('worker:a') | |||
, b = require('debug')('worker:b'); | |||
function work() { | |||
a('doing lots of uninteresting work'); | |||
setTimeout(work, Math.random() * 1000); | |||
} | |||
work(); | |||
function workb() { | |||
b('doing some work'); | |||
setTimeout(workb, Math.random() * 2000); | |||
} | |||
workb(); | |||
``` | |||
The `DEBUG` environment variable is then used to enable these based on space or | |||
comma-delimited names. | |||
Here are some examples: | |||
<img width="647" alt="screen shot 2017-08-08 at 12 53 04 pm" src="https://user-images.githubusercontent.com/71256/29091703-a6302cdc-7c38-11e7-8304-7c0b3bc600cd.png"> | |||
<img width="647" alt="screen shot 2017-08-08 at 12 53 38 pm" src="https://user-images.githubusercontent.com/71256/29091700-a62a6888-7c38-11e7-800b-db911291ca2b.png"> | |||
<img width="647" alt="screen shot 2017-08-08 at 12 53 25 pm" src="https://user-images.githubusercontent.com/71256/29091701-a62ea114-7c38-11e7-826a-2692bedca740.png"> | |||
#### Windows note | |||
On Windows the environment variable is set using the `set` command. | |||
```cmd | |||
set DEBUG=*,-not_this | |||
``` | |||
Note that PowerShell uses different syntax to set environment variables. | |||
```cmd | |||
$env:DEBUG = "*,-not_this" | |||
``` | |||
Then, run the program to be debugged as usual. | |||
## Namespace Colors | |||
Every debug instance has a color generated for it based on its namespace name. | |||
This helps when visually parsing the debug output to identify which debug instance | |||
a debug line belongs to. | |||
#### Node.js | |||
In Node.js, colors are enabled when stderr is a TTY. You also _should_ install | |||
the [`supports-color`](https://npmjs.org/supports-color) module alongside debug, | |||
otherwise debug will only use a small handful of basic colors. | |||
<img width="521" src="https://user-images.githubusercontent.com/71256/29092181-47f6a9e6-7c3a-11e7-9a14-1928d8a711cd.png"> | |||
#### Web Browser | |||
Colors are also enabled on "Web Inspectors" that understand the `%c` formatting | |||
option. These are WebKit web inspectors, Firefox ([since version | |||
31](https://hacks.mozilla.org/2014/05/editable-box-model-multiple-selection-sublime-text-keys-much-more-firefox-developer-tools-episode-31/)) | |||
and the Firebug plugin for Firefox (any version). | |||
<img width="524" src="https://user-images.githubusercontent.com/71256/29092033-b65f9f2e-7c39-11e7-8e32-f6f0d8e865c1.png"> | |||
## Millisecond diff | |||
When actively developing an application it can be useful to see when the time spent between one `debug()` call and the next. Suppose for example you invoke `debug()` before requesting a resource, and after as well, the "+NNNms" will show you how much time was spent between calls. | |||
<img width="647" src="https://user-images.githubusercontent.com/71256/29091486-fa38524c-7c37-11e7-895f-e7ec8e1039b6.png"> | |||
When stdout is not a TTY, `Date#toISOString()` is used, making it more useful for logging the debug information as shown below: | |||
<img width="647" src="https://user-images.githubusercontent.com/71256/29091956-6bd78372-7c39-11e7-8c55-c948396d6edd.png"> | |||
## Conventions | |||
If you're using this in one or more of your libraries, you _should_ use the name of your library so that developers may toggle debugging as desired without guessing names. If you have more than one debuggers you _should_ prefix them with your library name and use ":" to separate features. For example "bodyParser" from Connect would then be "connect:bodyParser". If you append a "*" to the end of your name, it will always be enabled regardless of the setting of the DEBUG environment variable. You can then use it for normal output as well as debug output. | |||
## Wildcards | |||
The `*` character may be used as a wildcard. Suppose for example your library has | |||
debuggers named "connect:bodyParser", "connect:compress", "connect:session", | |||
instead of listing all three with | |||
`DEBUG=connect:bodyParser,connect:compress,connect:session`, you may simply do | |||
`DEBUG=connect:*`, or to run everything using this module simply use `DEBUG=*`. | |||
You can also exclude specific debuggers by prefixing them with a "-" character. | |||
For example, `DEBUG=*,-connect:*` would include all debuggers except those | |||
starting with "connect:". | |||
## Environment Variables | |||
When running through Node.js, you can set a few environment variables that will | |||
change the behavior of the debug logging: | |||
| Name | Purpose | | |||
|-----------|-------------------------------------------------| | |||
| `DEBUG` | Enables/disables specific debugging namespaces. | | |||
| `DEBUG_HIDE_DATE` | Hide date from debug output (non-TTY). | | |||
| `DEBUG_COLORS`| Whether or not to use colors in the debug output. | | |||
| `DEBUG_DEPTH` | Object inspection depth. | | |||
| `DEBUG_SHOW_HIDDEN` | Shows hidden properties on inspected objects. | | |||
__Note:__ The environment variables beginning with `DEBUG_` end up being | |||
converted into an Options object that gets used with `%o`/`%O` formatters. | |||
See the Node.js documentation for | |||
[`util.inspect()`](https://nodejs.org/api/util.html#util_util_inspect_object_options) | |||
for the complete list. | |||
## Formatters | |||
Debug uses [printf-style](https://wikipedia.org/wiki/Printf_format_string) formatting. | |||
Below are the officially supported formatters: | |||
| Formatter | Representation | | |||
|-----------|----------------| | |||
| `%O` | Pretty-print an Object on multiple lines. | | |||
| `%o` | Pretty-print an Object all on a single line. | | |||
| `%s` | String. | | |||
| `%d` | Number (both integer and float). | | |||
| `%j` | JSON. Replaced with the string '[Circular]' if the argument contains circular references. | | |||
| `%%` | Single percent sign ('%'). This does not consume an argument. | | |||
### Custom formatters | |||
You can add custom formatters by extending the `debug.formatters` object. | |||
For example, if you wanted to add support for rendering a Buffer as hex with | |||
`%h`, you could do something like: | |||
```js | |||
const createDebug = require('debug') | |||
createDebug.formatters.h = (v) => { | |||
return v.toString('hex') | |||
} | |||
// …elsewhere | |||
const debug = createDebug('foo') | |||
debug('this is hex: %h', new Buffer('hello world')) | |||
// foo this is hex: 68656c6c6f20776f726c6421 +0ms | |||
``` | |||
## Browser Support | |||
You can build a browser-ready script using [browserify](https://github.com/substack/node-browserify), | |||
or just use the [browserify-as-a-service](https://wzrd.in/) [build](https://wzrd.in/standalone/debug@latest), | |||
if you don't want to build it yourself. | |||
Debug's enable state is currently persisted by `localStorage`. | |||
Consider the situation shown below where you have `worker:a` and `worker:b`, | |||
and wish to debug both. You can enable this using `localStorage.debug`: | |||
```js | |||
localStorage.debug = 'worker:*' | |||
``` | |||
And then refresh the page. | |||
```js | |||
a = debug('worker:a'); | |||
b = debug('worker:b'); | |||
setInterval(function(){ | |||
a('doing some work'); | |||
}, 1000); | |||
setInterval(function(){ | |||
b('doing some work'); | |||
}, 1200); | |||
``` | |||
## Output streams | |||
By default `debug` will log to stderr, however this can be configured per-namespace by overriding the `log` method: | |||
Example [_stdout.js_](./examples/node/stdout.js): | |||
```js | |||
var debug = require('debug'); | |||
var error = debug('app:error'); | |||
// by default stderr is used | |||
error('goes to stderr!'); | |||
var log = debug('app:log'); | |||
// set this namespace to log via console.log | |||
log.log = console.log.bind(console); // don't forget to bind to console! | |||
log('goes to stdout'); | |||
error('still goes to stderr!'); | |||
// set all output to go via console.info | |||
// overrides all per-namespace log settings | |||
debug.log = console.info.bind(console); | |||
error('now goes to stdout via console.info'); | |||
log('still goes to stdout, but via console.info now'); | |||
``` | |||
## Checking whether a debug target is enabled | |||
After you've created a debug instance, you can determine whether or not it is | |||
enabled by checking the `enabled` property: | |||
```javascript | |||
const debug = require('debug')('http'); | |||
if (debug.enabled) { | |||
// do stuff... | |||
} | |||
``` | |||
You can also manually toggle this property to force the debug instance to be | |||
enabled or disabled. | |||
## Authors | |||
- TJ Holowaychuk | |||
- Nathan Rajlich | |||
- Andrew Rhyne | |||
## Backers | |||
Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/debug#backer)] | |||
<a href="https://opencollective.com/debug/backer/0/website" target="_blank"><img src="https://opencollective.com/debug/backer/0/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/1/website" target="_blank"><img src="https://opencollective.com/debug/backer/1/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/2/website" target="_blank"><img src="https://opencollective.com/debug/backer/2/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/3/website" target="_blank"><img src="https://opencollective.com/debug/backer/3/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/4/website" target="_blank"><img src="https://opencollective.com/debug/backer/4/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/5/website" target="_blank"><img src="https://opencollective.com/debug/backer/5/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/6/website" target="_blank"><img src="https://opencollective.com/debug/backer/6/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/7/website" target="_blank"><img src="https://opencollective.com/debug/backer/7/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/8/website" target="_blank"><img src="https://opencollective.com/debug/backer/8/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/9/website" target="_blank"><img src="https://opencollective.com/debug/backer/9/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/10/website" target="_blank"><img src="https://opencollective.com/debug/backer/10/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/11/website" target="_blank"><img src="https://opencollective.com/debug/backer/11/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/12/website" target="_blank"><img src="https://opencollective.com/debug/backer/12/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/13/website" target="_blank"><img src="https://opencollective.com/debug/backer/13/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/14/website" target="_blank"><img src="https://opencollective.com/debug/backer/14/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/15/website" target="_blank"><img src="https://opencollective.com/debug/backer/15/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/16/website" target="_blank"><img src="https://opencollective.com/debug/backer/16/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/17/website" target="_blank"><img src="https://opencollective.com/debug/backer/17/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/18/website" target="_blank"><img src="https://opencollective.com/debug/backer/18/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/19/website" target="_blank"><img src="https://opencollective.com/debug/backer/19/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/20/website" target="_blank"><img src="https://opencollective.com/debug/backer/20/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/21/website" target="_blank"><img src="https://opencollective.com/debug/backer/21/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/22/website" target="_blank"><img src="https://opencollective.com/debug/backer/22/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/23/website" target="_blank"><img src="https://opencollective.com/debug/backer/23/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/24/website" target="_blank"><img src="https://opencollective.com/debug/backer/24/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/25/website" target="_blank"><img src="https://opencollective.com/debug/backer/25/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/26/website" target="_blank"><img src="https://opencollective.com/debug/backer/26/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/27/website" target="_blank"><img src="https://opencollective.com/debug/backer/27/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/28/website" target="_blank"><img src="https://opencollective.com/debug/backer/28/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/backer/29/website" target="_blank"><img src="https://opencollective.com/debug/backer/29/avatar.svg"></a> | |||
## Sponsors | |||
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/debug#sponsor)] | |||
<a href="https://opencollective.com/debug/sponsor/0/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/0/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/1/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/1/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/2/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/2/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/3/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/3/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/4/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/4/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/5/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/5/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/6/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/6/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/7/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/7/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/8/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/8/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/9/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/9/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/10/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/10/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/11/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/11/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/12/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/12/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/13/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/13/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/14/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/14/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/15/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/15/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/16/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/16/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/17/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/17/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/18/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/18/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/19/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/19/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/20/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/20/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/21/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/21/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/22/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/22/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/23/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/23/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/24/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/24/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/25/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/25/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/26/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/26/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/27/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/27/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/28/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/28/avatar.svg"></a> | |||
<a href="https://opencollective.com/debug/sponsor/29/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/29/avatar.svg"></a> | |||
## License | |||
(The MIT License) | |||
Copyright (c) 2014-2017 TJ Holowaychuk <tj@vision-media.ca> | |||
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 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. |
@ -1,70 +0,0 @@ | |||
// Karma configuration | |||
// Generated on Fri Dec 16 2016 13:09:51 GMT+0000 (UTC) | |||
module.exports = function(config) { | |||
config.set({ | |||
// base path that will be used to resolve all patterns (eg. files, exclude) | |||
basePath: '', | |||
// frameworks to use | |||
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter | |||
frameworks: ['mocha', 'chai', 'sinon'], | |||
// list of files / patterns to load in the browser | |||
files: [ | |||
'dist/debug.js', | |||
'test/*spec.js' | |||
], | |||
// list of files to exclude | |||
exclude: [ | |||
'src/node.js' | |||
], | |||
// preprocess matching files before serving them to the browser | |||
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor | |||
preprocessors: { | |||
}, | |||
// test results reporter to use | |||
// possible values: 'dots', 'progress' | |||
// available reporters: https://npmjs.org/browse/keyword/karma-reporter | |||
reporters: ['progress'], | |||
// web server port | |||
port: 9876, | |||
// enable / disable colors in the output (reporters and logs) | |||
colors: true, | |||
// level of logging | |||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG | |||
logLevel: config.LOG_INFO, | |||
// enable / disable watching file and executing tests whenever any file changes | |||
autoWatch: true, | |||
// start these browsers | |||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher | |||
browsers: ['PhantomJS'], | |||
// Continuous Integration mode | |||
// if true, Karma captures browsers, runs the tests and exits | |||
singleRun: false, | |||
// Concurrency level | |||
// how many browser should be started simultaneous | |||
concurrency: Infinity | |||
}) | |||
} |
@ -1 +0,0 @@ | |||
module.exports = require('./src/node'); |
@ -1,43 +0,0 @@ | |||
{ | |||
"name": "debug", | |||
"version": "3.1.0", | |||
"repository": { | |||
"type": "git", | |||
"url": "git://github.com/visionmedia/debug.git" | |||
}, | |||
"description": "small debugging utility", | |||
"keywords": [ | |||
"debug", | |||
"log", | |||
"debugger" | |||
], | |||
"author": "TJ Holowaychuk <tj@vision-media.ca>", | |||
"contributors": [ | |||
"Nathan Rajlich <nathan@tootallnate.net> (http://n8.io)", | |||
"Andrew Rhyne <rhyneandrew@gmail.com>" | |||
], | |||
"license": "MIT", | |||
"dependencies": { | |||
"ms": "2.0.0" | |||
}, | |||
"devDependencies": { | |||
"browserify": "14.4.0", | |||
"chai": "^3.5.0", | |||
"concurrently": "^3.1.0", | |||
"coveralls": "^2.11.15", | |||
"eslint": "^3.12.1", | |||
"istanbul": "^0.4.5", | |||
"karma": "^1.3.0", | |||
"karma-chai": "^0.1.0", | |||
"karma-mocha": "^1.3.0", | |||
"karma-phantomjs-launcher": "^1.0.2", | |||
"karma-sinon": "^1.0.5", | |||
"mocha": "^3.2.0", | |||
"mocha-lcov-reporter": "^1.2.0", | |||
"rimraf": "^2.5.4", | |||
"sinon": "^1.17.6", | |||
"sinon-chai": "^2.8.0" | |||
}, | |||
"main": "./src/index.js", | |||
"browser": "./src/browser.js" | |||
} |
@ -1,195 +0,0 @@ | |||
/** | |||
* This is the web browser implementation of `debug()`. | |||
* | |||
* Expose `debug()` as the module. | |||
*/ | |||
exports = module.exports = require('./debug'); | |||
exports.log = log; | |||
exports.formatArgs = formatArgs; | |||
exports.save = save; | |||
exports.load = load; | |||
exports.useColors = useColors; | |||
exports.storage = 'undefined' != typeof chrome | |||
&& 'undefined' != typeof chrome.storage | |||
? chrome.storage.local | |||
: localstorage(); | |||
/** | |||
* Colors. | |||
*/ | |||
exports.colors = [ | |||
'#0000CC', '#0000FF', '#0033CC', '#0033FF', '#0066CC', '#0066FF', '#0099CC', | |||
'#0099FF', '#00CC00', '#00CC33', '#00CC66', '#00CC99', '#00CCCC', '#00CCFF', | |||
'#3300CC', '#3300FF', '#3333CC', '#3333FF', '#3366CC', '#3366FF', '#3399CC', | |||
'#3399FF', '#33CC00', '#33CC33', '#33CC66', '#33CC99', '#33CCCC', '#33CCFF', | |||
'#6600CC', '#6600FF', '#6633CC', '#6633FF', '#66CC00', '#66CC33', '#9900CC', | |||
'#9900FF', '#9933CC', '#9933FF', '#99CC00', '#99CC33', '#CC0000', '#CC0033', | |||
'#CC0066', '#CC0099', '#CC00CC', '#CC00FF', '#CC3300', '#CC3333', '#CC3366', | |||
'#CC3399', '#CC33CC', '#CC33FF', '#CC6600', '#CC6633', '#CC9900', '#CC9933', | |||
'#CCCC00', '#CCCC33', '#FF0000', '#FF0033', '#FF0066', '#FF0099', '#FF00CC', | |||
'#FF00FF', '#FF3300', '#FF3333', '#FF3366', '#FF3399', '#FF33CC', '#FF33FF', | |||
'#FF6600', '#FF6633', '#FF9900', '#FF9933', '#FFCC00', '#FFCC33' | |||
]; | |||
/** | |||
* Currently only WebKit-based Web Inspectors, Firefox >= v31, | |||
* and the Firebug extension (any Firefox version) are known | |||
* to support "%c" CSS customizations. | |||
* | |||
* TODO: add a `localStorage` variable to explicitly enable/disable colors | |||
*/ | |||
function useColors() { | |||
// NB: In an Electron preload script, document will be defined but not fully | |||
// initialized. Since we know we're in Chrome, we'll just detect this case | |||
// explicitly | |||
if (typeof window !== 'undefined' && window.process && window.process.type === 'renderer') { | |||
return true; | |||
} | |||
// Internet Explorer and Edge do not support colors. | |||
if (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/)) { | |||
return false; | |||
} | |||
// is webkit? http://stackoverflow.com/a/16459606/376773 | |||
// document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 | |||
return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) || | |||
// is firebug? http://stackoverflow.com/a/398120/376773 | |||
(typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) || | |||
// is firefox >= v31? | |||
// https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages | |||
(typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31) || | |||
// double check webkit in userAgent just in case we are in a worker | |||
(typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)); | |||
} | |||
/** | |||
* Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. | |||
*/ | |||
exports.formatters.j = function(v) { | |||
try { | |||
return JSON.stringify(v); | |||
} catch (err) { | |||
return '[UnexpectedJSONParseError]: ' + err.message; | |||
} | |||
}; | |||
/** | |||
* Colorize log arguments if enabled. | |||
* | |||
* @api public | |||
*/ | |||
function formatArgs(args) { | |||
var useColors = this.useColors; | |||
args[0] = (useColors ? '%c' : '') | |||
+ this.namespace | |||
+ (useColors ? ' %c' : ' ') | |||
+ args[0] | |||
+ (useColors ? '%c ' : ' ') | |||
+ '+' + exports.humanize(this.diff); | |||
if (!useColors) return; | |||
var c = 'color: ' + this.color; | |||
args.splice(1, 0, c, 'color: inherit') | |||
// the final "%c" is somewhat tricky, because there could be other | |||
// arguments passed either before or after the %c, so we need to | |||
// figure out the correct index to insert the CSS into | |||
var index = 0; | |||
var lastC = 0; | |||
args[0].replace(/%[a-zA-Z%]/g, function(match) { | |||
if ('%%' === match) return; | |||
index++; | |||
if ('%c' === match) { | |||
// we only are interested in the *last* %c | |||
// (the user may have provided their own) | |||
lastC = index; | |||
} | |||
}); | |||
args.splice(lastC, 0, c); | |||
} | |||
/** | |||
* Invokes `console.log()` when available. | |||
* No-op when `console.log` is not a "function". | |||
* | |||
* @api public | |||
*/ | |||
function log() { | |||
// this hackery is required for IE8/9, where | |||
// the `console.log` function doesn't have 'apply' | |||
return 'object' === typeof console | |||
&& console.log | |||
&& Function.prototype.apply.call(console.log, console, arguments); | |||
} | |||
/** | |||
* Save `namespaces`. | |||
* | |||
* @param {String} namespaces | |||
* @api private | |||
*/ | |||
function save(namespaces) { | |||
try { | |||
if (null == namespaces) { | |||
exports.storage.removeItem('debug'); | |||
} else { | |||
exports.storage.debug = namespaces; | |||
} | |||
} catch(e) {} | |||
} | |||
/** | |||
* Load `namespaces`. | |||
* | |||
* @return {String} returns the previously persisted debug modes | |||
* @api private | |||
*/ | |||
function load() { | |||
var r; | |||
try { | |||
r = exports.storage.debug; | |||
} catch(e) {} | |||
// If debug isn't set in LS, and we're in Electron, try to load $DEBUG | |||
if (!r && typeof process !== 'undefined' && 'env' in process) { | |||
r = process.env.DEBUG; | |||
} | |||
return r; | |||
} | |||
/** | |||
* Enable namespaces listed in `localStorage.debug` initially. | |||
*/ | |||
exports.enable(load()); | |||
/** | |||
* Localstorage attempts to return the localstorage. | |||
* | |||
* This is necessary because safari throws | |||
* when a user disables cookies/localstorage | |||
* and you attempt to access it. | |||
* | |||
* @return {LocalStorage} | |||
* @api private | |||
*/ | |||
function localstorage() { | |||
try { | |||
return window.localStorage; | |||
} catch (e) {} | |||
} |
@ -1,225 +0,0 @@ | |||
/** | |||
* This is the common logic for both the Node.js and web browser | |||
* implementations of `debug()`. | |||
* | |||
* Expose `debug()` as the module. | |||
*/ | |||
exports = module.exports = createDebug.debug = createDebug['default'] = createDebug; | |||
exports.coerce = coerce; | |||
exports.disable = disable; | |||
exports.enable = enable; | |||
exports.enabled = enabled; | |||
exports.humanize = require('ms'); | |||
/** | |||
* Active `debug` instances. | |||
*/ | |||
exports.instances = []; | |||
/** | |||
* The currently active debug mode names, and names to skip. | |||
*/ | |||
exports.names = []; | |||
exports.skips = []; | |||
/** | |||
* Map of special "%n" handling functions, for the debug "format" argument. | |||
* | |||
* Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". | |||
*/ | |||
exports.formatters = {}; | |||
/** | |||
* Select a color. | |||
* @param {String} namespace | |||
* @return {Number} | |||
* @api private | |||
*/ | |||
function selectColor(namespace) { | |||
var hash = 0, i; | |||
for (i in namespace) { | |||
hash = ((hash << 5) - hash) + namespace.charCodeAt(i); | |||
hash |= 0; // Convert to 32bit integer | |||
} | |||
return exports.colors[Math.abs(hash) % exports.colors.length]; | |||
} | |||
/** | |||
* Create a debugger with the given `namespace`. | |||
* | |||
* @param {String} namespace | |||
* @return {Function} | |||
* @api public | |||
*/ | |||
function createDebug(namespace) { | |||
var prevTime; | |||
function debug() { | |||
// disabled? | |||
if (!debug.enabled) return; | |||
var self = debug; | |||
// set `diff` timestamp | |||
var curr = +new Date(); | |||
var ms = curr - (prevTime || curr); | |||
self.diff = ms; | |||
self.prev = prevTime; | |||
self.curr = curr; | |||
prevTime = curr; | |||
// turn the `arguments` into a proper Array | |||
var args = new Array(arguments.length); | |||
for (var i = 0; i < args.length; i++) { | |||
args[i] = arguments[i]; | |||
} | |||
args[0] = exports.coerce(args[0]); | |||
if ('string' !== typeof args[0]) { | |||
// anything else let's inspect with %O | |||
args.unshift('%O'); | |||
} | |||
// apply any `formatters` transformations | |||
var index = 0; | |||
args[0] = args[0].replace(/%([a-zA-Z%])/g, function(match, format) { | |||
// if we encounter an escaped % then don't increase the array index | |||
if (match === '%%') return match; | |||
index++; | |||
var formatter = exports.formatters[format]; | |||
if ('function' === typeof formatter) { | |||
var val = args[index]; | |||
match = formatter.call(self, val); | |||
// now we need to remove `args[index]` since it's inlined in the `format` | |||
args.splice(index, 1); | |||
index--; | |||
} | |||
return match; | |||
}); | |||
// apply env-specific formatting (colors, etc.) | |||
exports.formatArgs.call(self, args); | |||
var logFn = debug.log || exports.log || console.log.bind(console); | |||
logFn.apply(self, args); | |||
} | |||
debug.namespace = namespace; | |||
debug.enabled = exports.enabled(namespace); | |||
debug.useColors = exports.useColors(); | |||
debug.color = selectColor(namespace); | |||
debug.destroy = destroy; | |||
// env-specific initialization logic for debug instances | |||
if ('function' === typeof exports.init) { | |||
exports.init(debug); | |||
} | |||
exports.instances.push(debug); | |||
return debug; | |||
} | |||
function destroy () { | |||
var index = exports.instances.indexOf(this); | |||
if (index !== -1) { | |||
exports.instances.splice(index, 1); | |||
return true; | |||
} else { | |||
return false; | |||
} | |||
} | |||
/** | |||
* Enables a debug mode by namespaces. This can include modes | |||
* separated by a colon and wildcards. | |||
* | |||
* @param {String} namespaces | |||
* @api public | |||
*/ | |||
function enable(namespaces) { | |||
exports.save(namespaces); | |||
exports.names = []; | |||
exports.skips = []; | |||
var i; | |||
var split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/); | |||
var len = split.length; | |||
for (i = 0; i < len; i++) { | |||
if (!split[i]) continue; // ignore empty strings | |||
namespaces = split[i].replace(/\*/g, '.*?'); | |||
if (namespaces[0] === '-') { | |||
exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); | |||
} else { | |||
exports.names.push(new RegExp('^' + namespaces + '$')); | |||
} | |||
} | |||
for (i = 0; i < exports.instances.length; i++) { | |||
var instance = exports.instances[i]; | |||
instance.enabled = exports.enabled(instance.namespace); | |||
} | |||
} | |||
/** | |||
* Disable debug output. | |||
* | |||
* @api public | |||
*/ | |||
function disable() { | |||
exports.enable(''); | |||
} | |||
/** | |||
* Returns true if the given mode name is enabled, false otherwise. | |||
* | |||
* @param {String} name | |||
* @return {Boolean} | |||
* @api public | |||
*/ | |||
function enabled(name) { | |||
if (name[name.length - 1] === '*') { | |||
return true; | |||
} | |||
var i, len; | |||
for (i = 0, len = exports.skips.length; i < len; i++) { | |||
if (exports.skips[i].test(name)) { | |||
return false; | |||
} | |||
} | |||
for (i = 0, len = exports.names.length; i < len; i++) { | |||
if (exports.names[i].test(name)) { | |||
return true; | |||
} | |||
} | |||
return false; | |||
} | |||
/** | |||
* Coerce `val`. | |||
* | |||
* @param {Mixed} val | |||
* @return {Mixed} | |||
* @api private | |||
*/ | |||
function coerce(val) { | |||
if (val instanceof Error) return val.stack || val.message; | |||
return val; | |||
} |
@ -1,10 +0,0 @@ | |||
/** | |||
* Detect Electron renderer process, which is node, but we should | |||
* treat as a browser. | |||
*/ | |||
if (typeof process === 'undefined' || process.type === 'renderer') { | |||
module.exports = require('./browser.js'); | |||
} else { | |||
module.exports = require('./node.js'); | |||
} |
@ -1,186 +0,0 @@ | |||
/** | |||
* Module dependencies. | |||
*/ | |||
var tty = require('tty'); | |||
var util = require('util'); | |||
/** | |||
* This is the Node.js implementation of `debug()`. | |||
* | |||
* Expose `debug()` as the module. | |||
*/ | |||
exports = module.exports = require('./debug'); | |||
exports.init = init; | |||
exports.log = log; | |||
exports.formatArgs = formatArgs; | |||
exports.save = save; | |||
exports.load = load; | |||
exports.useColors = useColors; | |||
/** | |||
* Colors. | |||
*/ | |||
exports.colors = [ 6, 2, 3, 4, 5, 1 ]; | |||
try { | |||
var supportsColor = require('supports-color'); | |||
if (supportsColor && supportsColor.level >= 2) { | |||
exports.colors = [ | |||
20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62, 63, 68, | |||
69, 74, 75, 76, 77, 78, 79, 80, 81, 92, 93, 98, 99, 112, 113, 128, 129, 134, | |||
135, 148, 149, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, | |||
172, 173, 178, 179, 184, 185, 196, 197, 198, 199, 200, 201, 202, 203, 204, | |||
205, 206, 207, 208, 209, 214, 215, 220, 221 | |||
]; | |||
} | |||
} catch (err) { | |||
// swallow - we only care if `supports-color` is available; it doesn't have to be. | |||
} | |||
/** | |||
* Build up the default `inspectOpts` object from the environment variables. | |||
* | |||
* $ DEBUG_COLORS=no DEBUG_DEPTH=10 DEBUG_SHOW_HIDDEN=enabled node script.js | |||
*/ | |||
exports.inspectOpts = Object.keys(process.env).filter(function (key) { | |||
return /^debug_/i.test(key); | |||
}).reduce(function (obj, key) { | |||
// camel-case | |||
var prop = key | |||
.substring(6) | |||
.toLowerCase() | |||
.replace(/_([a-z])/g, function (_, k) { return k.toUpperCase() }); | |||
// coerce string value into JS value | |||
var val = process.env[key]; | |||
if (/^(yes|on|true|enabled)$/i.test(val)) val = true; | |||
else if (/^(no|off|false|disabled)$/i.test(val)) val = false; | |||
else if (val === 'null') val = null; | |||
else val = Number(val); | |||
obj[prop] = val; | |||
return obj; | |||
}, {}); | |||
/** | |||
* Is stdout a TTY? Colored output is enabled when `true`. | |||
*/ | |||
function useColors() { | |||
return 'colors' in exports.inspectOpts | |||
? Boolean(exports.inspectOpts.colors) | |||
: tty.isatty(process.stderr.fd); | |||
} | |||
/** | |||
* Map %o to `util.inspect()`, all on a single line. | |||
*/ | |||
exports.formatters.o = function(v) { | |||
this.inspectOpts.colors = this.useColors; | |||
return util.inspect(v, this.inspectOpts) | |||
.split('\n').map(function(str) { | |||
return str.trim() | |||
}).join(' '); | |||
}; | |||
/** | |||
* Map %o to `util.inspect()`, allowing multiple lines if needed. | |||
*/ | |||
exports.formatters.O = function(v) { | |||
this.inspectOpts.colors = this.useColors; | |||
return util.inspect(v, this.inspectOpts); | |||
}; | |||
/** | |||
* Adds ANSI color escape codes if enabled. | |||
* | |||
* @api public | |||
*/ | |||
function formatArgs(args) { | |||
var name = this.namespace; | |||
var useColors = this.useColors; | |||
if (useColors) { | |||
var c = this.color; | |||
var colorCode = '\u001b[3' + (c < 8 ? c : '8;5;' + c); | |||
var prefix = ' ' + colorCode + ';1m' + name + ' ' + '\u001b[0m'; | |||
args[0] = prefix + args[0].split('\n').join('\n' + prefix); | |||
args.push(colorCode + 'm+' + exports.humanize(this.diff) + '\u001b[0m'); | |||
} else { | |||
args[0] = getDate() + name + ' ' + args[0]; | |||
} | |||
} | |||
function getDate() { | |||
if (exports.inspectOpts.hideDate) { | |||
return ''; | |||
} else { | |||
return new Date().toISOString() + ' '; | |||
} | |||
} | |||
/** | |||
* Invokes `util.format()` with the specified arguments and writes to stderr. | |||
*/ | |||
function log() { | |||
return process.stderr.write(util.format.apply(util, arguments) + '\n'); | |||
} | |||
/** | |||
* Save `namespaces`. | |||
* | |||
* @param {String} namespaces | |||
* @api private | |||
*/ | |||
function save(namespaces) { | |||
if (null == namespaces) { | |||
// If you set a process.env field to null or undefined, it gets cast to the | |||
// string 'null' or 'undefined'. Just delete instead. | |||
delete process.env.DEBUG; | |||
} else { | |||
process.env.DEBUG = namespaces; | |||
} | |||
} | |||
/** | |||
* Load `namespaces`. | |||
* | |||
* @return {String} returns the previously persisted debug modes | |||
* @api private | |||
*/ | |||
function load() { | |||
return process.env.DEBUG; | |||
} | |||
/** | |||
* Init logic for `debug` instances. | |||
* | |||
* Create a new `inspectOpts` object in case `useColors` is set | |||
* differently for a particular `debug` instance. | |||
*/ | |||
function init (debug) { | |||
debug.inspectOpts = {}; | |||
var keys = Object.keys(exports.inspectOpts); | |||
for (var i = 0; i < keys.length; i++) { | |||
debug.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]]; | |||
} | |||
} | |||
/** | |||
* Enable namespaces listed in `process.env.DEBUG` initially. | |||
*/ | |||
exports.enable(load()); |
@ -1,103 +0,0 @@ | |||
2.0.0 / 2018-10-26 | |||
================== | |||
* Drop support for Node.js 0.6 | |||
* Replace internal `eval` usage with `Function` constructor | |||
* Use instance methods on `process` to check for listeners | |||
1.1.2 / 2018-01-11 | |||
================== | |||
* perf: remove argument reassignment | |||
* Support Node.js 0.6 to 9.x | |||
1.1.1 / 2017-07-27 | |||
================== | |||
* Remove unnecessary `Buffer` loading | |||
* Support Node.js 0.6 to 8.x | |||
1.1.0 / 2015-09-14 | |||
================== | |||
* Enable strict mode in more places | |||
* Support io.js 3.x | |||
* Support io.js 2.x | |||
* Support web browser loading | |||
- Requires bundler like Browserify or webpack | |||
1.0.1 / 2015-04-07 | |||
================== | |||
* Fix `TypeError`s when under `'use strict'` code | |||
* Fix useless type name on auto-generated messages | |||
* Support io.js 1.x | |||
* Support Node.js 0.12 | |||
1.0.0 / 2014-09-17 | |||
================== | |||
* No changes | |||
0.4.5 / 2014-09-09 | |||
================== | |||
* Improve call speed to functions using the function wrapper | |||
* Support Node.js 0.6 | |||
0.4.4 / 2014-07-27 | |||
================== | |||
* Work-around v8 generating empty stack traces | |||
0.4.3 / 2014-07-26 | |||
================== | |||
* Fix exception when global `Error.stackTraceLimit` is too low | |||
0.4.2 / 2014-07-19 | |||
================== | |||
* Correct call site for wrapped functions and properties | |||
0.4.1 / 2014-07-19 | |||
================== | |||
* Improve automatic message generation for function properties | |||
0.4.0 / 2014-07-19 | |||
================== | |||
* Add `TRACE_DEPRECATION` environment variable | |||
* Remove non-standard grey color from color output | |||
* Support `--no-deprecation` argument | |||
* Support `--trace-deprecation` argument | |||
* Support `deprecate.property(fn, prop, message)` | |||
0.3.0 / 2014-06-16 | |||
================== | |||
* Add `NO_DEPRECATION` environment variable | |||
0.2.0 / 2014-06-15 | |||
================== | |||
* Add `deprecate.property(obj, prop, message)` | |||
* Remove `supports-color` dependency for node.js 0.8 | |||
0.1.0 / 2014-06-15 | |||
================== | |||
* Add `deprecate.function(fn, message)` | |||
* Add `process.on('deprecation', fn)` emitter | |||
* Automatically generate message when omitted from `deprecate()` | |||
0.0.1 / 2014-06-15 | |||
================== | |||
* Fix warning for dynamic calls at singe call site | |||
0.0.0 / 2014-06-15 | |||
================== | |||
* Initial implementation |
@ -1,22 +0,0 @@ | |||
(The MIT License) | |||
Copyright (c) 2014-2018 Douglas Christopher Wilson | |||
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 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. |
@ -1,280 +0,0 @@ | |||
# depd | |||
[![NPM Version][npm-version-image]][npm-url] | |||
[![NPM Downloads][npm-downloads-image]][npm-url] | |||
[![Node.js Version][node-image]][node-url] | |||
[![Linux Build][travis-image]][travis-url] | |||
[![Windows Build][appveyor-image]][appveyor-url] | |||
[![Coverage Status][coveralls-image]][coveralls-url] | |||
Deprecate all the things | |||
> With great modules comes great responsibility; mark things deprecated! | |||
## Install | |||
This module is installed directly using `npm`: | |||
```sh | |||
$ npm install depd | |||
``` | |||
This module can also be bundled with systems like | |||
[Browserify](http://browserify.org/) or [webpack](https://webpack.github.io/), | |||
though by default this module will alter it's API to no longer display or | |||
track deprecations. | |||
## API | |||
<!-- eslint-disable no-unused-vars --> | |||
```js | |||
var deprecate = require('depd')('my-module') | |||
``` | |||
This library allows you to display deprecation messages to your users. | |||
This library goes above and beyond with deprecation warnings by | |||
introspection of the call stack (but only the bits that it is interested | |||
in). | |||
Instead of just warning on the first invocation of a deprecated | |||
function and never again, this module will warn on the first invocation | |||
of a deprecated function per unique call site, making it ideal to alert | |||
users of all deprecated uses across the code base, rather than just | |||
whatever happens to execute first. | |||
The deprecation warnings from this module also include the file and line | |||
information for the call into the module that the deprecated function was | |||
in. | |||
**NOTE** this library has a similar interface to the `debug` module, and | |||
this module uses the calling file to get the boundary for the call stacks, | |||
so you should always create a new `deprecate` object in each file and not | |||
within some central file. | |||
### depd(namespace) | |||
Create a new deprecate function that uses the given namespace name in the | |||
messages and will display the call site prior to the stack entering the | |||
file this function was called from. It is highly suggested you use the | |||
name of your module as the namespace. | |||
### deprecate(message) | |||
Call this function from deprecated code to display a deprecation message. | |||
This message will appear once per unique caller site. Caller site is the | |||
first call site in the stack in a different file from the caller of this | |||
function. | |||
If the message is omitted, a message is generated for you based on the site | |||
of the `deprecate()` call and will display the name of the function called, | |||
similar to the name displayed in a stack trace. | |||
### deprecate.function(fn, message) | |||
Call this function to wrap a given function in a deprecation message on any | |||
call to the function. An optional message can be supplied to provide a custom | |||
message. | |||
### deprecate.property(obj, prop, message) | |||
Call this function to wrap a given property on object in a deprecation message | |||
on any accessing or setting of the property. An optional message can be supplied | |||
to provide a custom message. | |||
The method must be called on the object where the property belongs (not | |||
inherited from the prototype). | |||
If the property is a data descriptor, it will be converted to an accessor | |||
descriptor in order to display the deprecation message. | |||
### process.on('deprecation', fn) | |||
This module will allow easy capturing of deprecation errors by emitting the | |||
errors as the type "deprecation" on the global `process`. If there are no | |||
listeners for this type, the errors are written to STDERR as normal, but if | |||
there are any listeners, nothing will be written to STDERR and instead only | |||
emitted. From there, you can write the errors in a different format or to a | |||
logging source. | |||
The error represents the deprecation and is emitted only once with the same | |||
rules as writing to STDERR. The error has the following properties: | |||
- `message` - This is the message given by the library | |||
- `name` - This is always `'DeprecationError'` | |||
- `namespace` - This is the namespace the deprecation came from | |||
- `stack` - This is the stack of the call to the deprecated thing | |||
Example `error.stack` output: | |||
``` | |||
DeprecationError: my-cool-module deprecated oldfunction | |||
at Object.<anonymous> ([eval]-wrapper:6:22) | |||
at Module._compile (module.js:456:26) | |||
at evalScript (node.js:532:25) | |||
at startup (node.js:80:7) | |||
at node.js:902:3 | |||
``` | |||
### process.env.NO_DEPRECATION | |||
As a user of modules that are deprecated, the environment variable `NO_DEPRECATION` | |||
is provided as a quick solution to silencing deprecation warnings from being | |||
output. The format of this is similar to that of `DEBUG`: | |||
```sh | |||
$ NO_DEPRECATION=my-module,othermod node app.js | |||
``` | |||
This will suppress deprecations from being output for "my-module" and "othermod". | |||
The value is a list of comma-separated namespaces. To suppress every warning | |||
across all namespaces, use the value `*` for a namespace. | |||
Providing the argument `--no-deprecation` to the `node` executable will suppress | |||
all deprecations (only available in Node.js 0.8 or higher). | |||
**NOTE** This will not suppress the deperecations given to any "deprecation" | |||
event listeners, just the output to STDERR. | |||
### process.env.TRACE_DEPRECATION | |||
As a user of modules that are deprecated, the environment variable `TRACE_DEPRECATION` | |||
is provided as a solution to getting more detailed location information in deprecation | |||
warnings by including the entire stack trace. The format of this is the same as | |||
`NO_DEPRECATION`: | |||
```sh | |||
$ TRACE_DEPRECATION=my-module,othermod node app.js | |||
``` | |||
This will include stack traces for deprecations being output for "my-module" and | |||
"othermod". The value is a list of comma-separated namespaces. To trace every | |||
warning across all namespaces, use the value `*` for a namespace. | |||
Providing the argument `--trace-deprecation` to the `node` executable will trace | |||
all deprecations (only available in Node.js 0.8 or higher). | |||
**NOTE** This will not trace the deperecations silenced by `NO_DEPRECATION`. | |||
## Display | |||
 | |||
When a user calls a function in your library that you mark deprecated, they | |||
will see the following written to STDERR (in the given colors, similar colors | |||
and layout to the `debug` module): | |||
``` | |||
bright cyan bright yellow | |||
| | reset cyan | |||
| | | | | |||
▼ ▼ ▼ ▼ | |||
my-cool-module deprecated oldfunction [eval]-wrapper:6:22 | |||
▲ ▲ ▲ ▲ | |||
| | | | | |||
namespace | | location of mycoolmod.oldfunction() call | |||
| deprecation message | |||
the word "deprecated" | |||
``` | |||
If the user redirects their STDERR to a file or somewhere that does not support | |||
colors, they see (similar layout to the `debug` module): | |||
``` | |||
Sun, 15 Jun 2014 05:21:37 GMT my-cool-module deprecated oldfunction at [eval]-wrapper:6:22 | |||
▲ ▲ ▲ ▲ ▲ | |||
| | | | | | |||
timestamp of message namespace | | location of mycoolmod.oldfunction() call | |||
| deprecation message | |||
the word "deprecated" | |||
``` | |||
## Examples | |||
### Deprecating all calls to a function | |||
This will display a deprecated message about "oldfunction" being deprecated | |||
from "my-module" on STDERR. | |||
```js | |||
var deprecate = require('depd')('my-cool-module') | |||
// message automatically derived from function name | |||
// Object.oldfunction | |||
exports.oldfunction = deprecate.function(function oldfunction () { | |||
// all calls to function are deprecated | |||
}) | |||
// specific message | |||
exports.oldfunction = deprecate.function(function () { | |||
// all calls to function are deprecated | |||
}, 'oldfunction') | |||
``` | |||
### Conditionally deprecating a function call | |||
This will display a deprecated message about "weirdfunction" being deprecated | |||
from "my-module" on STDERR when called with less than 2 arguments. | |||
```js | |||
var deprecate = require('depd')('my-cool-module') | |||
exports.weirdfunction = function () { | |||
if (arguments.length < 2) { | |||
// calls with 0 or 1 args are deprecated | |||
deprecate('weirdfunction args < 2') | |||
} | |||
} | |||
``` | |||
When calling `deprecate` as a function, the warning is counted per call site | |||
within your own module, so you can display different deprecations depending | |||
on different situations and the users will still get all the warnings: | |||
```js | |||
var deprecate = require('depd')('my-cool-module') | |||
exports.weirdfunction = function () { | |||
if (arguments.length < 2) { | |||
// calls with 0 or 1 args are deprecated | |||
deprecate('weirdfunction args < 2') | |||
} else if (typeof arguments[0] !== 'string') { | |||
// calls with non-string first argument are deprecated | |||
deprecate('weirdfunction non-string first arg') | |||
} | |||
} | |||
``` | |||
### Deprecating property access | |||
This will display a deprecated message about "oldprop" being deprecated | |||
from "my-module" on STDERR when accessed. A deprecation will be displayed | |||
when setting the value and when getting the value. | |||
```js | |||
var deprecate = require('depd')('my-cool-module') | |||
exports.oldprop = 'something' | |||
// message automatically derives from property name | |||
deprecate.property(exports, 'oldprop') | |||
// explicit message | |||
deprecate.property(exports, 'oldprop', 'oldprop >= 0.10') | |||
``` | |||
## License | |||
[MIT](LICENSE) | |||
[appveyor-image]: https://badgen.net/appveyor/ci/dougwilson/nodejs-depd/master?label=windows | |||
[appveyor-url]: https://ci.appveyor.com/project/dougwilson/nodejs-depd | |||
[coveralls-image]: https://badgen.net/coveralls/c/github/dougwilson/nodejs-depd/master | |||
[coveralls-url]: https://coveralls.io/r/dougwilson/nodejs-depd?branch=master | |||
[node-image]: https://badgen.net/npm/node/depd | |||
[node-url]: https://nodejs.org/en/download/ | |||
[npm-downloads-image]: https://badgen.net/npm/dm/depd | |||
[npm-url]: https://npmjs.org/package/depd | |||
[npm-version-image]: https://badgen.net/npm/v/depd | |||
[travis-image]: https://badgen.net/travis/dougwilson/nodejs-depd/master?label=linux | |||
[travis-url]: https://travis-ci.org/dougwilson/nodejs-depd |
@ -1,538 +0,0 @@ | |||
/*! | |||
* depd | |||
* Copyright(c) 2014-2018 Douglas Christopher Wilson | |||
* MIT Licensed | |||
*/ | |||
/** | |||
* Module dependencies. | |||
*/ | |||
var relative = require('path').relative | |||
/** | |||
* Module exports. | |||
*/ | |||
module.exports = depd | |||
/** | |||
* Get the path to base files on. | |||
*/ | |||
var basePath = process.cwd() | |||
/** | |||
* Determine if namespace is contained in the string. | |||
*/ | |||
function containsNamespace (str, namespace) { | |||
var vals = str.split(/[ ,]+/) | |||
var ns = String(namespace).toLowerCase() | |||
for (var i = 0; i < vals.length; i++) { | |||
var val = vals[i] | |||
// namespace contained | |||
if (val && (val === '*' || val.toLowerCase() === ns)) { | |||
return true | |||
} | |||
} | |||
return false | |||
} | |||
/** | |||
* Convert a data descriptor to accessor descriptor. | |||
*/ | |||
function convertDataDescriptorToAccessor (obj, prop, message) { | |||
var descriptor = Object.getOwnPropertyDescriptor(obj, prop) | |||
var value = descriptor.value | |||
descriptor.get = function getter () { return value } | |||
if (descriptor.writable) { | |||
descriptor.set = function setter (val) { return (value = val) } | |||
} | |||
delete descriptor.value | |||
delete descriptor.writable | |||
Object.defineProperty(obj, prop, descriptor) | |||
return descriptor | |||
} | |||
/** | |||
* Create arguments string to keep arity. | |||
*/ | |||
function createArgumentsString (arity) { | |||
var str = '' | |||
for (var i = 0; i < arity; i++) { | |||
str += ', arg' + i | |||
} | |||
return str.substr(2) | |||
} | |||
/** | |||
* Create stack string from stack. | |||
*/ | |||
function createStackString (stack) { | |||
var str = this.name + ': ' + this.namespace | |||
if (this.message) { | |||
str += ' deprecated ' + this.message | |||
} | |||
for (var i = 0; i < stack.length; i++) { | |||
str += '\n at ' + stack[i].toString() | |||
} | |||
return str | |||
} | |||
/** | |||
* Create deprecate for namespace in caller. | |||
*/ | |||
function depd (namespace) { | |||
if (!namespace) { | |||
throw new TypeError('argument namespace is required') | |||
} | |||
var stack = getStack() | |||
var site = callSiteLocation(stack[1]) | |||
var file = site[0] | |||
function deprecate (message) { | |||
// call to self as log | |||
log.call(deprecate, message) | |||
} | |||
deprecate._file = file | |||
deprecate._ignored = isignored(namespace) | |||
deprecate._namespace = namespace | |||
deprecate._traced = istraced(namespace) | |||
deprecate._warned = Object.create(null) | |||
deprecate.function = wrapfunction | |||
deprecate.property = wrapproperty | |||
return deprecate | |||
} | |||
/** | |||
* Determine if event emitter has listeners of a given type. | |||
* | |||
* The way to do this check is done three different ways in Node.js >= 0.8 | |||
* so this consolidates them into a minimal set using instance methods. | |||
* | |||
* @param {EventEmitter} emitter | |||
* @param {string} type | |||
* @returns {boolean} | |||
* @private | |||
*/ | |||
function eehaslisteners (emitter, type) { | |||
var count = typeof emitter.listenerCount !== 'function' | |||
? emitter.listeners(type).length | |||
: emitter.listenerCount(type) | |||
return count > 0 | |||
} | |||
/** | |||
* Determine if namespace is ignored. | |||
*/ | |||
function isignored (namespace) { | |||
if (process.noDeprecation) { | |||
// --no-deprecation support | |||
return true | |||
} | |||
var str = process.env.NO_DEPRECATION || '' | |||
// namespace ignored | |||
return containsNamespace(str, namespace) | |||
} | |||
/** | |||
* Determine if namespace is traced. | |||
*/ | |||
function istraced (namespace) { | |||
if (process.traceDeprecation) { | |||
// --trace-deprecation support | |||
return true | |||
} | |||
var str = process.env.TRACE_DEPRECATION || '' | |||
// namespace traced | |||
return containsNamespace(str, namespace) | |||
} | |||
/** | |||
* Display deprecation message. | |||
*/ | |||
function log (message, site) { | |||
var haslisteners = eehaslisteners(process, 'deprecation') | |||
// abort early if no destination | |||
if (!haslisteners && this._ignored) { | |||
return | |||
} | |||
var caller | |||
var callFile | |||
var callSite | |||
var depSite | |||
var i = 0 | |||
var seen = false | |||
var stack = getStack() | |||
var file = this._file | |||
if (site) { | |||
// provided site | |||
depSite = site | |||
callSite = callSiteLocation(stack[1]) | |||
callSite.name = depSite.name | |||
file = callSite[0] | |||
} else { | |||
// get call site | |||
i = 2 | |||
depSite = callSiteLocation(stack[i]) | |||
callSite = depSite | |||
} | |||
// get caller of deprecated thing in relation to file | |||
for (; i < stack.length; i++) { | |||
caller = callSiteLocation(stack[i]) | |||
callFile = caller[0] | |||
if (callFile === file) { | |||
seen = true | |||
} else if (callFile === this._file) { | |||
file = this._file | |||
} else if (seen) { | |||
break | |||
} | |||
} | |||
var key = caller | |||
? depSite.join(':') + '__' + caller.join(':') | |||
: undefined | |||
if (key !== undefined && key in this._warned) { | |||
// already warned | |||
return | |||
} | |||
this._warned[key] = true | |||
// generate automatic message from call site | |||
var msg = message | |||
if (!msg) { | |||
msg = callSite === depSite || !callSite.name | |||
? defaultMessage(depSite) | |||
: defaultMessage(callSite) | |||
} | |||
// emit deprecation if listeners exist | |||
if (haslisteners) { | |||
var err = DeprecationError(this._namespace, msg, stack.slice(i)) | |||
process.emit('deprecation', err) | |||
return | |||
} | |||
// format and write message | |||
var format = process.stderr.isTTY | |||
? formatColor | |||
: formatPlain | |||
var output = format.call(this, msg, caller, stack.slice(i)) | |||
process.stderr.write(output + '\n', 'utf8') | |||
} | |||
/** | |||
* Get call site location as array. | |||
*/ | |||
function callSiteLocation (callSite) { | |||
var file = callSite.getFileName() || '<anonymous>' | |||
var line = callSite.getLineNumber() | |||
var colm = callSite.getColumnNumber() | |||
if (callSite.isEval()) { | |||
file = callSite.getEvalOrigin() + ', ' + file | |||
} | |||
var site = [file, line, colm] | |||
site.callSite = callSite | |||
site.name = callSite.getFunctionName() | |||
return site | |||
} | |||
/** | |||
* Generate a default message from the site. | |||
*/ | |||
function defaultMessage (site) { | |||
var callSite = site.callSite | |||
var funcName = site.name | |||
// make useful anonymous name | |||
if (!funcName) { | |||
funcName = '<anonymous@' + formatLocation(site) + '>' | |||
} | |||
var context = callSite.getThis() | |||
var typeName = context && callSite.getTypeName() | |||
// ignore useless type name | |||
if (typeName === 'Object') { | |||
typeName = undefined | |||
} | |||
// make useful type name | |||
if (typeName === 'Function') { | |||
typeName = context.name || typeName | |||
} | |||
return typeName && callSite.getMethodName() | |||
? typeName + '.' + funcName | |||
: funcName | |||
} | |||
/** | |||
* Format deprecation message without color. | |||
*/ | |||
function formatPlain (msg, caller, stack) { | |||
var timestamp = new Date().toUTCString() | |||
var formatted = timestamp + | |||
' ' + this._namespace + | |||
' deprecated ' + msg | |||
// add stack trace | |||
if (this._traced) { | |||
for (var i = 0; i < stack.length; i++) { | |||
formatted += '\n at ' + stack[i].toString() | |||
} | |||
return formatted | |||
} | |||
if (caller) { | |||
formatted += ' at ' + formatLocation(caller) | |||
} | |||
return formatted | |||
} | |||
/** | |||
* Format deprecation message with color. | |||
*/ | |||
function formatColor (msg, caller, stack) { | |||
var formatted = '\x1b[36;1m' + this._namespace + '\x1b[22;39m' + // bold cyan | |||
' \x1b[33;1mdeprecated\x1b[22;39m' + // bold yellow | |||
' \x1b[0m' + msg + '\x1b[39m' // reset | |||
// add stack trace | |||
if (this._traced) { | |||
for (var i = 0; i < stack.length; i++) { | |||
formatted += '\n \x1b[36mat ' + stack[i].toString() + '\x1b[39m' // cyan | |||
} | |||
return formatted | |||
} | |||
if (caller) { | |||
formatted += ' \x1b[36m' + formatLocation(caller) + '\x1b[39m' // cyan | |||
} | |||
return formatted | |||
} | |||
/** | |||
* Format call site location. | |||
*/ | |||
function formatLocation (callSite) { | |||
return relative(basePath, callSite[0]) + | |||
':' + callSite[1] + | |||
':' + callSite[2] | |||
} | |||
/** | |||
* Get the stack as array of call sites. | |||
*/ | |||
function getStack () { | |||
var limit = Error.stackTraceLimit | |||
var obj = {} | |||
var prep = Error.prepareStackTrace | |||
Error.prepareStackTrace = prepareObjectStackTrace | |||
Error.stackTraceLimit = Math.max(10, limit) | |||
// capture the stack | |||
Error.captureStackTrace(obj) | |||
// slice this function off the top | |||
var stack = obj.stack.slice(1) | |||
Error.prepareStackTrace = prep | |||
Error.stackTraceLimit = limit | |||
return stack | |||
} | |||
/** | |||
* Capture call site stack from v8. | |||
*/ | |||
function prepareObjectStackTrace (obj, stack) { | |||
return stack | |||
} | |||
/** | |||
* Return a wrapped function in a deprecation message. | |||
*/ | |||
function wrapfunction (fn, message) { | |||
if (typeof fn !== 'function') { | |||
throw new TypeError('argument fn must be a function') | |||
} | |||
var args = createArgumentsString(fn.length) | |||
var stack = getStack() | |||
var site = callSiteLocation(stack[1]) | |||
site.name = fn.name | |||
// eslint-disable-next-line no-new-func | |||
var deprecatedfn = new Function('fn', 'log', 'deprecate', 'message', 'site', | |||
'"use strict"\n' + | |||
'return function (' + args + ') {' + | |||
'log.call(deprecate, message, site)\n' + | |||
'return fn.apply(this, arguments)\n' + | |||
'}')(fn, log, this, message, site) | |||
return deprecatedfn | |||
} | |||
/** | |||
* Wrap property in a deprecation message. | |||
*/ | |||
function wrapproperty (obj, prop, message) { | |||
if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) { | |||
throw new TypeError('argument obj must be object') | |||
} | |||
var descriptor = Object.getOwnPropertyDescriptor(obj, prop) | |||
if (!descriptor) { | |||
throw new TypeError('must call property on owner object') | |||
} | |||
if (!descriptor.configurable) { | |||
throw new TypeError('property must be configurable') | |||
} | |||
var deprecate = this | |||
var stack = getStack() | |||
var site = callSiteLocation(stack[1]) | |||
// set site name | |||
site.name = prop | |||
// convert data descriptor | |||
if ('value' in descriptor) { | |||
descriptor = convertDataDescriptorToAccessor(obj, prop, message) | |||
} | |||
var get = descriptor.get | |||
var set = descriptor.set | |||
// wrap getter | |||
if (typeof get === 'function') { | |||
descriptor.get = function getter () { | |||
log.call(deprecate, message, site) | |||
return get.apply(this, arguments) | |||
} | |||
} | |||
// wrap setter | |||
if (typeof set === 'function') { | |||
descriptor.set = function setter () { | |||
log.call(deprecate, message, site) | |||
return set.apply(this, arguments) | |||
} | |||
} | |||
Object.defineProperty(obj, prop, descriptor) | |||
} | |||
/** | |||
* Create DeprecationError for deprecation | |||
*/ | |||
function DeprecationError (namespace, message, stack) { | |||
var error = new Error() | |||
var stackString | |||
Object.defineProperty(error, 'constructor', { | |||
value: DeprecationError | |||
}) | |||
Object.defineProperty(error, 'message', { | |||
configurable: true, | |||
enumerable: false, | |||
value: message, | |||
writable: true | |||
}) | |||
Object.defineProperty(error, 'name', { | |||
enumerable: false, | |||
configurable: true, | |||
value: 'DeprecationError', | |||
writable: true | |||
}) | |||
Object.defineProperty(error, 'namespace', { | |||
configurable: true, | |||
enumerable: false, | |||
value: namespace, | |||
writable: true | |||
}) | |||
Object.defineProperty(error, 'stack', { | |||
configurable: true, | |||
enumerable: false, | |||
get: function () { | |||
if (stackString !== undefined) { | |||
return stackString | |||
} | |||
// prepare stack trace | |||
return (stackString = createStackString.call(this, stack)) | |||
}, | |||
set: function setter (val) { | |||
stackString = val | |||
} | |||
}) | |||
return error | |||
} |
@ -1,77 +0,0 @@ | |||
/*! | |||
* depd | |||
* Copyright(c) 2015 Douglas Christopher Wilson | |||
* MIT Licensed | |||
*/ | |||
'use strict' | |||
/** | |||
* Module exports. | |||
* @public | |||
*/ | |||
module.exports = depd | |||
/** | |||
* Create deprecate for namespace in caller. | |||
*/ | |||
function depd (namespace) { | |||
if (!namespace) { | |||
throw new TypeError('argument namespace is required') | |||
} | |||
function deprecate (message) { | |||
// no-op in browser | |||
} | |||
deprecate._file = undefined | |||
deprecate._ignored = true | |||
deprecate._namespace = namespace | |||
deprecate._traced = false | |||
deprecate._warned = Object.create(null) | |||
deprecate.function = wrapfunction | |||
deprecate.property = wrapproperty | |||
return deprecate | |||
} | |||
/** | |||
* Return a wrapped function in a deprecation message. | |||
* | |||
* This is a no-op version of the wrapper, which does nothing but call | |||
* validation. | |||
*/ | |||
function wrapfunction (fn, message) { | |||
if (typeof fn !== 'function') { | |||
throw new TypeError('argument fn must be a function') | |||
} | |||
return fn | |||
} | |||
/** | |||
* Wrap property in a deprecation message. | |||
* | |||
* This is a no-op version of the wrapper, which does nothing but call | |||
* validation. | |||
*/ | |||
function wrapproperty (obj, prop, message) { | |||
if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) { | |||
throw new TypeError('argument obj must be object') | |||
} | |||
var descriptor = Object.getOwnPropertyDescriptor(obj, prop) | |||
if (!descriptor) { | |||
throw new TypeError('must call property on owner object') | |||
} | |||
if (!descriptor.configurable) { | |||
throw new TypeError('property must be configurable') | |||
} | |||
} |
@ -1,45 +0,0 @@ | |||
{ | |||
"name": "depd", | |||
"description": "Deprecate all the things", | |||
"version": "2.0.0", | |||
"author": "Douglas Christopher Wilson <doug@somethingdoug.com>", | |||
"license": "MIT", | |||
"keywords": [ | |||
"deprecate", | |||
"deprecated" | |||
], | |||
"repository": "dougwilson/nodejs-depd", | |||
"browser": "lib/browser/index.js", | |||
"devDependencies": { | |||
"benchmark": "2.1.4", | |||
"beautify-benchmark": "0.2.4", | |||
"eslint": "5.7.0", | |||
"eslint-config-standard": "12.0.0", | |||
"eslint-plugin-import": "2.14.0", | |||
"eslint-plugin-markdown": "1.0.0-beta.7", | |||
"eslint-plugin-node": "7.0.1", | |||
"eslint-plugin-promise": "4.0.1", | |||
"eslint-plugin-standard": "4.0.0", | |||
"istanbul": "0.4.5", | |||
"mocha": "5.2.0", | |||
"safe-buffer": "5.1.2", | |||
"uid-safe": "2.1.5" | |||
}, | |||
"files": [ | |||
"lib/", | |||
"History.md", | |||
"LICENSE", | |||
"index.js", | |||
"Readme.md" | |||
], | |||
"engines": { | |||
"node": ">= 0.8" | |||
}, | |||
"scripts": { | |||
"bench": "node benchmark/index.js", | |||
"lint": "eslint --plugin markdown --ext js,md .", | |||
"test": "mocha --reporter spec --bail test/", | |||
"test-ci": "istanbul cover --print=none node_modules/mocha/bin/_mocha -- --reporter spec test/ && istanbul report lcovonly text-summary", | |||
"test-cov": "istanbul cover --print=none node_modules/mocha/bin/_mocha -- --reporter dot test/ && istanbul report lcov text-summary" | |||
} | |||
} |
@ -1,5 +0,0 @@ | |||
module.exports = { | |||
printWidth: 160, | |||
tabWidth: 4, | |||
singleQuote: true | |||
}; |
@ -1,602 +0,0 @@ | |||
# CHANGELOG | |||
## 6.4.6 2020-03-20 | |||
- fix: `requeueAttempts=n` should requeue `n` times (Patrick Malouin) [a27ed2f7] | |||
## 6.4.4 2020-03-01 | |||
- Add `options.forceAuth` for SMTP (Patrick Malouin) [a27ed2f7] | |||
## 6.4.3 2020-02-22 | |||
- Added an option to specify max number of requeues when connection closes unexpectedly (Igor Sechyn) [8a927f5a] | |||
## 6.4.2 2019-12-11 | |||
- Fixed bug where array item was used with a potentially empty array | |||
## 6.4.1 2019-12-07 | |||
- Fix processing server output with unterminated responses | |||
## 6.4.0 2019-12-04 | |||
- Do not use auth if server does not advertise AUTH support [f419b09d] | |||
- add dns.CONNREFUSED (Hiroyuki Okada) [5c4c8ca8] | |||
## 6.3.1 2019-10-09 | |||
- Ignore "end" events because it might be "error" after it (dex4er) [72bade9] | |||
- Set username and password on the connection proxy object correctly (UsamaAshraf) [250b1a8] | |||
- Support more DNS errors (madarche) [2391aa4] | |||
## 6.3.0 2019-07-14 | |||
- Added new option to pass a set of httpHeaders to be sent when fetching attachments. See [PR #1034](https://github.com/nodemailer/nodemailer/pull/1034) | |||
## 6.2.1 2019-05-24 | |||
- No changes. It is the same as 6.2.0 that was accidentally published as 6.2.1 to npm | |||
## 6.2.0 2019-05-24 | |||
- Added new option for addressparser: `flatten`. If true then ignores group names and returns a single list of all addresses | |||
## 6.1.1 2019-04-20 | |||
- Fixed regression bug with missing smtp `authMethod` property | |||
## 6.1.0 2019-04-06 | |||
- Added new message property `amp` for providing AMP4EMAIL content | |||
## 6.0.0 2019-03-25 | |||
- SMTPConnection: use removeListener instead of removeAllListeners (xr0master) [ddc4af15] | |||
Using removeListener should fix memory leak with Node.js streams | |||
## 5.1.1 2019-01-09 | |||
- Added missing option argument for custom auth | |||
## 5.1.0 2019-01-09 | |||
- Official support for custom authentication methods and examples (examples/custom-auth-async.js and examples/custom-auth-cb.js) | |||
## 5.0.1 2019-01-09 | |||
- Fixed regression error to support Node versions lower than 6.11 | |||
- Added expiremental custom authentication support | |||
## 5.0.0 2018-12-28 | |||
- Start using dns.resolve() instead of dns.lookup() for resolving SMTP hostnames. Might be breaking change on some environments so upgrade with care | |||
- Show more logs for renewing OAuth2 tokens, previously it was not possible to see what actually failed | |||
## 4.7.0 2018-11-19 | |||
- Cleaned up List-\* header generation | |||
- Fixed 'full' return option for DSN (klaronix) [23b93a3b] | |||
- Support promises `for mailcomposer.build()` | |||
## 4.6.8 2018-08-15 | |||
- Use first IP address from DNS resolution when using a proxy (Limbozz) [d4ca847c] | |||
- Return raw email from SES transport (gabegorelick) [3aa08967] | |||
## 4.6.7 2018-06-15 | |||
- Added option `skipEncoding` to JSONTransport | |||
## 4.6.6 2018-06-10 | |||
- Fixes mime encoded-word compatibility issue with invalid clients like Zimbra | |||
## 4.6.5 2018-05-23 | |||
- Fixed broken DKIM stream in Node.js v10 | |||
- Updated error messages for SMTP responses to not include a newline | |||
## 4.6.4 2018-03-31 | |||
- Readded logo author link to README that was accidentally removed a while ago | |||
## 4.6.3 2018-03-13 | |||
- Removed unneeded dependency | |||
## 4.6.2 2018-03-06 | |||
- When redirecting URL calls then do not include original POST content | |||
## 4.6.1 2018-03-06 | |||
- Fixed Smtp connection freezing, when trying to send after close / quit (twawszczak) [73d3911c] | |||
## 4.6.0 2018-02-22 | |||
- Support socks module v2 in addition to v1 [e228bcb2] | |||
- Fixed invalid promise return value when using createTestAccount [5524e627] | |||
- Allow using local addresses [8f6fa35f] | |||
## 4.5.0 2018-02-21 | |||
- Added new message transport option `normalizeHeaderKey(key)=>normalizedKey` for custom header formatting | |||
## 4.4.2 2018-01-20 | |||
- Added sponsors section to README | |||
- enclose encodeURIComponent in try..catch to handle invalid urls | |||
## 4.4.1 2017-12-08 | |||
- Better handling of unexpectedly dropping connections | |||
## 4.4.0 2017-11-10 | |||
- Changed default behavior for attachment option contentTransferEncoding. If it is unset then base64 encoding is used for the attachment. If it is set to false then previous default applies (base64 for most, 7bit for text) | |||
## 4.3.1 2017-10-25 | |||
- Fixed a confict with Electron.js where timers do not have unref method | |||
## 4.3.0 2017-10-23 | |||
- Added new mail object method `mail.normalize(cb)` that should make creating HTTP API based transports much easier | |||
## 4.2.0 2017-10-13 | |||
- Expose streamed messages size and timers in info response | |||
## v4.1.3 2017-10-06 | |||
- Allow generating preview links without calling createTestAccount first | |||
## v4.1.2 2017-10-03 | |||
- No actual changes. Needed to push updated README to npmjs | |||
## v4.1.1 2017-09-25 | |||
- Fixed JSONTransport attachment handling | |||
## v4.1.0 2017-08-28 | |||
- Added new methods `createTestAccount` and `getTestMessageUrl` to use autogenerated email accounts from https://Ethereal.email | |||
## v4.0.1 2017-04-13 | |||
- Fixed issue with LMTP and STARTTLS | |||
## v4.0.0 2017-04-06 | |||
- License changed from EUPLv1.1 to MIT | |||
## v3.1.8 2017-03-21 | |||
- Fixed invalid List-\* header generation | |||
## v3.1.7 2017-03-14 | |||
- Emit an error if STARTTLS ends with connection being closed | |||
## v3.1.6 2017-03-14 | |||
- Expose last server response for smtpConnection | |||
## v3.1.5 2017-03-08 | |||
- Fixed SES transport, added missing `response` value | |||
## v3.1.4 2017-02-26 | |||
- Fixed DKIM calculation for empty body | |||
- Ensure linebreak after message content. This fixes DKIM signatures for non-multipart messages where input did not end with a newline | |||
## v3.1.3 2017-02-17 | |||
- Fixed missing `transport.verify()` methods for SES transport | |||
## v3.1.2 2017-02-17 | |||
- Added missing error handlers for Sendmail, SES and Stream transports. If a messages contained an invalid URL as attachment then these transports threw an uncatched error | |||
## v3.1.1 2017-02-13 | |||
- Fixed missing `transport.on('idle')` and `transport.isIdle()` methods for SES transports | |||
## v3.1.0 2017-02-13 | |||
- Added built-in transport for AWS SES. [Docs](http://localhost:1313/transports/ses/) | |||
- Updated stream transport to allow building JSON strings. [Docs](http://localhost:1313/transports/stream/#json-transport) | |||
- Added new method _mail.resolveAll_ that fetches all attachments and such to be able to more easily build API-based transports | |||
## v3.0.2 2017-02-04 | |||
- Fixed a bug with OAuth2 login where error callback was fired twice if getToken was not available. | |||
## v3.0.1 2017-02-03 | |||
- Fixed a bug where Nodemailer threw an exception if `disableFileAccess` option was used | |||
- Added FLOSS [exception declaration](FLOSS_EXCEPTIONS.md) | |||
## v3.0.0 2017-01-31 | |||
- Initial version of Nodemailer 3 | |||
This update brings a lot of breaking changes: | |||
- License changed from MIT to **EUPL-1.1**. This was possible as the new version of Nodemailer is a major rewrite. The features I don't have ownership for, were removed or reimplemented. If there's still some snippets in the code that have vague ownership then notify <mailto:andris@kreata.ee> about the conflicting code and I'll fix it. | |||
- Requires **Node.js v6+** | |||
- All **templating is gone**. It was too confusing to use and to be really universal a huge list of different renderers would be required. Nodemailer is about email, not about parsing different template syntaxes | |||
- **No NTLM authentication**. It was too difficult to re-implement. If you still need it then it would be possible to introduce a pluggable SASL interface where you could load the NTLM module in your own code and pass it to Nodemailer. Currently this is not possible. | |||
- **OAuth2 authentication** is built in and has a different [configuration](https://nodemailer.com/smtp/oauth2/). You can use both user (3LO) and service (2LO) accounts to generate access tokens from Nodemailer. Additionally there's a new feature to authenticate differently for every message – useful if your application sends on behalf of different users instead of a single sender. | |||
- **Improved Calendaring**. Provide an ical file to Nodemailer to send out [calendar events](https://nodemailer.com/message/calendar-events/). | |||
And also some non-breaking changes: | |||
- All **dependencies were dropped**. There is exactly 0 dependencies needed to use Nodemailer. This brings the installation time of Nodemailer from NPM down to less than 2 seconds | |||
- **Delivery status notifications** added to Nodemailer | |||
- Improved and built-in **DKIM** signing of messages. Previously you needed an external module for this and it did quite a lousy job with larger messages | |||
- **Stream transport** to return a RFC822 formatted message as a stream. Useful if you want to use Nodemailer as a preprocessor and not for actual delivery. | |||
- **Sendmail** transport built-in, no need for external transport plugin | |||
See [Nodemailer.com](https://nodemailer.com/) for full documentation | |||
## 2.7.0 2016-12-08 | |||
- Bumped mailcomposer that generates encoded-words differently which might break some tests | |||
## 2.6.0 2016-09-05 | |||
- Added new options disableFileAccess and disableUrlAccess | |||
- Fixed envelope handling where cc/bcc fields were ignored in the envelope object | |||
## 2.4.2 2016-05-25 | |||
- Removed shrinkwrap file. Seemed to cause more trouble than help | |||
## 2.4.1 2016-05-12 | |||
- Fixed outdated shrinkwrap file | |||
## 2.4.0 2016-05-11 | |||
- Bumped mailcomposer module to allow using `false` as attachment filename (suppresses filename usage) | |||
- Added NTLM authentication support | |||
## 2.3.2 2016-04-11 | |||
- Bumped smtp transport modules to get newest smtp-connection that fixes SMTPUTF8 support for internationalized email addresses | |||
## 2.3.1 2016-04-08 | |||
- Bumped mailcomposer to have better support for message/822 attachments | |||
## 2.3.0 2016-03-03 | |||
- Fixed a bug with attachment filename that contains mixed unicode and dashes | |||
- Added built-in support for proxies by providing a new SMTP option `proxy` that takes a proxy configuration url as its value | |||
- Added option `transport` to dynamically load transport plugins | |||
- Do not require globally installed grunt-cli | |||
## 2.2.1 2016-02-20 | |||
- Fixed a bug in SMTP requireTLS option that was broken | |||
## 2.2.0 2016-02-18 | |||
- Removed the need to use `clone` dependency | |||
- Added new method `verify` to check SMTP configuration | |||
- Direct transport uses STARTTLS by default, fallbacks to plaintext if STARTTLS fails | |||
- Added new message option `list` for setting List-\* headers | |||
- Add simple proxy support with `getSocket` method | |||
- Added new message option `textEncoding`. If `textEncoding` is not set then detect best encoding automatically | |||
- Added new message option `icalEvent` to embed iCalendar events. Example [here](examples/ical-event.js) | |||
- Added new attachment option `raw` to use prepared MIME contents instead of generating a new one. This might be useful when you want to handcraft some parts of the message yourself, for example if you want to inject a PGP encrypted message as the contents of a MIME node | |||
- Added new message option `raw` to use an existing MIME message instead of generating a new one | |||
## 2.1.0 2016-02-01 | |||
Republishing 2.1.0-rc.1 as stable. To recap, here's the notable changes between v2.0 and v2.1: | |||
- Implemented templating support. You can either use a simple built-in renderer or some external advanced renderer, eg. [node-email-templates](https://github.com/niftylettuce/node-email-templates). Templating [docs](http://nodemailer.com/2-0-0-beta/templating/). | |||
- Updated smtp-pool to emit 'idle' events in order to handle message queue more effectively | |||
- Updated custom header handling, works everywhere the same now, no differences between adding custom headers to the message or to an attachment | |||
## 2.1.0-rc.1 2016-01-25 | |||
Sneaked in some new features even though it is already rc | |||
- If a SMTP pool is closed while there are still messages in a queue, the message callbacks are invoked with an error | |||
- In case of SMTP pool the transporter emits 'idle' when there is a free connection slot available | |||
- Added method `isIdle()` that checks if a pool has still some free connection slots available | |||
## 2.1.0-rc.0 2016-01-20 | |||
- Bumped dependency versions | |||
## 2.1.0-beta.3 2016-01-20 | |||
- Added support for node-email-templates templating in addition to the built-in renderer | |||
## 2.1.0-beta.2 2016-01-20 | |||
- Implemented simple templating feature | |||
## 2.1.0-beta.1 2016-01-20 | |||
- Allow using prepared header values that are not folded or encoded by Nodemailer | |||
## 2.1.0-beta.0 2016-01-20 | |||
- Use the same header custom structure for message root, attachments and alternatives | |||
- Ensure that Message-Id exists when accessing message | |||
- Allow using array values for custom headers (inserts every value in its own row) | |||
## 2.0.0 2016-01-11 | |||
- Released rc.2 as stable | |||
## 2.0.0-rc.2 2016-01-04 | |||
- Locked dependencies | |||
## 2.0.0-beta.2 2016-01-04 | |||
- Updated documentation to reflect changes with SMTP handling | |||
- Use beta versions for smtp/pool/direct transports | |||
- Updated logging | |||
## 2.0.0-beta.1 2016-01-03 | |||
- Use bunyan compatible logger instead of the emit('log') style | |||
- Outsourced some reusable methods to nodemailer-shared | |||
- Support setting direct/smtp/pool with the default configuration | |||
## 2.0.0-beta.0 2015-12-31 | |||
- Stream errors are not silently swallowed | |||
- Do not use format=flowed | |||
- Use nodemailer-fetch to fetch URL streams | |||
- jshint replaced by eslint | |||
## v1.11.0 2015-12-28 | |||
Allow connection url based SMTP configurations | |||
## v1.10.0 2015-11-13 | |||
Added `defaults` argument for `createTransport` to predefine commonn values (eg. `from` address) | |||
## v1.9.0 2015-11-09 | |||
Returns a Promise for `sendMail` if callback is not defined | |||
## v1.8.0 2015-10-08 | |||
Added priority option (high, normal, low) for setting Importance header | |||
## v1.7.0 2015-10-06 | |||
Replaced hyperquest with needle. Fixes issues with compressed data and redirects | |||
## v1.6.0 2015-10-05 | |||
Maintenance release. Bumped dependencies to get support for unicode filenames for QQ webmail and to support emoji in filenames | |||
## v1.5.0 2015-09-24 | |||
Use mailcomposer instead of built in solution to generate message sources. Bumped libmime gives better quoted-printable handling. | |||
## v1.4.0 2015-06-27 | |||
Added new message option `watchHtml` to specify Apple Watch specific HTML part of the message. See [this post](https://litmus.com/blog/how-to-send-hidden-version-email-apple-watch) for details | |||
## v1.3.4 2015-04-25 | |||
Maintenance release, bumped buildmail version to get fixed format=flowed handling | |||
## v1.3.3 2015-04-25 | |||
Maintenance release, bumped dependencies | |||
## v1.3.2 2015-03-09 | |||
Maintenance release, upgraded dependencies. Replaced simplesmtp based tests with smtp-server based ones. | |||
## v1.3.0 2014-09-12 | |||
Maintenance release, upgrades buildmail and libmime. Allows using functions as transform plugins and fixes issue with unicode filenames in Gmail. | |||
## v1.2.2 2014-09-05 | |||
Proper handling of data uris as attachments. Attachment `path` property can also be defined as a data uri, not just regular url or file path. | |||
## v1.2.1 2014-08-21 | |||
Bumped libmime and mailbuild versions to properly handle filenames with spaces (short ascii only filenames with spaces were left unquoted). | |||
## v1.2.0 2014-08-18 | |||
Allow using encoded strings as attachments. Added new property `encoding` which defines the encoding used for a `content` string. If encoding is set, the content value is converted to a Buffer value using the defined encoding before usage. Useful for including binary attachemnts in JSON formatted email objects. | |||
## v1.1.2 2014-08-18 | |||
Return deprecatin error for v0.x style configuration | |||
## v1.1.1 2014-07-30 | |||
Bumped nodemailer-direct-transport dependency. Updated version includes a bugfix for Stream nodes handling. Important only if use direct-transport with Streams (not file paths or urls) as attachment content. | |||
## v1.1.0 2014-07-29 | |||
Added new method `resolveContent()` to get the html/text/attachment content as a String or Buffer. | |||
## v1.0.4 2014-07-23 | |||
Bugfix release. HTML node was instered twice if the message consisted of a HTML content (but no text content) + at least one attachment with CID + at least one attachment without CID. In this case the HTML node was inserted both to the root level multipart/mixed section and to the multipart/related sub section | |||
## v1.0.3 2014-07-16 | |||
Fixed a bug where Nodemailer crashed if the message content type was multipart/related | |||
## v1.0.2 2014-07-16 | |||
Upgraded nodemailer-smtp-transport to 0.1.11\. The docs state that for SSL you should use 'secure' option but the underlying smtp-connection module used 'secureConnection' for this purpose. Fixed smpt-connection to match the docs. | |||
## v1.0.1 2014-07-15 | |||
Implemented missing #close method that is passed to the underlying transport object. Required by the smtp pool. | |||
## v1.0.0 2014-07-15 | |||
Total rewrite. See migration guide here: <http://www.andrisreinman.com/nodemailer-v1-0/#migrationguide> | |||
## v0.7.1 2014-07-09 | |||
- Upgraded aws-sdk to 2.0.5 | |||
## v0.7.0 2014-06-17 | |||
- Bumped version to v0.7.0 | |||
- Fix AWS-SES usage [5b6bc144] | |||
- Replace current SES with new SES using AWS-SDK (Elanorr) [c79d797a] | |||
- Updated README.md about Node Email Templates (niftylettuce) [e52bef81] | |||
## v0.6.5 2014-05-15 | |||
- Bumped version to v0.6.5 | |||
- Use tildes instead of carets for dependency listing [5296ce41] | |||
- Allow clients to set a custom identityString (venables) [5373287d] | |||
- bugfix (adding "-i" to sendmail command line for each new mail) by copying this.args (vrodic) [05a8a9a3] | |||
- update copyright (gdi2290) [3a6cba3a] | |||
## v0.6.4 2014-05-13 | |||
- Bumped version to v0.6.4 | |||
- added npmignore, bumped dependencies [21bddcd9] | |||
- Add AOL to well-known services (msouce) [da7dd3b7] | |||
## v0.6.3 2014-04-16 | |||
- Bumped version to v0.6.3 | |||
- Upgraded simplesmtp dependency [dd367f59] | |||
## v0.6.2 2014-04-09 | |||
- Bumped version to v0.6.2 | |||
- Added error option to Stub transport [c423acad] | |||
- Use SVG npm badge (t3chnoboy) [677117b7] | |||
- add SendCloud to well known services (haio) [43c358e0] | |||
- High-res build-passing and NPM module badges (sahat) [9fdc37cd] | |||
## v0.6.1 2014-01-26 | |||
- Bumped version to v0.6.1 | |||
- Do not throw on multiple errors from sendmail command [c6e2cd12] | |||
- Do not require callback for pickup, fixes #238 [93eb3214] | |||
- Added AWSSecurityToken information to README, fixes #235 [58e921d1] | |||
- Added Nodemailer logo [06b7d1a8] | |||
## v0.6.0 2013-12-30 | |||
- Bumped version to v0.6.0 | |||
- Allow defining custom transport methods [ec5b48ce] | |||
- Return messageId with responseObject for all built in transport methods [74445cec] | |||
- Bumped dependency versions for mailcomposer and readable-stream [9a034c34] | |||
- Changed pickup argument name to 'directory' [01c3ea53] | |||
- Added support for IIS pickup directory with PICKUP transport (philipproplesch) [36940b59..360a2878] | |||
- Applied common styles [9e93a409] | |||
- Updated readme [c78075e7] | |||
## v0.5.15 2013-12-13 | |||
- bumped version to v0.5.15 | |||
- Updated README, added global options info for setting uo transports [554bb0e5] | |||
- Resolve public hostname, if resolveHostname property for a transport object is set to `true` [9023a6e1..4c66b819] | |||
## v0.5.14 2013-12-05 | |||
- bumped version to v0.5.14 | |||
- Expose status for direct messages [f0312df6] | |||
- Allow to skip the X-Mailer header if xMailer value is set to 'false' [f2c20a68] | |||
## v0.5.13 2013-12-03 | |||
- bumped version to v0.5.13 | |||
- Use the name property from the transport object to use for the domain part of message-id values (1598eee9) | |||
## v0.5.12 2013-12-02 | |||
- bumped version to v0.5.12 | |||
- Expose transport method and transport module version if available [a495106e] | |||
- Added 'he' module instead of using custom html entity decoding [c197d102] | |||
- Added xMailer property for transport configuration object to override X-Mailer value [e8733a61] | |||
- Updated README, added description for 'mail' method [e1f5f3a6] | |||
## v0.5.11 2013-11-28 | |||
- bumped version to v0.5.11 | |||
- Updated mailcomposer version. Replaces ent with he [6a45b790e] | |||
## v0.5.10 2013-11-26 | |||
- bumped version to v0.5.10 | |||
- added shorthand function mail() for direct transport type [88129bd7] | |||
- minor tweaks and typo fixes [f797409e..ceac0ca4] | |||
## v0.5.9 2013-11-25 | |||
- bumped version to v0.5.9 | |||
- Update for 'direct' handling [77b84e2f] | |||
- do not require callback to be provided for 'direct' type [ec51c79f] | |||
## v0.5.8 2013-11-22 | |||
- bumped version to v0.5.8 | |||
- Added support for 'direct' transport [826f226d..0dbbcbbc] | |||
## v0.5.7 2013-11-18 | |||
- bumped version to v0.5.7 | |||
- Replace \r\n by \n in Sendmail transport (rolftimmermans) [fed2089e..616ec90c] A lot of sendmail implementations choke on \r\n newlines and require \n This commit addresses this by transforming all \r\n sequences passed to the sendmail command with \n | |||
## v0.5.6 2013-11-15 | |||
- bumped version to v0.5.6 | |||
- Upgraded mailcomposer dependency to 0.2.4 [e5ff9c40] | |||
- Removed noCR option [e810d1b8] | |||
- Update wellknown.js, added FastMail (k-j-kleist) [cf930f6d] | |||
## v0.5.5 2013-10-30 | |||
- bumped version to v0.5.5 | |||
- Updated mailcomposer dependnecy version to 0.2.3 | |||
- Remove legacy code - node v0.4 is not supported anymore anyway | |||
- Use hostname (autodetected or from the options.name property) for Message-Id instead of "Nodemailer" (helps a bit when messages are identified as spam) | |||
- Added maxMessages info to README | |||
## v0.5.4 2013-10-29 | |||
- bumped version to v0.5.4 | |||
- added "use strict" statements | |||
- Added DSN info to README | |||
- add support for QQ enterprise email (coderhaoxin) | |||
- Add a Bitdeli Badge to README | |||
- DSN options Passthrought into simplesmtp. (irvinzz) | |||
## v0.5.3 2013-10-03 | |||
- bumped version v0.5.3 | |||
- Using a stub transport to prevent sendmail from being called during a test. (jsdevel) | |||
- closes #78: sendmail transport does not work correctly on Unix machines. (jsdevel) | |||
- Updated PaaS Support list to include Modulus. (fiveisprime) | |||
- Translate self closing break tags to newline (kosmasgiannis) | |||
- fix typos (aeosynth) | |||
## v0.5.2 2013-07-25 | |||
- bumped version v0.5.2 | |||
- Merge pull request #177 from MrSwitch/master Fixing Amazon SES, fatal error caused by bad connection |
@ -1,67 +0,0 @@ | |||
# Contribute | |||
## Introduction | |||
First, thank you for considering contributing to nodemailer! It's people like you that make the open source community such a great community! 😊 | |||
We welcome any type of contribution, not only code. You can help with | |||
- **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open) | |||
- **Marketing**: writing blog posts, howto's, printing stickers, ... | |||
- **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, ... | |||
- **Code**: take a look at the [open issues](https://github.com/nodemailer/nodemailer/issues). Even if you can't write code, commenting on them, showing that you care about a given issue matters. It helps us triage them. | |||
- **Money**: we welcome financial contributions in full transparency on our [open collective](https://opencollective.com/nodemailer). | |||
## Your First Contribution | |||
Working on your first Pull Request? You can learn how from this *free* series, [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). | |||
## Submitting code | |||
Any code change should be submitted as a pull request. The description should explain what the code does and give steps to execute it. The pull request should also contain tests. | |||
## Code review process | |||
The bigger the pull request, the longer it will take to review and merge. Try to break down large pull requests in smaller chunks that are easier to review and merge. | |||
It is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you? | |||
## Financial contributions | |||
We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/nodemailer). | |||
Anyone can file an expense. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed. | |||
## Questions | |||
If you have any questions, create an [issue](https://github.com/nodemailer/nodemailer/issues) (protip: do a quick search first to see if someone else didn't ask the same question before!). | |||
You can also reach us at hello@nodemailer.opencollective.com. | |||
## Credits | |||
### Contributors | |||
Thank you to all the people who have already contributed to nodemailer! | |||
<a href="graphs/contributors"><img src="https://opencollective.com/nodemailer/contributors.svg?width=890" /></a> | |||
### Backers | |||
Thank you to all our backers! [[Become a backer](https://opencollective.com/nodemailer#backer)] | |||
<a href="https://opencollective.com/nodemailer#backers" target="_blank"><img src="https://opencollective.com/nodemailer/backers.svg?width=890"></a> | |||
### Sponsors | |||
Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/nodemailer#sponsor)) | |||
<a href="https://opencollective.com/nodemailer/sponsor/0/website" target="_blank"><img src="https://opencollective.com/nodemailer/sponsor/0/avatar.svg"></a> | |||
<a href="https://opencollective.com/nodemailer/sponsor/1/website" target="_blank"><img src="https://opencollective.com/nodemailer/sponsor/1/avatar.svg"></a> | |||
<a href="https://opencollective.com/nodemailer/sponsor/2/website" target="_blank"><img src="https://opencollective.com/nodemailer/sponsor/2/avatar.svg"></a> | |||
<a href="https://opencollective.com/nodemailer/sponsor/3/website" target="_blank"><img src="https://opencollective.com/nodemailer/sponsor/3/avatar.svg"></a> | |||
<a href="https://opencollective.com/nodemailer/sponsor/4/website" target="_blank"><img src="https://opencollective.com/nodemailer/sponsor/4/avatar.svg"></a> | |||
<a href="https://opencollective.com/nodemailer/sponsor/5/website" target="_blank"><img src="https://opencollective.com/nodemailer/sponsor/5/avatar.svg"></a> | |||
<a href="https://opencollective.com/nodemailer/sponsor/6/website" target="_blank"><img src="https://opencollective.com/nodemailer/sponsor/6/avatar.svg"></a> | |||
<a href="https://opencollective.com/nodemailer/sponsor/7/website" target="_blank"><img src="https://opencollective.com/nodemailer/sponsor/7/avatar.svg"></a> | |||
<a href="https://opencollective.com/nodemailer/sponsor/8/website" target="_blank"><img src="https://opencollective.com/nodemailer/sponsor/8/avatar.svg"></a> | |||
<a href="https://opencollective.com/nodemailer/sponsor/9/website" target="_blank"><img src="https://opencollective.com/nodemailer/sponsor/9/avatar.svg"></a> | |||
<!-- This `CONTRIBUTING.md` is based on @nayafia's template https://github.com/nayafia/contributing-template --> |
@ -1,16 +0,0 @@ | |||
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. |
@ -1,44 +0,0 @@ | |||
# Nodemailer | |||
[](https://nodemailer.com/about/) | |||
Send e-mails from Node.js – easy as cake! 🍰✉️ | |||
[](https://nodemailer.com/about/) | |||
See [nodemailer.com](https://nodemailer.com/) for documentation and terms. | |||
## Having an issue? | |||
#### First review the docs | |||
Documentation for Nodemailer can be found at [nodemailer.com](https://nodemailer.com/about/). | |||
#### Nodemailer throws a SyntaxError for "..." | |||
You are using older Node.js version than v6.0. Upgrade Node.js to get support for the spread operator. | |||
#### I'm having issues with Gmail | |||
Gmail either works well or it does not work at all. It is probably easier to switch to an alternative service instead of fixing issues with Gmail. If Gmail does not work for you then don't use it. Read more about it [here](https://nodemailer.com/usage/using-gmail/). | |||
#### I get ETIMEDOUT errors | |||
Check your firewall settings. Timeout usually occurs when you try to open a connection to a port that is firewalled either on the server or on your machine. | |||
#### I get TLS errors | |||
* If you are running the code in your own machine, then check your antivirus settings. Antiviruses often mess around with email ports usage. Node.js might not recognize the MITM cert your antivirus is using. | |||
* Latest Node versions allow only TLS versions 1.2 and higher, some servers might still use TLS 1.1 or lower. Check Node.js docs how to get correct TLS support for your app. | |||
#### I have a different problem | |||
If you are having issues with Nodemailer, then the best way to find help would be [Stack Overflow](https://stackoverflow.com/search?q=nodemailer) or revisit the [docs](https://nodemailer.com/about/). | |||
### License | |||
Nodemailer is licensed under the **MIT license** | |||
--- | |||
The Nodemailer logo was designed by [Sven Kristjansen](https://www.behance.net/kristjansen). |
@ -1,309 +0,0 @@ | |||
'use strict'; | |||
/** | |||
* Converts tokens for a single address into an address object | |||
* | |||
* @param {Array} tokens Tokens object | |||
* @return {Object} Address object | |||
*/ | |||
function _handleAddress(tokens) { | |||
let token; | |||
let isGroup = false; | |||
let state = 'text'; | |||
let address; | |||
let addresses = []; | |||
let data = { | |||
address: [], | |||
comment: [], | |||
group: [], | |||
text: [] | |||
}; | |||
let i; | |||
let len; | |||
// Filter out <addresses>, (comments) and regular text | |||
for (i = 0, len = tokens.length; i < len; i++) { | |||
token = tokens[i]; | |||
if (token.type === 'operator') { | |||
switch (token.value) { | |||
case '<': | |||
state = 'address'; | |||
break; | |||
case '(': | |||
state = 'comment'; | |||
break; | |||
case ':': | |||
state = 'group'; | |||
isGroup = true; | |||
break; | |||
default: | |||
state = 'text'; | |||
} | |||
} else if (token.value) { | |||
if (state === 'address') { | |||
// handle use case where unquoted name includes a "<" | |||
// Apple Mail truncates everything between an unexpected < and an address | |||
// and so will we | |||
token.value = token.value.replace(/^[^<]*<\s*/, ''); | |||
} | |||
data[state].push(token.value); | |||
} | |||
} | |||
// If there is no text but a comment, replace the two | |||
if (!data.text.length && data.comment.length) { | |||
data.text = data.comment; | |||
data.comment = []; | |||
} | |||
if (isGroup) { | |||
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3 | |||
data.text = data.text.join(' '); | |||
addresses.push({ | |||
name: data.text || (address && address.name), | |||
group: data.group.length ? addressparser(data.group.join(',')) : [] | |||
}); | |||
} else { | |||
// If no address was found, try to detect one from regular text | |||
if (!data.address.length && data.text.length) { | |||
for (i = data.text.length - 1; i >= 0; i--) { | |||
if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) { | |||
data.address = data.text.splice(i, 1); | |||
break; | |||
} | |||
} | |||
let _regexHandler = function(address) { | |||
if (!data.address.length) { | |||
data.address = [address.trim()]; | |||
return ' '; | |||
} else { | |||
return address; | |||
} | |||
}; | |||
// still no address | |||
if (!data.address.length) { | |||
for (i = data.text.length - 1; i >= 0; i--) { | |||
// fixed the regex to parse email address correctly when email address has more than one @ | |||
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, _regexHandler).trim(); | |||
if (data.address.length) { | |||
break; | |||
} | |||
} | |||
} | |||
} | |||
// If there's still is no text but a comment exixts, replace the two | |||
if (!data.text.length && data.comment.length) { | |||
data.text = data.comment; | |||
data.comment = []; | |||
} | |||
// Keep only the first address occurence, push others to regular text | |||
if (data.address.length > 1) { | |||
data.text = data.text.concat(data.address.splice(1)); | |||
} | |||
// Join values with spaces | |||
data.text = data.text.join(' '); | |||
data.address = data.address.join(' '); | |||
if (!data.address && isGroup) { | |||
return []; | |||
} else { | |||
address = { | |||
address: data.address || data.text || '', | |||
name: data.text || data.address || '' | |||
}; | |||
if (address.address === address.name) { | |||
if ((address.address || '').match(/@/)) { | |||
address.name = ''; | |||
} else { | |||
address.address = ''; | |||
} | |||
} | |||
addresses.push(address); | |||
} | |||
} | |||
return addresses; | |||
} | |||
/** | |||
* Creates a Tokenizer object for tokenizing address field strings | |||
* | |||
* @constructor | |||
* @param {String} str Address field string | |||
*/ | |||
class Tokenizer { | |||
constructor(str) { | |||
this.str = (str || '').toString(); | |||
this.operatorCurrent = ''; | |||
this.operatorExpecting = ''; | |||
this.node = null; | |||
this.escaped = false; | |||
this.list = []; | |||
/** | |||
* Operator tokens and which tokens are expected to end the sequence | |||
*/ | |||
this.operators = { | |||
'"': '"', | |||
'(': ')', | |||
'<': '>', | |||
',': '', | |||
':': ';', | |||
// Semicolons are not a legal delimiter per the RFC2822 grammar other | |||
// than for terminating a group, but they are also not valid for any | |||
// other use in this context. Given that some mail clients have | |||
// historically allowed the semicolon as a delimiter equivalent to the | |||
// comma in their UI, it makes sense to treat them the same as a comma | |||
// when used outside of a group. | |||
';': '' | |||
}; | |||
} | |||
/** | |||
* Tokenizes the original input string | |||
* | |||
* @return {Array} An array of operator|text tokens | |||
*/ | |||
tokenize() { | |||
let chr, | |||
list = []; | |||
for (let i = 0, len = this.str.length; i < len; i++) { | |||
chr = this.str.charAt(i); | |||
this.checkChar(chr); | |||
} | |||
this.list.forEach(node => { | |||
node.value = (node.value || '').toString().trim(); | |||
if (node.value) { | |||
list.push(node); | |||
} | |||
}); | |||
return list; | |||
} | |||
/** | |||
* Checks if a character is an operator or text and acts accordingly | |||
* | |||
* @param {String} chr Character from the address field | |||
*/ | |||
checkChar(chr) { | |||
if ((chr in this.operators || chr === '\\') && this.escaped) { | |||
this.escaped = false; | |||
} else if (this.operatorExpecting && chr === this.operatorExpecting) { | |||
this.node = { | |||
type: 'operator', | |||
value: chr | |||
}; | |||
this.list.push(this.node); | |||
this.node = null; | |||
this.operatorExpecting = ''; | |||
this.escaped = false; | |||
return; | |||
} else if (!this.operatorExpecting && chr in this.operators) { | |||
this.node = { | |||
type: 'operator', | |||
value: chr | |||
}; | |||
this.list.push(this.node); | |||
this.node = null; | |||
this.operatorExpecting = this.operators[chr]; | |||
this.escaped = false; | |||
return; | |||
} | |||
if (!this.escaped && chr === '\\') { | |||
this.escaped = true; | |||
return; | |||
} | |||
if (!this.node) { | |||
this.node = { | |||
type: 'text', | |||
value: '' | |||
}; | |||
this.list.push(this.node); | |||
} | |||
if (this.escaped && chr !== '\\') { | |||
this.node.value += '\\'; | |||
} | |||
this.node.value += chr; | |||
this.escaped = false; | |||
} | |||
} | |||
/** | |||
* Parses structured e-mail addresses from an address field | |||
* | |||
* Example: | |||
* | |||
* 'Name <address@domain>' | |||
* | |||
* will be converted to | |||
* | |||
* [{name: 'Name', address: 'address@domain'}] | |||
* | |||
* @param {String} str Address field | |||
* @return {Array} An array of address objects | |||
*/ | |||
function addressparser(str, options) { | |||
options = options || {}; | |||
let tokenizer = new Tokenizer(str); | |||
let tokens = tokenizer.tokenize(); | |||
let addresses = []; | |||
let address = []; | |||
let parsedAddresses = []; | |||
tokens.forEach(token => { | |||
if (token.type === 'operator' && (token.value === ',' || token.value === ';')) { | |||
if (address.length) { | |||
addresses.push(address); | |||
} | |||
address = []; | |||
} else { | |||
address.push(token); | |||
} | |||
}); | |||
if (address.length) { | |||
addresses.push(address); | |||
} | |||
addresses.forEach(address => { | |||
address = _handleAddress(address); | |||
if (address.length) { | |||
parsedAddresses = parsedAddresses.concat(address); | |||
} | |||
}); | |||
if (options.flatten) { | |||
let addresses = []; | |||
let walkAddressList = list => { | |||
list.forEach(address => { | |||
if (address.group) { | |||
return walkAddressList(address.group); | |||
} else { | |||
addresses.push(address); | |||
} | |||
}); | |||
}; | |||
walkAddressList(parsedAddresses); | |||
return addresses; | |||
} | |||
return parsedAddresses; | |||
} | |||
// expose to the world | |||
module.exports = addressparser; |
@ -1,142 +0,0 @@ | |||
'use strict'; | |||
const Transform = require('stream').Transform; | |||
/** | |||
* Encodes a Buffer into a base64 encoded string | |||
* | |||
* @param {Buffer} buffer Buffer to convert | |||
* @returns {String} base64 encoded string | |||
*/ | |||
function encode(buffer) { | |||
if (typeof buffer === 'string') { | |||
buffer = Buffer.from(buffer, 'utf-8'); | |||
} | |||
return buffer.toString('base64'); | |||
} | |||
/** | |||
* Adds soft line breaks to a base64 string | |||
* | |||
* @param {String} str base64 encoded string that might need line wrapping | |||
* @param {Number} [lineLength=76] Maximum allowed length for a line | |||
* @returns {String} Soft-wrapped base64 encoded string | |||
*/ | |||
function wrap(str, lineLength) { | |||
str = (str || '').toString(); | |||
lineLength = lineLength || 76; | |||
if (str.length <= lineLength) { | |||
return str; | |||
} | |||
let result = []; | |||
let pos = 0; | |||
let chunkLength = lineLength * 1024; | |||
while (pos < str.length) { | |||
let wrappedLines = str | |||
.substr(pos, chunkLength) | |||
.replace(new RegExp('.{' + lineLength + '}', 'g'), '$&\r\n') | |||
.trim(); | |||
result.push(wrappedLines); | |||
pos += chunkLength; | |||
} | |||
return result.join('\r\n').trim(); | |||
} | |||
/** | |||
* Creates a transform stream for encoding data to base64 encoding | |||
* | |||
* @constructor | |||
* @param {Object} options Stream options | |||
* @param {Number} [options.lineLength=76] Maximum lenght for lines, set to false to disable wrapping | |||
*/ | |||
class Encoder extends Transform { | |||
constructor(options) { | |||
super(); | |||
// init Transform | |||
this.options = options || {}; | |||
if (this.options.lineLength !== false) { | |||
this.options.lineLength = this.options.lineLength || 76; | |||
} | |||
this._curLine = ''; | |||
this._remainingBytes = false; | |||
this.inputBytes = 0; | |||
this.outputBytes = 0; | |||
} | |||
_transform(chunk, encoding, done) { | |||
if (encoding !== 'buffer') { | |||
chunk = Buffer.from(chunk, encoding); | |||
} | |||
if (!chunk || !chunk.length) { | |||
return setImmediate(done); | |||
} | |||
this.inputBytes += chunk.length; | |||
if (this._remainingBytes && this._remainingBytes.length) { | |||
chunk = Buffer.concat([this._remainingBytes, chunk], this._remainingBytes.length + chunk.length); | |||
this._remainingBytes = false; | |||
} | |||
if (chunk.length % 3) { | |||
this._remainingBytes = chunk.slice(chunk.length - (chunk.length % 3)); | |||
chunk = chunk.slice(0, chunk.length - (chunk.length % 3)); | |||
} else { | |||
this._remainingBytes = false; | |||
} | |||
let b64 = this._curLine + encode(chunk); | |||
if (this.options.lineLength) { | |||
b64 = wrap(b64, this.options.lineLength); | |||
// remove last line as it is still most probably incomplete | |||
let lastLF = b64.lastIndexOf('\n'); | |||
if (lastLF < 0) { | |||
this._curLine = b64; | |||
b64 = ''; | |||
} else if (lastLF === b64.length - 1) { | |||
this._curLine = ''; | |||
} else { | |||
this._curLine = b64.substr(lastLF + 1); | |||
b64 = b64.substr(0, lastLF + 1); | |||
} | |||
} | |||
if (b64) { | |||
this.outputBytes += b64.length; | |||
this.push(Buffer.from(b64, 'ascii')); | |||
} | |||
setImmediate(done); | |||
} | |||
_flush(done) { | |||
if (this._remainingBytes && this._remainingBytes.length) { | |||
this._curLine += encode(this._remainingBytes); | |||
} | |||
if (this._curLine) { | |||
this._curLine = wrap(this._curLine, this.options.lineLength); | |||
this.outputBytes += this._curLine.length; | |||
this.push(this._curLine, 'ascii'); | |||
this._curLine = ''; | |||
} | |||
done(); | |||
} | |||
} | |||
// expose to the world | |||
module.exports = { | |||
encode, | |||
wrap, | |||
Encoder | |||
}; |
@ -1,251 +0,0 @@ | |||
'use strict'; | |||
// FIXME: | |||
// replace this Transform mess with a method that pipes input argument to output argument | |||
const MessageParser = require('./message-parser'); | |||
const RelaxedBody = require('./relaxed-body'); | |||
const sign = require('./sign'); | |||
const PassThrough = require('stream').PassThrough; | |||
const fs = require('fs'); | |||
const path = require('path'); | |||
const crypto = require('crypto'); | |||
const DKIM_ALGO = 'sha256'; | |||
const MAX_MESSAGE_SIZE = 128 * 1024; // buffer messages larger than this to disk | |||
/* | |||
// Usage: | |||
let dkim = new DKIM({ | |||
domainName: 'example.com', | |||
keySelector: 'key-selector', | |||
privateKey, | |||
cacheDir: '/tmp' | |||
}); | |||
dkim.sign(input).pipe(process.stdout); | |||
// Where inputStream is a rfc822 message (either a stream, string or Buffer) | |||
// and outputStream is a DKIM signed rfc822 message | |||
*/ | |||
class DKIMSigner { | |||
constructor(options, keys, input, output) { | |||
this.options = options || {}; | |||
this.keys = keys; | |||
this.cacheTreshold = Number(this.options.cacheTreshold) || MAX_MESSAGE_SIZE; | |||
this.hashAlgo = this.options.hashAlgo || DKIM_ALGO; | |||
this.cacheDir = this.options.cacheDir || false; | |||
this.chunks = []; | |||
this.chunklen = 0; | |||
this.readPos = 0; | |||
this.cachePath = this.cacheDir ? path.join(this.cacheDir, 'message.' + Date.now() + '-' + crypto.randomBytes(14).toString('hex')) : false; | |||
this.cache = false; | |||
this.headers = false; | |||
this.bodyHash = false; | |||
this.parser = false; | |||
this.relaxedBody = false; | |||
this.input = input; | |||
this.output = output; | |||
this.output.usingCache = false; | |||
this.errored = false; | |||
this.input.on('error', err => { | |||
this.errored = true; | |||
this.cleanup(); | |||
output.emit('error', err); | |||
}); | |||
} | |||
cleanup() { | |||
if (!this.cache || !this.cachePath) { | |||
return; | |||
} | |||
fs.unlink(this.cachePath, () => false); | |||
} | |||
createReadCache() { | |||
// pipe remainings to cache file | |||
this.cache = fs.createReadStream(this.cachePath); | |||
this.cache.once('error', err => { | |||
this.cleanup(); | |||
this.output.emit('error', err); | |||
}); | |||
this.cache.once('close', () => { | |||
this.cleanup(); | |||
}); | |||
this.cache.pipe(this.output); | |||
} | |||
sendNextChunk() { | |||
if (this.errored) { | |||
return; | |||
} | |||
if (this.readPos >= this.chunks.length) { | |||
if (!this.cache) { | |||
return this.output.end(); | |||
} | |||
return this.createReadCache(); | |||
} | |||
let chunk = this.chunks[this.readPos++]; | |||
if (this.output.write(chunk) === false) { | |||
return this.output.once('drain', () => { | |||
this.sendNextChunk(); | |||
}); | |||
} | |||
setImmediate(() => this.sendNextChunk()); | |||
} | |||
sendSignedOutput() { | |||
let keyPos = 0; | |||
let signNextKey = () => { | |||
if (keyPos >= this.keys.length) { | |||
this.output.write(this.parser.rawHeaders); | |||
return setImmediate(() => this.sendNextChunk()); | |||
} | |||
let key = this.keys[keyPos++]; | |||
let dkimField = sign(this.headers, this.hashAlgo, this.bodyHash, { | |||
domainName: key.domainName, | |||
keySelector: key.keySelector, | |||
privateKey: key.privateKey, | |||
headerFieldNames: this.options.headerFieldNames, | |||
skipFields: this.options.skipFields | |||
}); | |||
if (dkimField) { | |||
this.output.write(Buffer.from(dkimField + '\r\n')); | |||
} | |||
return setImmediate(signNextKey); | |||
}; | |||
if (this.bodyHash && this.headers) { | |||
return signNextKey(); | |||
} | |||
this.output.write(this.parser.rawHeaders); | |||
this.sendNextChunk(); | |||
} | |||
createWriteCache() { | |||
this.output.usingCache = true; | |||
// pipe remainings to cache file | |||
this.cache = fs.createWriteStream(this.cachePath); | |||
this.cache.once('error', err => { | |||
this.cleanup(); | |||
// drain input | |||
this.relaxedBody.unpipe(this.cache); | |||
this.relaxedBody.on('readable', () => { | |||
while (this.relaxedBody.read() !== null) { | |||
// do nothing | |||
} | |||
}); | |||
this.errored = true; | |||
// emit error | |||
this.output.emit('error', err); | |||
}); | |||
this.cache.once('close', () => { | |||
this.sendSignedOutput(); | |||
}); | |||
this.relaxedBody.removeAllListeners('readable'); | |||
this.relaxedBody.pipe(this.cache); | |||
} | |||
signStream() { | |||
this.parser = new MessageParser(); | |||
this.relaxedBody = new RelaxedBody({ | |||
hashAlgo: this.hashAlgo | |||
}); | |||
this.parser.on('headers', value => { | |||
this.headers = value; | |||
}); | |||
this.relaxedBody.on('hash', value => { | |||
this.bodyHash = value; | |||
}); | |||
this.relaxedBody.on('readable', () => { | |||
let chunk; | |||
if (this.cache) { | |||
return; | |||
} | |||
while ((chunk = this.relaxedBody.read()) !== null) { | |||
this.chunks.push(chunk); | |||
this.chunklen += chunk.length; | |||
if (this.chunklen >= this.cacheTreshold && this.cachePath) { | |||
return this.createWriteCache(); | |||
} | |||
} | |||
}); | |||
this.relaxedBody.on('end', () => { | |||
if (this.cache) { | |||
return; | |||
} | |||
this.sendSignedOutput(); | |||
}); | |||
this.parser.pipe(this.relaxedBody); | |||
setImmediate(() => this.input.pipe(this.parser)); | |||
} | |||
} | |||
class DKIM { | |||
constructor(options) { | |||
this.options = options || {}; | |||
this.keys = [].concat( | |||
this.options.keys || { | |||
domainName: options.domainName, | |||
keySelector: options.keySelector, | |||
privateKey: options.privateKey | |||
} | |||
); | |||
} | |||
sign(input, extraOptions) { | |||
let output = new PassThrough(); | |||
let inputStream = input; | |||
let writeValue = false; | |||
if (Buffer.isBuffer(input)) { | |||
writeValue = input; | |||
inputStream = new PassThrough(); | |||
} else if (typeof input === 'string') { | |||
writeValue = Buffer.from(input); | |||
inputStream = new PassThrough(); | |||
} | |||
let options = this.options; | |||
if (extraOptions && Object.keys(extraOptions).length) { | |||
options = {}; | |||
Object.keys(this.options || {}).forEach(key => { | |||
options[key] = this.options[key]; | |||
}); | |||
Object.keys(extraOptions || {}).forEach(key => { | |||
if (!(key in options)) { | |||
options[key] = extraOptions[key]; | |||
} | |||
}); | |||
} | |||
let signer = new DKIMSigner(options, this.keys, inputStream, output); | |||
setImmediate(() => { | |||
signer.signStream(); | |||
if (writeValue) { | |||
setImmediate(() => { | |||
inputStream.end(writeValue); | |||
}); | |||
} | |||
}); | |||
return output; | |||
} | |||
} | |||
module.exports = DKIM; |
@ -1,158 +0,0 @@ | |||
'use strict'; | |||
const Transform = require('stream').Transform; | |||
/** | |||
* MessageParser instance is a transform stream that separates message headers | |||
* from the rest of the body. Headers are emitted with the 'headers' event. Message | |||
* body is passed on as the resulting stream. | |||
*/ | |||
class MessageParser extends Transform { | |||
constructor(options) { | |||
super(options); | |||
this.lastBytes = Buffer.alloc(4); | |||
this.headersParsed = false; | |||
this.headerBytes = 0; | |||
this.headerChunks = []; | |||
this.rawHeaders = false; | |||
this.bodySize = 0; | |||
} | |||
/** | |||
* Keeps count of the last 4 bytes in order to detect line breaks on chunk boundaries | |||
* | |||
* @param {Buffer} data Next data chunk from the stream | |||
*/ | |||
updateLastBytes(data) { | |||
let lblen = this.lastBytes.length; | |||
let nblen = Math.min(data.length, lblen); | |||
// shift existing bytes | |||
for (let i = 0, len = lblen - nblen; i < len; i++) { | |||
this.lastBytes[i] = this.lastBytes[i + nblen]; | |||
} | |||
// add new bytes | |||
for (let i = 1; i <= nblen; i++) { | |||
this.lastBytes[lblen - i] = data[data.length - i]; | |||
} | |||
} | |||
/** | |||
* Finds and removes message headers from the remaining body. We want to keep | |||
* headers separated until final delivery to be able to modify these | |||
* | |||
* @param {Buffer} data Next chunk of data | |||
* @return {Boolean} Returns true if headers are already found or false otherwise | |||
*/ | |||
checkHeaders(data) { | |||
if (this.headersParsed) { | |||
return true; | |||
} | |||
let lblen = this.lastBytes.length; | |||
let headerPos = 0; | |||
this.curLinePos = 0; | |||
for (let i = 0, len = this.lastBytes.length + data.length; i < len; i++) { | |||
let chr; | |||
if (i < lblen) { | |||
chr = this.lastBytes[i]; | |||
} else { | |||
chr = data[i - lblen]; | |||
} | |||
if (chr === 0x0a && i) { | |||
let pr1 = i - 1 < lblen ? this.lastBytes[i - 1] : data[i - 1 - lblen]; | |||
let pr2 = i > 1 ? (i - 2 < lblen ? this.lastBytes[i - 2] : data[i - 2 - lblen]) : false; | |||
if (pr1 === 0x0a) { | |||
this.headersParsed = true; | |||
headerPos = i - lblen + 1; | |||
this.headerBytes += headerPos; | |||
break; | |||
} else if (pr1 === 0x0d && pr2 === 0x0a) { | |||
this.headersParsed = true; | |||
headerPos = i - lblen + 1; | |||
this.headerBytes += headerPos; | |||
break; | |||
} | |||
} | |||
} | |||
if (this.headersParsed) { | |||
this.headerChunks.push(data.slice(0, headerPos)); | |||
this.rawHeaders = Buffer.concat(this.headerChunks, this.headerBytes); | |||
this.headerChunks = null; | |||
this.emit('headers', this.parseHeaders()); | |||
if (data.length - 1 > headerPos) { | |||
let chunk = data.slice(headerPos); | |||
this.bodySize += chunk.length; | |||
// this would be the first chunk of data sent downstream | |||
setImmediate(() => this.push(chunk)); | |||
} | |||
return false; | |||
} else { | |||
this.headerBytes += data.length; | |||
this.headerChunks.push(data); | |||
} | |||
// store last 4 bytes to catch header break | |||
this.updateLastBytes(data); | |||
return false; | |||
} | |||
_transform(chunk, encoding, callback) { | |||
if (!chunk || !chunk.length) { | |||
return callback(); | |||
} | |||
if (typeof chunk === 'string') { | |||
chunk = Buffer.from(chunk, encoding); | |||
} | |||
let headersFound; | |||
try { | |||
headersFound = this.checkHeaders(chunk); | |||
} catch (E) { | |||
return callback(E); | |||
} | |||
if (headersFound) { | |||
this.bodySize += chunk.length; | |||
this.push(chunk); | |||
} | |||
setImmediate(callback); | |||
} | |||
_flush(callback) { | |||
if (this.headerChunks) { | |||
let chunk = Buffer.concat(this.headerChunks, this.headerBytes); | |||
this.bodySize += chunk.length; | |||
this.push(chunk); | |||
this.headerChunks = null; | |||
} | |||
callback(); | |||
} | |||
parseHeaders() { | |||
let lines = (this.rawHeaders || '').toString().split(/\r?\n/); | |||
for (let i = lines.length - 1; i > 0; i--) { | |||
if (/^\s/.test(lines[i])) { | |||
lines[i - 1] += '\n' + lines[i]; | |||
lines.splice(i, 1); | |||
} | |||
} | |||
return lines | |||
.filter(line => line.trim()) | |||
.map(line => ({ | |||
key: line | |||
.substr(0, line.indexOf(':')) | |||
.trim() | |||
.toLowerCase(), | |||
line | |||
})); | |||
} | |||
} | |||
module.exports = MessageParser; |
@ -1,154 +0,0 @@ | |||
'use strict'; | |||
// streams through a message body and calculates relaxed body hash | |||
const Transform = require('stream').Transform; | |||
const crypto = require('crypto'); | |||
class RelaxedBody extends Transform { | |||
constructor(options) { | |||
super(); | |||
options = options || {}; | |||
this.chunkBuffer = []; | |||
this.chunkBufferLen = 0; | |||
this.bodyHash = crypto.createHash(options.hashAlgo || 'sha1'); | |||
this.remainder = ''; | |||
this.byteLength = 0; | |||
this.debug = options.debug; | |||
this._debugBody = options.debug ? [] : false; | |||
} | |||
updateHash(chunk) { | |||
let bodyStr; | |||
// find next remainder | |||
let nextRemainder = ''; | |||
// This crux finds and removes the spaces from the last line and the newline characters after the last non-empty line | |||
// If we get another chunk that does not match this description then we can restore the previously processed data | |||
let state = 'file'; | |||
for (let i = chunk.length - 1; i >= 0; i--) { | |||
let c = chunk[i]; | |||
if (state === 'file' && (c === 0x0a || c === 0x0d)) { | |||
// do nothing, found \n or \r at the end of chunk, stil end of file | |||
} else if (state === 'file' && (c === 0x09 || c === 0x20)) { | |||
// switch to line ending mode, this is the last non-empty line | |||
state = 'line'; | |||
} else if (state === 'line' && (c === 0x09 || c === 0x20)) { | |||
// do nothing, found ' ' or \t at the end of line, keep processing the last non-empty line | |||
} else if (state === 'file' || state === 'line') { | |||
// non line/file ending character found, switch to body mode | |||
state = 'body'; | |||
if (i === chunk.length - 1) { | |||
// final char is not part of line end or file end, so do nothing | |||
break; | |||
} | |||
} | |||
if (i === 0) { | |||
// reached to the beginning of the chunk, check if it is still about the ending | |||
// and if the remainder also matches | |||
if ( | |||
(state === 'file' && (!this.remainder || /[\r\n]$/.test(this.remainder))) || | |||
(state === 'line' && (!this.remainder || /[ \t]$/.test(this.remainder))) | |||
) { | |||
// keep everything | |||
this.remainder += chunk.toString('binary'); | |||
return; | |||
} else if (state === 'line' || state === 'file') { | |||
// process existing remainder as normal line but store the current chunk | |||
nextRemainder = chunk.toString('binary'); | |||
chunk = false; | |||
break; | |||
} | |||
} | |||
if (state !== 'body') { | |||
continue; | |||
} | |||
// reached first non ending byte | |||
nextRemainder = chunk.slice(i + 1).toString('binary'); | |||
chunk = chunk.slice(0, i + 1); | |||
break; | |||
} | |||
let needsFixing = !!this.remainder; | |||
if (chunk && !needsFixing) { | |||
// check if we even need to change anything | |||
for (let i = 0, len = chunk.length; i < len; i++) { | |||
if (i && chunk[i] === 0x0a && chunk[i - 1] !== 0x0d) { | |||
// missing \r before \n | |||
needsFixing = true; | |||
break; | |||
} else if (i && chunk[i] === 0x0d && chunk[i - 1] === 0x20) { | |||
// trailing WSP found | |||
needsFixing = true; | |||
break; | |||
} else if (i && chunk[i] === 0x20 && chunk[i - 1] === 0x20) { | |||
// multiple spaces found, needs to be replaced with just one | |||
needsFixing = true; | |||
break; | |||
} else if (chunk[i] === 0x09) { | |||
// TAB found, needs to be replaced with a space | |||
needsFixing = true; | |||
break; | |||
} | |||
} | |||
} | |||
if (needsFixing) { | |||
bodyStr = this.remainder + (chunk ? chunk.toString('binary') : ''); | |||
this.remainder = nextRemainder; | |||
bodyStr = bodyStr | |||
.replace(/\r?\n/g, '\n') // use js line endings | |||
.replace(/[ \t]*$/gm, '') // remove line endings, rtrim | |||
.replace(/[ \t]+/gm, ' ') // single spaces | |||
.replace(/\n/g, '\r\n'); // restore rfc822 line endings | |||
chunk = Buffer.from(bodyStr, 'binary'); | |||
} else if (nextRemainder) { | |||
this.remainder = nextRemainder; | |||
} | |||
if (this.debug) { | |||
this._debugBody.push(chunk); | |||
} | |||
this.bodyHash.update(chunk); | |||
} | |||
_transform(chunk, encoding, callback) { | |||
if (!chunk || !chunk.length) { | |||
return callback(); | |||
} | |||
if (typeof chunk === 'string') { | |||
chunk = Buffer.from(chunk, encoding); | |||
} | |||
this.updateHash(chunk); | |||
this.byteLength += chunk.length; | |||
this.push(chunk); | |||
callback(); | |||
} | |||
_flush(callback) { | |||
// generate final hash and emit it | |||
if (/[\r\n]$/.test(this.remainder) && this.byteLength > 2) { | |||
// add terminating line end | |||
this.bodyHash.update(Buffer.from('\r\n')); | |||
} | |||
if (!this.byteLength) { | |||
// emit empty line buffer to keep the stream flowing | |||
this.push(Buffer.from('\r\n')); | |||
// this.bodyHash.update(Buffer.from('\r\n')); | |||
} | |||
this.emit('hash', this.bodyHash.digest('base64'), this.debug ? Buffer.concat(this._debugBody) : false); | |||
callback(); | |||
} | |||
} | |||
module.exports = RelaxedBody; |
@ -1,117 +0,0 @@ | |||
'use strict'; | |||
const punycode = require('punycode'); | |||
const mimeFuncs = require('../mime-funcs'); | |||
const crypto = require('crypto'); | |||
/** | |||
* Returns DKIM signature header line | |||
* | |||
* @param {Object} headers Parsed headers object from MessageParser | |||
* @param {String} bodyHash Base64 encoded hash of the message | |||
* @param {Object} options DKIM options | |||
* @param {String} options.domainName Domain name to be signed for | |||
* @param {String} options.keySelector DKIM key selector to use | |||
* @param {String} options.privateKey DKIM private key to use | |||
* @return {String} Complete header line | |||
*/ | |||
module.exports = (headers, hashAlgo, bodyHash, options) => { | |||
options = options || {}; | |||
// all listed fields from RFC4871 #5.5 | |||
let defaultFieldNames = | |||
'From:Sender:Reply-To:Subject:Date:Message-ID:To:' + | |||
'Cc:MIME-Version:Content-Type:Content-Transfer-Encoding:Content-ID:' + | |||
'Content-Description:Resent-Date:Resent-From:Resent-Sender:' + | |||
'Resent-To:Resent-Cc:Resent-Message-ID:In-Reply-To:References:' + | |||
'List-Id:List-Help:List-Unsubscribe:List-Subscribe:List-Post:' + | |||
'List-Owner:List-Archive'; | |||
let fieldNames = options.headerFieldNames || defaultFieldNames; | |||
let canonicalizedHeaderData = relaxedHeaders(headers, fieldNames, options.skipFields); | |||
let dkimHeader = generateDKIMHeader(options.domainName, options.keySelector, canonicalizedHeaderData.fieldNames, hashAlgo, bodyHash); | |||
let signer, signature; | |||
canonicalizedHeaderData.headers += 'dkim-signature:' + relaxedHeaderLine(dkimHeader); | |||
signer = crypto.createSign(('rsa-' + hashAlgo).toUpperCase()); | |||
signer.update(canonicalizedHeaderData.headers); | |||
try { | |||
signature = signer.sign(options.privateKey, 'base64'); | |||
} catch (E) { | |||
return false; | |||
} | |||
return dkimHeader + signature.replace(/(^.{73}|.{75}(?!\r?\n|\r))/g, '$&\r\n ').trim(); | |||
}; | |||
module.exports.relaxedHeaders = relaxedHeaders; | |||
function generateDKIMHeader(domainName, keySelector, fieldNames, hashAlgo, bodyHash) { | |||
let dkim = [ | |||
'v=1', | |||
'a=rsa-' + hashAlgo, | |||
'c=relaxed/relaxed', | |||
'd=' + punycode.toASCII(domainName), | |||
'q=dns/txt', | |||
's=' + keySelector, | |||
'bh=' + bodyHash, | |||
'h=' + fieldNames | |||
].join('; '); | |||
return mimeFuncs.foldLines('DKIM-Signature: ' + dkim, 76) + ';\r\n b='; | |||
} | |||
function relaxedHeaders(headers, fieldNames, skipFields) { | |||
let includedFields = new Set(); | |||
let skip = new Set(); | |||
let headerFields = new Map(); | |||
(skipFields || '') | |||
.toLowerCase() | |||
.split(':') | |||
.forEach(field => { | |||
skip.add(field.trim()); | |||
}); | |||
(fieldNames || '') | |||
.toLowerCase() | |||
.split(':') | |||
.filter(field => !skip.has(field.trim())) | |||
.forEach(field => { | |||
includedFields.add(field.trim()); | |||
}); | |||
for (let i = headers.length - 1; i >= 0; i--) { | |||
let line = headers[i]; | |||
// only include the first value from bottom to top | |||
if (includedFields.has(line.key) && !headerFields.has(line.key)) { | |||
headerFields.set(line.key, relaxedHeaderLine(line.line)); | |||
} | |||
} | |||
let headersList = []; | |||
let fields = []; | |||
includedFields.forEach(field => { | |||
if (headerFields.has(field)) { | |||
fields.push(field); | |||
headersList.push(field + ':' + headerFields.get(field)); | |||
} | |||
}); | |||
return { | |||
headers: headersList.join('\r\n') + '\r\n', | |||
fieldNames: fields.join(':') | |||
}; | |||
} | |||
function relaxedHeaderLine(line) { | |||
return line | |||
.substr(line.indexOf(':') + 1) | |||
.replace(/\r?\n/g, '') | |||
.replace(/\s+/g, ' ') | |||
.trim(); | |||
} |
@ -1,284 +0,0 @@ | |||
'use strict'; | |||
// module to handle cookies | |||
const urllib = require('url'); | |||
const SESSION_TIMEOUT = 1800; // 30 min | |||
/** | |||
* Creates a biskviit cookie jar for managing cookie values in memory | |||
* | |||
* @constructor | |||
* @param {Object} [options] Optional options object | |||
*/ | |||
class Cookies { | |||
constructor(options) { | |||
this.options = options || {}; | |||
this.cookies = []; | |||
} | |||
/** | |||
* Stores a cookie string to the cookie storage | |||
* | |||
* @param {String} cookieStr Value from the 'Set-Cookie:' header | |||
* @param {String} url Current URL | |||
*/ | |||
set(cookieStr, url) { | |||
let urlparts = urllib.parse(url || ''); | |||
let cookie = this.parse(cookieStr); | |||
let domain; | |||
if (cookie.domain) { | |||
domain = cookie.domain.replace(/^\./, ''); | |||
// do not allow cross origin cookies | |||
if ( | |||
// can't be valid if the requested domain is shorter than current hostname | |||
urlparts.hostname.length < domain.length || | |||
// prefix domains with dot to be sure that partial matches are not used | |||
('.' + urlparts.hostname).substr(-domain.length + 1) !== '.' + domain | |||
) { | |||
cookie.domain = urlparts.hostname; | |||
} | |||
} else { | |||
cookie.domain = urlparts.hostname; | |||
} | |||
if (!cookie.path) { | |||
cookie.path = this.getPath(urlparts.pathname); | |||
} | |||
// if no expire date, then use sessionTimeout value | |||
if (!cookie.expires) { | |||
cookie.expires = new Date(Date.now() + (Number(this.options.sessionTimeout || SESSION_TIMEOUT) || SESSION_TIMEOUT) * 1000); | |||
} | |||
return this.add(cookie); | |||
} | |||
/** | |||
* Returns cookie string for the 'Cookie:' header. | |||
* | |||
* @param {String} url URL to check for | |||
* @returns {String} Cookie header or empty string if no matches were found | |||
*/ | |||
get(url) { | |||
return this.list(url) | |||
.map(cookie => cookie.name + '=' + cookie.value) | |||
.join('; '); | |||
} | |||
/** | |||
* Lists all valied cookie objects for the specified URL | |||
* | |||
* @param {String} url URL to check for | |||
* @returns {Array} An array of cookie objects | |||
*/ | |||
list(url) { | |||
let result = []; | |||
let i; | |||
let cookie; | |||
for (i = this.cookies.length - 1; i >= 0; i--) { | |||
cookie = this.cookies[i]; | |||
if (this.isExpired(cookie)) { | |||
this.cookies.splice(i, i); | |||
continue; | |||
} | |||
if (this.match(cookie, url)) { | |||
result.unshift(cookie); | |||
} | |||
} | |||
return result; | |||
} | |||
/** | |||
* Parses cookie string from the 'Set-Cookie:' header | |||
* | |||
* @param {String} cookieStr String from the 'Set-Cookie:' header | |||
* @returns {Object} Cookie object | |||
*/ | |||
parse(cookieStr) { | |||
let cookie = {}; | |||
(cookieStr || '') | |||
.toString() | |||
.split(';') | |||
.forEach(cookiePart => { | |||
let valueParts = cookiePart.split('='); | |||
let key = valueParts | |||
.shift() | |||
.trim() | |||
.toLowerCase(); | |||
let value = valueParts.join('=').trim(); | |||
let domain; | |||
if (!key) { | |||
// skip empty parts | |||
return; | |||
} | |||
switch (key) { | |||
case 'expires': | |||
value = new Date(value); | |||
// ignore date if can not parse it | |||
if (value.toString() !== 'Invalid Date') { | |||
cookie.expires = value; | |||
} | |||
break; | |||
case 'path': | |||
cookie.path = value; | |||
break; | |||
case 'domain': | |||
domain = value.toLowerCase(); | |||
if (domain.length && domain.charAt(0) !== '.') { | |||
domain = '.' + domain; // ensure preceeding dot for user set domains | |||
} | |||
cookie.domain = domain; | |||
break; | |||
case 'max-age': | |||
cookie.expires = new Date(Date.now() + (Number(value) || 0) * 1000); | |||
break; | |||
case 'secure': | |||
cookie.secure = true; | |||
break; | |||
case 'httponly': | |||
cookie.httponly = true; | |||
break; | |||
default: | |||
if (!cookie.name) { | |||
cookie.name = key; | |||
cookie.value = value; | |||
} | |||
} | |||
}); | |||
return cookie; | |||
} | |||
/** | |||
* Checks if a cookie object is valid for a specified URL | |||
* | |||
* @param {Object} cookie Cookie object | |||
* @param {String} url URL to check for | |||
* @returns {Boolean} true if cookie is valid for specifiec URL | |||
*/ | |||
match(cookie, url) { | |||
let urlparts = urllib.parse(url || ''); | |||
// check if hostname matches | |||
// .foo.com also matches subdomains, foo.com does not | |||
if ( | |||
urlparts.hostname !== cookie.domain && | |||
(cookie.domain.charAt(0) !== '.' || ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain) | |||
) { | |||
return false; | |||
} | |||
// check if path matches | |||
let path = this.getPath(urlparts.pathname); | |||
if (path.substr(0, cookie.path.length) !== cookie.path) { | |||
return false; | |||
} | |||
// check secure argument | |||
if (cookie.secure && urlparts.protocol !== 'https:') { | |||
return false; | |||
} | |||
return true; | |||
} | |||
/** | |||
* Adds (or updates/removes if needed) a cookie object to the cookie storage | |||
* | |||
* @param {Object} cookie Cookie value to be stored | |||
*/ | |||
add(cookie) { | |||
let i; | |||
let len; | |||
// nothing to do here | |||
if (!cookie || !cookie.name) { | |||
return false; | |||
} | |||
// overwrite if has same params | |||
for (i = 0, len = this.cookies.length; i < len; i++) { | |||
if (this.compare(this.cookies[i], cookie)) { | |||
// check if the cookie needs to be removed instead | |||
if (this.isExpired(cookie)) { | |||
this.cookies.splice(i, 1); // remove expired/unset cookie | |||
return false; | |||
} | |||
this.cookies[i] = cookie; | |||
return true; | |||
} | |||
} | |||
// add as new if not already expired | |||
if (!this.isExpired(cookie)) { | |||
this.cookies.push(cookie); | |||
} | |||
return true; | |||
} | |||
/** | |||
* Checks if two cookie objects are the same | |||
* | |||
* @param {Object} a Cookie to check against | |||
* @param {Object} b Cookie to check against | |||
* @returns {Boolean} True, if the cookies are the same | |||
*/ | |||
compare(a, b) { | |||
return a.name === b.name && a.path === b.path && a.domain === b.domain && a.secure === b.secure && a.httponly === a.httponly; | |||
} | |||
/** | |||
* Checks if a cookie is expired | |||
* | |||
* @param {Object} cookie Cookie object to check against | |||
* @returns {Boolean} True, if the cookie is expired | |||
*/ | |||
isExpired(cookie) { | |||
return (cookie.expires && cookie.expires < new Date()) || !cookie.value; | |||
} | |||
/** | |||
* Returns normalized cookie path for an URL path argument | |||
* | |||
* @param {String} pathname | |||
* @returns {String} Normalized path | |||
*/ | |||
getPath(pathname) { | |||
let path = (pathname || '/').split('/'); | |||
path.pop(); // remove filename part | |||
path = path.join('/').trim(); | |||
// ensure path prefix / | |||
if (path.charAt(0) !== '/') { | |||
path = '/' + path; | |||
} | |||
// ensure path suffix / | |||
if (path.substr(-1) !== '/') { | |||
path += '/'; | |||
} | |||
return path; | |||
} | |||
} | |||
module.exports = Cookies; |
@ -1,277 +0,0 @@ | |||
'use strict'; | |||
const http = require('http'); | |||
const https = require('https'); | |||
const urllib = require('url'); | |||
const zlib = require('zlib'); | |||
const PassThrough = require('stream').PassThrough; | |||
const Cookies = require('./cookies'); | |||
const packageData = require('../../package.json'); | |||
const MAX_REDIRECTS = 5; | |||
module.exports = function(url, options) { | |||
return fetch(url, options); | |||
}; | |||
module.exports.Cookies = Cookies; | |||
function fetch(url, options) { | |||
options = options || {}; | |||
options.fetchRes = options.fetchRes || new PassThrough(); | |||
options.cookies = options.cookies || new Cookies(); | |||
options.redirects = options.redirects || 0; | |||
options.maxRedirects = isNaN(options.maxRedirects) ? MAX_REDIRECTS : options.maxRedirects; | |||
if (options.cookie) { | |||
[].concat(options.cookie || []).forEach(cookie => { | |||
options.cookies.set(cookie, url); | |||
}); | |||
options.cookie = false; | |||
} | |||
let fetchRes = options.fetchRes; | |||
let parsed = urllib.parse(url); | |||
let method = | |||
(options.method || '') | |||
.toString() | |||
.trim() | |||
.toUpperCase() || 'GET'; | |||
let finished = false; | |||
let cookies; | |||
let body; | |||
let handler = parsed.protocol === 'https:' ? https : http; | |||
let headers = { | |||
'accept-encoding': 'gzip,deflate', | |||
'user-agent': 'nodemailer/' + packageData.version | |||
}; | |||
Object.keys(options.headers || {}).forEach(key => { | |||
headers[key.toLowerCase().trim()] = options.headers[key]; | |||
}); | |||
if (options.userAgent) { | |||
headers['user-agent'] = options.userAgent; | |||
} | |||
if (parsed.auth) { | |||
headers.Authorization = 'Basic ' + Buffer.from(parsed.auth).toString('base64'); | |||
} | |||
if ((cookies = options.cookies.get(url))) { | |||
headers.cookie = cookies; | |||
} | |||
if (options.body) { | |||
if (options.contentType !== false) { | |||
headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded'; | |||
} | |||
if (typeof options.body.pipe === 'function') { | |||
// it's a stream | |||
headers['Transfer-Encoding'] = 'chunked'; | |||
body = options.body; | |||
body.on('error', err => { | |||
if (finished) { | |||
return; | |||
} | |||
finished = true; | |||
err.type = 'FETCH'; | |||
err.sourceUrl = url; | |||
fetchRes.emit('error', err); | |||
}); | |||
} else { | |||
if (options.body instanceof Buffer) { | |||
body = options.body; | |||
} else if (typeof options.body === 'object') { | |||
try { | |||
// encodeURIComponent can fail on invalid input (partial emoji etc.) | |||
body = Buffer.from( | |||
Object.keys(options.body) | |||
.map(key => { | |||
let value = options.body[key].toString().trim(); | |||
return encodeURIComponent(key) + '=' + encodeURIComponent(value); | |||
}) | |||
.join('&') | |||
); | |||
} catch (E) { | |||
if (finished) { | |||
return; | |||
} | |||
finished = true; | |||
E.type = 'FETCH'; | |||
E.sourceUrl = url; | |||
fetchRes.emit('error', E); | |||
return; | |||
} | |||
} else { | |||
body = Buffer.from(options.body.toString().trim()); | |||
} | |||
headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded'; | |||
headers['Content-Length'] = body.length; | |||
} | |||
// if method is not provided, use POST instead of GET | |||
method = | |||
(options.method || '') | |||
.toString() | |||
.trim() | |||
.toUpperCase() || 'POST'; | |||
} | |||
let req; | |||
let reqOptions = { | |||
method, | |||
host: parsed.hostname, | |||
path: parsed.path, | |||
port: parsed.port ? parsed.port : parsed.protocol === 'https:' ? 443 : 80, | |||
headers, | |||
rejectUnauthorized: false, | |||
agent: false | |||
}; | |||
if (options.tls) { | |||
Object.keys(options.tls).forEach(key => { | |||
reqOptions[key] = options.tls[key]; | |||
}); | |||
} | |||
try { | |||
req = handler.request(reqOptions); | |||
} catch (E) { | |||
finished = true; | |||
setImmediate(() => { | |||
E.type = 'FETCH'; | |||
E.sourceUrl = url; | |||
fetchRes.emit('error', E); | |||
}); | |||
return fetchRes; | |||
} | |||
if (options.timeout) { | |||
req.setTimeout(options.timeout, () => { | |||
if (finished) { | |||
return; | |||
} | |||
finished = true; | |||
req.abort(); | |||
let err = new Error('Request Timeout'); | |||
err.type = 'FETCH'; | |||
err.sourceUrl = url; | |||
fetchRes.emit('error', err); | |||
}); | |||
} | |||
req.on('error', err => { | |||
if (finished) { | |||
return; | |||
} | |||
finished = true; | |||
err.type = 'FETCH'; | |||
err.sourceUrl = url; | |||
fetchRes.emit('error', err); | |||
}); | |||
req.on('response', res => { | |||
let inflate; | |||
if (finished) { | |||
return; | |||
} | |||
switch (res.headers['content-encoding']) { | |||
case 'gzip': | |||
case 'deflate': | |||
inflate = zlib.createUnzip(); | |||
break; | |||
} | |||
if (res.headers['set-cookie']) { | |||
[].concat(res.headers['set-cookie'] || []).forEach(cookie => { | |||
options.cookies.set(cookie, url); | |||
}); | |||
} | |||
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) { | |||
// redirect | |||
options.redirects++; | |||
if (options.redirects > options.maxRedirects) { | |||
finished = true; | |||
let err = new Error('Maximum redirect count exceeded'); | |||
err.type = 'FETCH'; | |||
err.sourceUrl = url; | |||
fetchRes.emit('error', err); | |||
req.abort(); | |||
return; | |||
} | |||
// redirect does not include POST body | |||
options.method = 'GET'; | |||
options.body = false; | |||
return fetch(urllib.resolve(url, res.headers.location), options); | |||
} | |||
fetchRes.statusCode = res.statusCode; | |||
fetchRes.headers = res.headers; | |||
if (res.statusCode >= 300 && !options.allowErrorResponse) { | |||
finished = true; | |||
let err = new Error('Invalid status code ' + res.statusCode); | |||
err.type = 'FETCH'; | |||
err.sourceUrl = url; | |||
fetchRes.emit('error', err); | |||
req.abort(); | |||
return; | |||
} | |||
res.on('error', err => { | |||
if (finished) { | |||
return; | |||
} | |||
finished = true; | |||
err.type = 'FETCH'; | |||
err.sourceUrl = url; | |||
fetchRes.emit('error', err); | |||
req.abort(); | |||
}); | |||
if (inflate) { | |||
res.pipe(inflate).pipe(fetchRes); | |||
inflate.on('error', err => { | |||
if (finished) { | |||
return; | |||
} | |||
finished = true; | |||
err.type = 'FETCH'; | |||
err.sourceUrl = url; | |||
fetchRes.emit('error', err); | |||
req.abort(); | |||
}); | |||
} else { | |||
res.pipe(fetchRes); | |||
} | |||
}); | |||
setImmediate(() => { | |||
if (body) { | |||
try { | |||
if (typeof body.pipe === 'function') { | |||
return body.pipe(req); | |||
} else { | |||
req.write(body); | |||
} | |||
} catch (err) { | |||
finished = true; | |||
err.type = 'FETCH'; | |||
err.sourceUrl = url; | |||
fetchRes.emit('error', err); | |||
return; | |||
} | |||
} | |||
req.end(); | |||
}); | |||
return fetchRes; | |||
} |
@ -1,82 +0,0 @@ | |||
'use strict'; | |||
const packageData = require('../../package.json'); | |||
const shared = require('../shared'); | |||
/** | |||
* Generates a Transport object to generate JSON output | |||
* | |||
* @constructor | |||
* @param {Object} optional config parameter | |||
*/ | |||
class JSONTransport { | |||
constructor(options) { | |||
options = options || {}; | |||
this.options = options || {}; | |||
this.name = 'JSONTransport'; | |||
this.version = packageData.version; | |||
this.logger = shared.getLogger(this.options, { | |||
component: this.options.component || 'json-transport' | |||
}); | |||
} | |||
/** | |||
* <p>Compiles a mailcomposer message and forwards it to handler that sends it.</p> | |||
* | |||
* @param {Object} emailMessage MailComposer object | |||
* @param {Function} callback Callback function to run when the sending is completed | |||
*/ | |||
send(mail, done) { | |||
// Sendmail strips this header line by itself | |||
mail.message.keepBcc = true; | |||
let envelope = mail.data.envelope || mail.message.getEnvelope(); | |||
let messageId = mail.message.messageId(); | |||
let recipients = [].concat(envelope.to || []); | |||
if (recipients.length > 3) { | |||
recipients.push('...and ' + recipients.splice(2).length + ' more'); | |||
} | |||
this.logger.info( | |||
{ | |||
tnx: 'send', | |||
messageId | |||
}, | |||
'Composing JSON structure of %s to <%s>', | |||
messageId, | |||
recipients.join(', ') | |||
); | |||
setImmediate(() => { | |||
mail.normalize((err, data) => { | |||
if (err) { | |||
this.logger.error( | |||
{ | |||
err, | |||
tnx: 'send', | |||
messageId | |||
}, | |||
'Failed building JSON structure for %s. %s', | |||
messageId, | |||
err.message | |||
); | |||
return done(err); | |||
} | |||
delete data.envelope; | |||
delete data.normalizedHeaders; | |||
return done(null, { | |||
envelope, | |||
messageId, | |||
message: this.options.skipEncoding ? data : JSON.stringify(data) | |||
}); | |||
}); | |||
}); | |||
} | |||
} | |||
module.exports = JSONTransport; |
@ -1,559 +0,0 @@ | |||
/* eslint no-undefined: 0 */ | |||
'use strict'; | |||
const MimeNode = require('../mime-node'); | |||
const mimeFuncs = require('../mime-funcs'); | |||
/** | |||
* Creates the object for composing a MimeNode instance out from the mail options | |||
* | |||
* @constructor | |||
* @param {Object} mail Mail options | |||
*/ | |||
class MailComposer { | |||
constructor(mail) { | |||
this.mail = mail || {}; | |||
this.message = false; | |||
} | |||
/** | |||
* Builds MimeNode instance | |||
*/ | |||
compile() { | |||
this._alternatives = this.getAlternatives(); | |||
this._htmlNode = this._alternatives.filter(alternative => /^text\/html\b/i.test(alternative.contentType)).pop(); | |||
this._attachments = this.getAttachments(!!this._htmlNode); | |||
this._useRelated = !!(this._htmlNode && this._attachments.related.length); | |||
this._useAlternative = this._alternatives.length > 1; | |||
this._useMixed = this._attachments.attached.length > 1 || (this._alternatives.length && this._attachments.attached.length === 1); | |||
// Compose MIME tree | |||
if (this.mail.raw) { | |||
this.message = new MimeNode().setRaw(this.mail.raw); | |||
} else if (this._useMixed) { | |||
this.message = this._createMixed(); | |||
} else if (this._useAlternative) { | |||
this.message = this._createAlternative(); | |||
} else if (this._useRelated) { | |||
this.message = this._createRelated(); | |||
} else { | |||
this.message = this._createContentNode( | |||
false, | |||
[] | |||
.concat(this._alternatives || []) | |||
.concat(this._attachments.attached || []) | |||
.shift() || { | |||
contentType: 'text/plain', | |||
content: '' | |||
} | |||
); | |||
} | |||
// Add custom headers | |||
if (this.mail.headers) { | |||
this.message.addHeader(this.mail.headers); | |||
} | |||
// Add headers to the root node, always overrides custom headers | |||
['from', 'sender', 'to', 'cc', 'bcc', 'reply-to', 'in-reply-to', 'references', 'subject', 'message-id', 'date'].forEach(header => { | |||
let key = header.replace(/-(\w)/g, (o, c) => c.toUpperCase()); | |||
if (this.mail[key]) { | |||
this.message.setHeader(header, this.mail[key]); | |||
} | |||
}); | |||
// Sets custom envelope | |||
if (this.mail.envelope) { | |||
this.message.setEnvelope(this.mail.envelope); | |||
} | |||
// ensure Message-Id value | |||
this.message.messageId(); | |||
return this.message; | |||
} | |||
/** | |||
* List all attachments. Resulting attachment objects can be used as input for MimeNode nodes | |||
* | |||
* @param {Boolean} findRelated If true separate related attachments from attached ones | |||
* @returns {Object} An object of arrays (`related` and `attached`) | |||
*/ | |||
getAttachments(findRelated) { | |||
let icalEvent, eventObject; | |||
let attachments = [].concat(this.mail.attachments || []).map((attachment, i) => { | |||
let data; | |||
let isMessageNode = /^message\//i.test(attachment.contentType); | |||
if (/^data:/i.test(attachment.path || attachment.href)) { | |||
attachment = this._processDataUrl(attachment); | |||
} | |||
data = { | |||
contentType: attachment.contentType || mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin'), | |||
contentDisposition: attachment.contentDisposition || (isMessageNode ? 'inline' : 'attachment'), | |||
contentTransferEncoding: 'contentTransferEncoding' in attachment ? attachment.contentTransferEncoding : 'base64' | |||
}; | |||
if (attachment.filename) { | |||
data.filename = attachment.filename; | |||
} else if (!isMessageNode && attachment.filename !== false) { | |||
data.filename = | |||
(attachment.path || attachment.href || '') | |||
.split('/') | |||
.pop() | |||
.split('?') | |||
.shift() || 'attachment-' + (i + 1); | |||
if (data.filename.indexOf('.') < 0) { | |||
data.filename += '.' + mimeFuncs.detectExtension(data.contentType); | |||
} | |||
} | |||
if (/^https?:\/\//i.test(attachment.path)) { | |||
attachment.href = attachment.path; | |||
attachment.path = undefined; | |||
} | |||
if (attachment.cid) { | |||
data.cid = attachment.cid; | |||
} | |||
if (attachment.raw) { | |||
data.raw = attachment.raw; | |||
} else if (attachment.path) { | |||
data.content = { | |||
path: attachment.path | |||
}; | |||
} else if (attachment.href) { | |||
data.content = { | |||
href: attachment.href, | |||
httpHeaders: attachment.httpHeaders | |||
}; | |||
} else { | |||
data.content = attachment.content || ''; | |||
} | |||
if (attachment.encoding) { | |||
data.encoding = attachment.encoding; | |||
} | |||
if (attachment.headers) { | |||
data.headers = attachment.headers; | |||
} | |||
return data; | |||
}); | |||
if (this.mail.icalEvent) { | |||
if ( | |||
typeof this.mail.icalEvent === 'object' && | |||
(this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw) | |||
) { | |||
icalEvent = this.mail.icalEvent; | |||
} else { | |||
icalEvent = { | |||
content: this.mail.icalEvent | |||
}; | |||
} | |||
eventObject = {}; | |||
Object.keys(icalEvent).forEach(key => { | |||
eventObject[key] = icalEvent[key]; | |||
}); | |||
eventObject.contentType = 'application/ics'; | |||
if (!eventObject.headers) { | |||
eventObject.headers = {}; | |||
} | |||
eventObject.filename = eventObject.filename || 'invite.ics'; | |||
eventObject.headers['Content-Disposition'] = 'attachment'; | |||
eventObject.headers['Content-Transfer-Encoding'] = 'base64'; | |||
} | |||
if (!findRelated) { | |||
return { | |||
attached: attachments.concat(eventObject || []), | |||
related: [] | |||
}; | |||
} else { | |||
return { | |||
attached: attachments.filter(attachment => !attachment.cid).concat(eventObject || []), | |||
related: attachments.filter(attachment => !!attachment.cid) | |||
}; | |||
} | |||
} | |||
/** | |||
* List alternatives. Resulting objects can be used as input for MimeNode nodes | |||
* | |||
* @returns {Array} An array of alternative elements. Includes the `text` and `html` values as well | |||
*/ | |||
getAlternatives() { | |||
let alternatives = [], | |||
text, | |||
html, | |||
watchHtml, | |||
amp, | |||
icalEvent, | |||
eventObject; | |||
if (this.mail.text) { | |||
if (typeof this.mail.text === 'object' && (this.mail.text.content || this.mail.text.path || this.mail.text.href || this.mail.text.raw)) { | |||
text = this.mail.text; | |||
} else { | |||
text = { | |||
content: this.mail.text | |||
}; | |||
} | |||
text.contentType = 'text/plain' + (!text.encoding && mimeFuncs.isPlainText(text.content) ? '' : '; charset=utf-8'); | |||
} | |||
if (this.mail.watchHtml) { | |||
if ( | |||
typeof this.mail.watchHtml === 'object' && | |||
(this.mail.watchHtml.content || this.mail.watchHtml.path || this.mail.watchHtml.href || this.mail.watchHtml.raw) | |||
) { | |||
watchHtml = this.mail.watchHtml; | |||
} else { | |||
watchHtml = { | |||
content: this.mail.watchHtml | |||
}; | |||
} | |||
watchHtml.contentType = 'text/watch-html' + (!watchHtml.encoding && mimeFuncs.isPlainText(watchHtml.content) ? '' : '; charset=utf-8'); | |||
} | |||
if (this.mail.amp) { | |||
if (typeof this.mail.amp === 'object' && (this.mail.amp.content || this.mail.amp.path || this.mail.amp.href || this.mail.amp.raw)) { | |||
amp = this.mail.amp; | |||
} else { | |||
amp = { | |||
content: this.mail.amp | |||
}; | |||
} | |||
amp.contentType = 'text/x-amp-html' + (!amp.encoding && mimeFuncs.isPlainText(amp.content) ? '' : '; charset=utf-8'); | |||
} | |||
// only include the calendar alternative if there are no attachments | |||
// otherwise you might end up in a blank screen on some clients | |||
if (this.mail.icalEvent && !(this.mail.attachments && this.mail.attachments.length)) { | |||
if ( | |||
typeof this.mail.icalEvent === 'object' && | |||
(this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw) | |||
) { | |||
icalEvent = this.mail.icalEvent; | |||
} else { | |||
icalEvent = { | |||
content: this.mail.icalEvent | |||
}; | |||
} | |||
eventObject = {}; | |||
Object.keys(icalEvent).forEach(key => { | |||
eventObject[key] = icalEvent[key]; | |||
}); | |||
if (eventObject.content && typeof eventObject.content === 'object') { | |||
// we are going to have the same attachment twice, so mark this to be | |||
// resolved just once | |||
eventObject.content._resolve = true; | |||
} | |||
eventObject.filename = false; | |||
eventObject.contentType = | |||
'text/calendar; charset="utf-8"; method=' + | |||
(eventObject.method || 'PUBLISH') | |||
.toString() | |||
.trim() | |||
.toUpperCase(); | |||
if (!eventObject.headers) { | |||
eventObject.headers = {}; | |||
} | |||
} | |||
if (this.mail.html) { | |||
if (typeof this.mail.html === 'object' && (this.mail.html.content || this.mail.html.path || this.mail.html.href || this.mail.html.raw)) { | |||
html = this.mail.html; | |||
} else { | |||
html = { | |||
content: this.mail.html | |||
}; | |||
} | |||
html.contentType = 'text/html' + (!html.encoding && mimeFuncs.isPlainText(html.content) ? '' : '; charset=utf-8'); | |||
} | |||
[] | |||
.concat(text || []) | |||
.concat(watchHtml || []) | |||
.concat(amp || []) | |||
.concat(html || []) | |||
.concat(eventObject || []) | |||
.concat(this.mail.alternatives || []) | |||
.forEach(alternative => { | |||
let data; | |||
if (/^data:/i.test(alternative.path || alternative.href)) { | |||
alternative = this._processDataUrl(alternative); | |||
} | |||
data = { | |||
contentType: alternative.contentType || mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'), | |||
contentTransferEncoding: alternative.contentTransferEncoding | |||
}; | |||
if (alternative.filename) { | |||
data.filename = alternative.filename; | |||
} | |||
if (/^https?:\/\//i.test(alternative.path)) { | |||
alternative.href = alternative.path; | |||
alternative.path = undefined; | |||
} | |||
if (alternative.raw) { | |||
data.raw = alternative.raw; | |||
} else if (alternative.path) { | |||
data.content = { | |||
path: alternative.path | |||
}; | |||
} else if (alternative.href) { | |||
data.content = { | |||
href: alternative.href | |||
}; | |||
} else { | |||
data.content = alternative.content || ''; | |||
} | |||
if (alternative.encoding) { | |||
data.encoding = alternative.encoding; | |||
} | |||
if (alternative.headers) { | |||
data.headers = alternative.headers; | |||
} | |||
alternatives.push(data); | |||
}); | |||
return alternatives; | |||
} | |||
/** | |||
* Builds multipart/mixed node. It should always contain different type of elements on the same level | |||
* eg. text + attachments | |||
* | |||
* @param {Object} parentNode Parent for this note. If it does not exist, a root node is created | |||
* @returns {Object} MimeNode node element | |||
*/ | |||
_createMixed(parentNode) { | |||
let node; | |||
if (!parentNode) { | |||
node = new MimeNode('multipart/mixed', { | |||
baseBoundary: this.mail.baseBoundary, | |||
textEncoding: this.mail.textEncoding, | |||
boundaryPrefix: this.mail.boundaryPrefix, | |||
disableUrlAccess: this.mail.disableUrlAccess, | |||
disableFileAccess: this.mail.disableFileAccess, | |||
normalizeHeaderKey: this.mail.normalizeHeaderKey | |||
}); | |||
} else { | |||
node = parentNode.createChild('multipart/mixed', { | |||
disableUrlAccess: this.mail.disableUrlAccess, | |||
disableFileAccess: this.mail.disableFileAccess, | |||
normalizeHeaderKey: this.mail.normalizeHeaderKey | |||
}); | |||
} | |||
if (this._useAlternative) { | |||
this._createAlternative(node); | |||
} else if (this._useRelated) { | |||
this._createRelated(node); | |||
} | |||
[] | |||
.concat((!this._useAlternative && this._alternatives) || []) | |||
.concat(this._attachments.attached || []) | |||
.forEach(element => { | |||
// if the element is a html node from related subpart then ignore it | |||
if (!this._useRelated || element !== this._htmlNode) { | |||
this._createContentNode(node, element); | |||
} | |||
}); | |||
return node; | |||
} | |||
/** | |||
* Builds multipart/alternative node. It should always contain same type of elements on the same level | |||
* eg. text + html view of the same data | |||
* | |||
* @param {Object} parentNode Parent for this note. If it does not exist, a root node is created | |||
* @returns {Object} MimeNode node element | |||
*/ | |||
_createAlternative(parentNode) { | |||
let node; | |||
if (!parentNode) { | |||
node = new MimeNode('multipart/alternative', { | |||
baseBoundary: this.mail.baseBoundary, | |||
textEncoding: this.mail.textEncoding, | |||
boundaryPrefix: this.mail.boundaryPrefix, | |||
disableUrlAccess: this.mail.disableUrlAccess, | |||
disableFileAccess: this.mail.disableFileAccess, | |||
normalizeHeaderKey: this.mail.normalizeHeaderKey | |||
}); | |||
} else { | |||
node = parentNode.createChild('multipart/alternative', { | |||
disableUrlAccess: this.mail.disableUrlAccess, | |||
disableFileAccess: this.mail.disableFileAccess, | |||
normalizeHeaderKey: this.mail.normalizeHeaderKey | |||
}); | |||
} | |||
this._alternatives.forEach(alternative => { | |||
if (this._useRelated && this._htmlNode === alternative) { | |||
this._createRelated(node); | |||
} else { | |||
this._createContentNode(node, alternative); | |||
} | |||
}); | |||
return node; | |||
} | |||
/** | |||
* Builds multipart/related node. It should always contain html node with related attachments | |||
* | |||
* @param {Object} parentNode Parent for this note. If it does not exist, a root node is created | |||
* @returns {Object} MimeNode node element | |||
*/ | |||
_createRelated(parentNode) { | |||
let node; | |||
if (!parentNode) { | |||
node = new MimeNode('multipart/related; type="text/html"', { | |||
baseBoundary: this.mail.baseBoundary, | |||
textEncoding: this.mail.textEncoding, | |||
boundaryPrefix: this.mail.boundaryPrefix, | |||
disableUrlAccess: this.mail.disableUrlAccess, | |||
disableFileAccess: this.mail.disableFileAccess, | |||
normalizeHeaderKey: this.mail.normalizeHeaderKey | |||
}); | |||
} else { | |||
node = parentNode.createChild('multipart/related; type="text/html"', { | |||
disableUrlAccess: this.mail.disableUrlAccess, | |||
disableFileAccess: this.mail.disableFileAccess, | |||
normalizeHeaderKey: this.mail.normalizeHeaderKey | |||
}); | |||
} | |||
this._createContentNode(node, this._htmlNode); | |||
this._attachments.related.forEach(alternative => this._createContentNode(node, alternative)); | |||
return node; | |||
} | |||
/** | |||
* Creates a regular node with contents | |||
* | |||
* @param {Object} parentNode Parent for this note. If it does not exist, a root node is created | |||
* @param {Object} element Node data | |||
* @returns {Object} MimeNode node element | |||
*/ | |||
_createContentNode(parentNode, element) { | |||
element = element || {}; | |||
element.content = element.content || ''; | |||
let node; | |||
let encoding = (element.encoding || 'utf8') | |||
.toString() | |||
.toLowerCase() | |||
.replace(/[-_\s]/g, ''); | |||
if (!parentNode) { | |||
node = new MimeNode(element.contentType, { | |||
filename: element.filename, | |||
baseBoundary: this.mail.baseBoundary, | |||
textEncoding: this.mail.textEncoding, | |||
boundaryPrefix: this.mail.boundaryPrefix, | |||
disableUrlAccess: this.mail.disableUrlAccess, | |||
disableFileAccess: this.mail.disableFileAccess | |||
}); | |||
} else { | |||
node = parentNode.createChild(element.contentType, { | |||
filename: element.filename, | |||
disableUrlAccess: this.mail.disableUrlAccess, | |||
disableFileAccess: this.mail.disableFileAccess, | |||
normalizeHeaderKey: this.mail.normalizeHeaderKey | |||
}); | |||
} | |||
// add custom headers | |||
if (element.headers) { | |||
node.addHeader(element.headers); | |||
} | |||
if (element.cid) { | |||
node.setHeader('Content-Id', '<' + element.cid.replace(/[<>]/g, '') + '>'); | |||
} | |||
if (element.contentTransferEncoding) { | |||
node.setHeader('Content-Transfer-Encoding', element.contentTransferEncoding); | |||
} else if (this.mail.encoding && /^text\//i.test(element.contentType)) { | |||
node.setHeader('Content-Transfer-Encoding', this.mail.encoding); | |||
} | |||
if (!/^text\//i.test(element.contentType) || element.contentDisposition) { | |||
node.setHeader('Content-Disposition', element.contentDisposition || (element.cid ? 'inline' : 'attachment')); | |||
} | |||
if (typeof element.content === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) { | |||
element.content = Buffer.from(element.content, encoding); | |||
} | |||
// prefer pregenerated raw content | |||
if (element.raw) { | |||
node.setRaw(element.raw); | |||
} else { | |||
node.setContent(element.content); | |||
} | |||
return node; | |||
} | |||
/** | |||
* Parses data uri and converts it to a Buffer | |||
* | |||
* @param {Object} element Content element | |||
* @return {Object} Parsed element | |||
*/ | |||
_processDataUrl(element) { | |||
let parts = (element.path || element.href).match(/^data:((?:[^;]*;)*(?:[^,]*)),(.*)$/i); | |||
if (!parts) { | |||
return element; | |||
} | |||
element.content = /\bbase64$/i.test(parts[1]) ? Buffer.from(parts[2], 'base64') : Buffer.from(decodeURIComponent(parts[2])); | |||
if ('path' in element) { | |||
element.path = false; | |||
} | |||
if ('href' in element) { | |||
element.href = false; | |||
} | |||
parts[1].split(';').forEach(item => { | |||
if (/^\w+\/[^/]+$/i.test(item)) { | |||
element.contentType = element.contentType || item.toLowerCase(); | |||
} | |||
}); | |||
return element; | |||
} | |||
} | |||
module.exports = MailComposer; |
@ -1,423 +0,0 @@ | |||
'use strict'; | |||
const EventEmitter = require('events'); | |||
const shared = require('../shared'); | |||
const mimeTypes = require('../mime-funcs/mime-types'); | |||
const MailComposer = require('../mail-composer'); | |||
const DKIM = require('../dkim'); | |||
const httpProxyClient = require('../smtp-connection/http-proxy-client'); | |||
const util = require('util'); | |||
const urllib = require('url'); | |||
const packageData = require('../../package.json'); | |||
const MailMessage = require('./mail-message'); | |||
const net = require('net'); | |||
const dns = require('dns'); | |||
const crypto = require('crypto'); | |||
/** | |||
* Creates an object for exposing the Mail API | |||
* | |||
* @constructor | |||
* @param {Object} transporter Transport object instance to pass the mails to | |||
*/ | |||
class Mail extends EventEmitter { | |||
constructor(transporter, options, defaults) { | |||
super(); | |||
this.options = options || {}; | |||
this._defaults = defaults || {}; | |||
this._defaultPlugins = { | |||
compile: [(...args) => this._convertDataImages(...args)], | |||
stream: [] | |||
}; | |||
this._userPlugins = { | |||
compile: [], | |||
stream: [] | |||
}; | |||
this.meta = new Map(); | |||
this.dkim = this.options.dkim ? new DKIM(this.options.dkim) : false; | |||
this.transporter = transporter; | |||
this.transporter.mailer = this; | |||
this.logger = shared.getLogger(this.options, { | |||
component: this.options.component || 'mail' | |||
}); | |||
this.logger.debug( | |||
{ | |||
tnx: 'create' | |||
}, | |||
'Creating transport: %s', | |||
this.getVersionString() | |||
); | |||
// setup emit handlers for the transporter | |||
if (typeof this.transporter.on === 'function') { | |||
// deprecated log interface | |||
this.transporter.on('log', log => { | |||
this.logger.debug( | |||
{ | |||
tnx: 'transport' | |||
}, | |||
'%s: %s', | |||
log.type, | |||
log.message | |||
); | |||
}); | |||
// transporter errors | |||
this.transporter.on('error', err => { | |||
this.logger.error( | |||
{ | |||
err, | |||
tnx: 'transport' | |||
}, | |||
'Transport Error: %s', | |||
err.message | |||
); | |||
this.emit('error', err); | |||
}); | |||
// indicates if the sender has became idle | |||
this.transporter.on('idle', (...args) => { | |||
this.emit('idle', ...args); | |||
}); | |||
} | |||
/** | |||
* Optional methods passed to the underlying transport object | |||
*/ | |||
['close', 'isIdle', 'verify'].forEach(method => { | |||
this[method] = (...args) => { | |||
if (typeof this.transporter[method] === 'function') { | |||
return this.transporter[method](...args); | |||
} else { | |||
this.logger.warn( | |||
{ | |||
tnx: 'transport', | |||
methodName: method | |||
}, | |||
'Non existing method %s called for transport', | |||
method | |||
); | |||
return false; | |||
} | |||
}; | |||
}); | |||
// setup proxy handling | |||
if (this.options.proxy && typeof this.options.proxy === 'string') { | |||
this.setupProxy(this.options.proxy); | |||
} | |||
} | |||
use(step, plugin) { | |||
step = (step || '').toString(); | |||
if (!this._userPlugins.hasOwnProperty(step)) { | |||
this._userPlugins[step] = [plugin]; | |||
} else { | |||
this._userPlugins[step].push(plugin); | |||
} | |||
return this; | |||
} | |||
/** | |||
* Sends an email using the preselected transport object | |||
* | |||
* @param {Object} data E-data description | |||
* @param {Function?} callback Callback to run once the sending succeeded or failed | |||
*/ | |||
sendMail(data, callback) { | |||
let promise; | |||
if (!callback) { | |||
promise = new Promise((resolve, reject) => { | |||
callback = shared.callbackPromise(resolve, reject); | |||
}); | |||
} | |||
if (typeof this.getSocket === 'function') { | |||
this.transporter.getSocket = this.getSocket; | |||
this.getSocket = false; | |||
} | |||
let mail = new MailMessage(this, data); | |||
this.logger.debug( | |||
{ | |||
tnx: 'transport', | |||
name: this.transporter.name, | |||
version: this.transporter.version, | |||
action: 'send' | |||
}, | |||
'Sending mail using %s/%s', | |||
this.transporter.name, | |||
this.transporter.version | |||
); | |||
this._processPlugins('compile', mail, err => { | |||
if (err) { | |||
this.logger.error( | |||
{ | |||
err, | |||
tnx: 'plugin', | |||
action: 'compile' | |||
}, | |||
'PluginCompile Error: %s', | |||
err.message | |||
); | |||
return callback(err); | |||
} | |||
mail.message = new MailComposer(mail.data).compile(); | |||
mail.setMailerHeader(); | |||
mail.setPriorityHeaders(); | |||
mail.setListHeaders(); | |||
this._processPlugins('stream', mail, err => { | |||
if (err) { | |||
this.logger.error( | |||
{ | |||
err, | |||
tnx: 'plugin', | |||
action: 'stream' | |||
}, | |||
'PluginStream Error: %s', | |||
err.message | |||
); | |||
return callback(err); | |||
} | |||
if (mail.data.dkim || this.dkim) { | |||
mail.message.processFunc(input => { | |||
let dkim = mail.data.dkim ? new DKIM(mail.data.dkim) : this.dkim; | |||
this.logger.debug( | |||
{ | |||
tnx: 'DKIM', | |||
messageId: mail.message.messageId(), | |||
dkimDomains: dkim.keys.map(key => key.keySelector + '.' + key.domainName).join(', ') | |||
}, | |||
'Signing outgoing message with %s keys', | |||
dkim.keys.length | |||
); | |||
return dkim.sign(input, mail.data._dkim); | |||
}); | |||
} | |||
this.transporter.send(mail, (...args) => { | |||
if (args[0]) { | |||
this.logger.error( | |||
{ | |||
err: args[0], | |||
tnx: 'transport', | |||
action: 'send' | |||
}, | |||
'Send Error: %s', | |||
args[0].message | |||
); | |||
} | |||
callback(...args); | |||
}); | |||
}); | |||
}); | |||
return promise; | |||
} | |||
getVersionString() { | |||
return util.format('%s (%s; +%s; %s/%s)', packageData.name, packageData.version, packageData.homepage, this.transporter.name, this.transporter.version); | |||
} | |||
_processPlugins(step, mail, callback) { | |||
step = (step || '').toString(); | |||
if (!this._userPlugins.hasOwnProperty(step)) { | |||
return callback(); | |||
} | |||
let userPlugins = this._userPlugins[step] || []; | |||
let defaultPlugins = this._defaultPlugins[step] || []; | |||
if (userPlugins.length) { | |||
this.logger.debug( | |||
{ | |||
tnx: 'transaction', | |||
pluginCount: userPlugins.length, | |||
step | |||
}, | |||
'Using %s plugins for %s', | |||
userPlugins.length, | |||
step | |||
); | |||
} | |||
if (userPlugins.length + defaultPlugins.length === 0) { | |||
return callback(); | |||
} | |||
let pos = 0; | |||
let block = 'default'; | |||
let processPlugins = () => { | |||
let curplugins = block === 'default' ? defaultPlugins : userPlugins; | |||
if (pos >= curplugins.length) { | |||
if (block === 'default' && userPlugins.length) { | |||
block = 'user'; | |||
pos = 0; | |||
curplugins = userPlugins; | |||
} else { | |||
return callback(); | |||
} | |||
} | |||
let plugin = curplugins[pos++]; | |||
plugin(mail, err => { | |||
if (err) { | |||
return callback(err); | |||
} | |||
processPlugins(); | |||
}); | |||
}; | |||
processPlugins(); | |||
} | |||
/** | |||
* Sets up proxy handler for a Nodemailer object | |||
* | |||
* @param {String} proxyUrl Proxy configuration url | |||
*/ | |||
setupProxy(proxyUrl) { | |||
let proxy = urllib.parse(proxyUrl); | |||
// setup socket handler for the mailer object | |||
this.getSocket = (options, callback) => { | |||
let protocol = proxy.protocol.replace(/:$/, '').toLowerCase(); | |||
if (this.meta.has('proxy_handler_' + protocol)) { | |||
return this.meta.get('proxy_handler_' + protocol)(proxy, options, callback); | |||
} | |||
switch (protocol) { | |||
// Connect using a HTTP CONNECT method | |||
case 'http': | |||
case 'https': | |||
httpProxyClient(proxy.href, options.port, options.host, (err, socket) => { | |||
if (err) { | |||
return callback(err); | |||
} | |||
return callback(null, { | |||
connection: socket | |||
}); | |||
}); | |||
return; | |||
case 'socks': | |||
case 'socks5': | |||
case 'socks4': | |||
case 'socks4a': { | |||
if (!this.meta.has('proxy_socks_module')) { | |||
return callback(new Error('Socks module not loaded')); | |||
} | |||
let connect = ipaddress => { | |||
let proxyV2 = !!this.meta.get('proxy_socks_module').SocksClient; | |||
let socksClient = proxyV2 ? this.meta.get('proxy_socks_module').SocksClient : this.meta.get('proxy_socks_module'); | |||
let proxyType = Number(proxy.protocol.replace(/\D/g, '')) || 5; | |||
let connectionOpts = { | |||
proxy: { | |||
ipaddress, | |||
port: Number(proxy.port), | |||
type: proxyType | |||
}, | |||
[proxyV2 ? 'destination' : 'target']: { | |||
host: options.host, | |||
port: options.port | |||
}, | |||
command: 'connect' | |||
}; | |||
if (proxy.auth) { | |||
let username = decodeURIComponent(proxy.auth.split(':').shift()); | |||
let password = decodeURIComponent(proxy.auth.split(':').pop()); | |||
if (proxyV2) { | |||
connectionOpts.proxy.userId = username; | |||
connectionOpts.proxy.password = password; | |||
} else if (proxyType === 4) { | |||
connectionOpts.userid = username; | |||
} else { | |||
connectionOpts.authentication = { | |||
username, | |||
password | |||
}; | |||
} | |||
} | |||
socksClient.createConnection(connectionOpts, (err, info) => { | |||
if (err) { | |||
return callback(err); | |||
} | |||
return callback(null, { | |||
connection: info.socket || info | |||
}); | |||
}); | |||
}; | |||
if (net.isIP(proxy.hostname)) { | |||
return connect(proxy.hostname); | |||
} | |||
return dns.resolve(proxy.hostname, (err, address) => { | |||
if (err) { | |||
return callback(err); | |||
} | |||
connect(Array.isArray(address) ? address[0] : address); | |||
}); | |||
} | |||
} | |||
callback(new Error('Unknown proxy configuration')); | |||
}; | |||
} | |||
_convertDataImages(mail, callback) { | |||
if ((!this.options.attachDataUrls && !mail.data.attachDataUrls) || !mail.data.html) { | |||
return callback(); | |||
} | |||
mail.resolveContent(mail.data, 'html', (err, html) => { | |||
if (err) { | |||
return callback(err); | |||
} | |||
let cidCounter = 0; | |||
html = (html || '').toString().replace(/(<img\b[^>]* src\s*=[\s"']*)(data:([^;]+);[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => { | |||
let cid = crypto.randomBytes(10).toString('hex') + '@localhost'; | |||
if (!mail.data.attachments) { | |||
mail.data.attachments = []; | |||
} | |||
if (!Array.isArray(mail.data.attachments)) { | |||
mail.data.attachments = [].concat(mail.data.attachments || []); | |||
} | |||
mail.data.attachments.push({ | |||
path: dataUri, | |||
cid, | |||
filename: 'image-' + ++cidCounter + '.' + mimeTypes.detectExtension(mimeType) | |||
}); | |||
return prefix + 'cid:' + cid; | |||
}); | |||
mail.data.html = html; | |||
callback(); | |||
}); | |||
} | |||
set(key, value) { | |||
return this.meta.set(key, value); | |||
} | |||
get(key) { | |||
return this.meta.get(key); | |||
} | |||
} | |||
module.exports = Mail; |
@ -1,320 +0,0 @@ | |||
'use strict'; | |||
const shared = require('../shared'); | |||
const MimeNode = require('../mime-node'); | |||
const mimeFuncs = require('../mime-funcs'); | |||
class MailMessage { | |||
constructor(mailer, data) { | |||
this.mailer = mailer; | |||
this.data = {}; | |||
this.message = null; | |||
data = data || {}; | |||
let options = mailer.options || {}; | |||
let defaults = mailer._defaults || {}; | |||
Object.keys(data).forEach(key => { | |||
this.data[key] = data[key]; | |||
}); | |||
this.data.headers = this.data.headers || {}; | |||
// apply defaults | |||
Object.keys(defaults).forEach(key => { | |||
if (!(key in this.data)) { | |||
this.data[key] = defaults[key]; | |||
} else if (key === 'headers') { | |||
// headers is a special case. Allow setting individual default headers | |||
Object.keys(defaults.headers).forEach(key => { | |||
if (!(key in this.data.headers)) { | |||
this.data.headers[key] = defaults.headers[key]; | |||
} | |||
}); | |||
} | |||
}); | |||
// force specific keys from transporter options | |||
['disableFileAccess', 'disableUrlAccess', 'normalizeHeaderKey'].forEach(key => { | |||
if (key in options) { | |||
this.data[key] = options[key]; | |||
} | |||
}); | |||
} | |||
resolveContent(...args) { | |||
return shared.resolveContent(...args); | |||
} | |||
resolveAll(callback) { | |||
let keys = [ | |||
[this.data, 'html'], | |||
[this.data, 'text'], | |||
[this.data, 'watchHtml'], | |||
[this.data, 'amp'], | |||
[this.data, 'icalEvent'] | |||
]; | |||
if (this.data.alternatives && this.data.alternatives.length) { | |||
this.data.alternatives.forEach((alternative, i) => { | |||
keys.push([this.data.alternatives, i]); | |||
}); | |||
} | |||
if (this.data.attachments && this.data.attachments.length) { | |||
this.data.attachments.forEach((attachment, i) => { | |||
if (!attachment.filename) { | |||
attachment.filename = | |||
(attachment.path || attachment.href || '') | |||
.split('/') | |||
.pop() | |||
.split('?') | |||
.shift() || 'attachment-' + (i + 1); | |||
if (attachment.filename.indexOf('.') < 0) { | |||
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType); | |||
} | |||
} | |||
if (!attachment.contentType) { | |||
attachment.contentType = mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin'); | |||
} | |||
keys.push([this.data.attachments, i]); | |||
}); | |||
} | |||
let mimeNode = new MimeNode(); | |||
let addressKeys = ['from', 'to', 'cc', 'bcc', 'sender', 'replyTo']; | |||
addressKeys.forEach(address => { | |||
let value; | |||
if (this.message) { | |||
value = [].concat(mimeNode._parseAddresses(this.message.getHeader(address === 'replyTo' ? 'reply-to' : address)) || []); | |||
} else if (this.data[address]) { | |||
value = [].concat(mimeNode._parseAddresses(this.data[address]) || []); | |||
} | |||
if (value && value.length) { | |||
this.data[address] = value; | |||
} else if (address in this.data) { | |||
this.data[address] = null; | |||
} | |||
}); | |||
let singleKeys = ['from', 'sender', 'replyTo']; | |||
singleKeys.forEach(address => { | |||
if (this.data[address]) { | |||
this.data[address] = this.data[address].shift(); | |||
} | |||
}); | |||
let pos = 0; | |||
let resolveNext = () => { | |||
if (pos >= keys.length) { | |||
return callback(null, this.data); | |||
} | |||
let args = keys[pos++]; | |||
if (!args[0] || !args[0][args[1]]) { | |||
return resolveNext(); | |||
} | |||
shared.resolveContent(...args, (err, value) => { | |||
if (err) { | |||
return callback(err); | |||
} | |||
let node = { | |||
content: value | |||
}; | |||
if (args[0][args[1]] && typeof args[0][args[1]] === 'object' && !Buffer.isBuffer(args[0][args[1]])) { | |||
Object.keys(args[0][args[1]]).forEach(key => { | |||
if (!(key in node) && !['content', 'path', 'href', 'raw'].includes(key)) { | |||
node[key] = args[0][args[1]][key]; | |||
} | |||
}); | |||
} | |||
args[0][args[1]] = node; | |||
resolveNext(); | |||
}); | |||
}; | |||
setImmediate(() => resolveNext()); | |||
} | |||
normalize(callback) { | |||
let envelope = this.data.envelope || this.message.getEnvelope(); | |||
let messageId = this.message.messageId(); | |||
this.resolveAll((err, data) => { | |||
if (err) { | |||
return callback(err); | |||
} | |||
data.envelope = envelope; | |||
data.messageId = messageId; | |||
['html', 'text', 'watchHtml', 'amp'].forEach(key => { | |||
if (data[key] && data[key].content) { | |||
if (typeof data[key].content === 'string') { | |||
data[key] = data[key].content; | |||
} else if (Buffer.isBuffer(data[key].content)) { | |||
data[key] = data[key].content.toString(); | |||
} | |||
} | |||
}); | |||
if (data.icalEvent && Buffer.isBuffer(data.icalEvent.content)) { | |||
data.icalEvent.content = data.icalEvent.content.toString('base64'); | |||
data.icalEvent.encoding = 'base64'; | |||
} | |||
if (data.alternatives && data.alternatives.length) { | |||
data.alternatives.forEach(alternative => { | |||
if (alternative && alternative.content && Buffer.isBuffer(alternative.content)) { | |||
alternative.content = alternative.content.toString('base64'); | |||
alternative.encoding = 'base64'; | |||
} | |||
}); | |||
} | |||
if (data.attachments && data.attachments.length) { | |||
data.attachments.forEach(attachment => { | |||
if (attachment && attachment.content && Buffer.isBuffer(attachment.content)) { | |||
attachment.content = attachment.content.toString('base64'); | |||
attachment.encoding = 'base64'; | |||
} | |||
}); | |||
} | |||
data.normalizedHeaders = {}; | |||
Object.keys(data.headers || {}).forEach(key => { | |||
let value = [].concat(data.headers[key] || []).shift(); | |||
value = (value && value.value) || value; | |||
if (value) { | |||
if (['references', 'in-reply-to', 'message-id', 'content-id'].includes(key)) { | |||
value = this.message._encodeHeaderValue(key, value); | |||
} | |||
data.normalizedHeaders[key] = value; | |||
} | |||
}); | |||
if (data.list && typeof data.list === 'object') { | |||
let listHeaders = this._getListHeaders(data.list); | |||
listHeaders.forEach(entry => { | |||
data.normalizedHeaders[entry.key] = entry.value.map(val => (val && val.value) || val).join(', '); | |||
}); | |||
} | |||
if (data.references) { | |||
data.normalizedHeaders.references = this.message._encodeHeaderValue('references', data.references); | |||
} | |||
if (data.inReplyTo) { | |||
data.normalizedHeaders['in-reply-to'] = this.message._encodeHeaderValue('in-reply-to', data.inReplyTo); | |||
} | |||
return callback(null, data); | |||
}); | |||
} | |||
setMailerHeader() { | |||
if (!this.message || !this.data.xMailer) { | |||
return; | |||
} | |||
this.message.setHeader('X-Mailer', this.data.xMailer); | |||
} | |||
setPriorityHeaders() { | |||
if (!this.message || !this.data.priority) { | |||
return; | |||
} | |||
switch ((this.data.priority || '').toString().toLowerCase()) { | |||
case 'high': | |||
this.message.setHeader('X-Priority', '1 (Highest)'); | |||
this.message.setHeader('X-MSMail-Priority', 'High'); | |||
this.message.setHeader('Importance', 'High'); | |||
break; | |||
case 'low': | |||
this.message.setHeader('X-Priority', '5 (Lowest)'); | |||
this.message.setHeader('X-MSMail-Priority', 'Low'); | |||
this.message.setHeader('Importance', 'Low'); | |||
break; | |||
default: | |||
// do not add anything, since all messages are 'Normal' by default | |||
} | |||
} | |||
setListHeaders() { | |||
if (!this.message || !this.data.list || typeof this.data.list !== 'object') { | |||
return; | |||
} | |||
// add optional List-* headers | |||
if (this.data.list && typeof this.data.list === 'object') { | |||
this._getListHeaders(this.data.list).forEach(listHeader => { | |||
listHeader.value.forEach(value => { | |||
this.message.addHeader(listHeader.key, value); | |||
}); | |||
}); | |||
} | |||
} | |||
_getListHeaders(listData) { | |||
// make sure an url looks like <protocol:url> | |||
return Object.keys(listData).map(key => ({ | |||
key: 'list-' + key.toLowerCase().trim(), | |||
value: [].concat(listData[key] || []).map(value => ({ | |||
prepared: true, | |||
foldLines: true, | |||
value: [] | |||
.concat(value || []) | |||
.map(value => { | |||
if (typeof value === 'string') { | |||
value = { | |||
url: value | |||
}; | |||
} | |||
if (value && value.url) { | |||
if (key.toLowerCase().trim() === 'id') { | |||
// List-ID: "comment" <domain> | |||
let comment = value.comment || ''; | |||
if (mimeFuncs.isPlainText(comment)) { | |||
comment = '"' + comment + '"'; | |||
} else { | |||
comment = mimeFuncs.encodeWord(comment); | |||
} | |||
return (value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+\/{,2}/, ''); | |||
} | |||
// List-*: <http://domain> (comment) | |||
let comment = value.comment || ''; | |||
if (!mimeFuncs.isPlainText(comment)) { | |||
comment = mimeFuncs.encodeWord(comment); | |||
} | |||
return this._formatListUrl(value.url) + (value.comment ? ' (' + comment + ')' : ''); | |||
} | |||
return ''; | |||
}) | |||
.filter(value => value) | |||
.join(', ') | |||
})) | |||
})); | |||
} | |||
_formatListUrl(url) { | |||
url = url.replace(/[\s<]+|[\s>]+/g, ''); | |||
if (/^(https?|mailto|ftp):/.test(url)) { | |||
return '<' + url + '>'; | |||
} | |||
if (/^[^@]+@[^@]+$/.test(url)) { | |||
return '<mailto:' + url + '>'; | |||
} | |||
return '<http://' + url + '>'; | |||
} | |||
} | |||
module.exports = MailMessage; |
@ -1,628 +0,0 @@ | |||
/* eslint no-control-regex:0 */ | |||
'use strict'; | |||
const base64 = require('../base64'); | |||
const qp = require('../qp'); | |||
const mimeTypes = require('./mime-types'); | |||
module.exports = { | |||
/** | |||
* Checks if a value is plaintext string (uses only printable 7bit chars) | |||
* | |||
* @param {String} value String to be tested | |||
* @returns {Boolean} true if it is a plaintext string | |||
*/ | |||
isPlainText(value) { | |||
if (typeof value !== 'string' || /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/.test(value)) { | |||
return false; | |||
} else { | |||
return true; | |||
} | |||
}, | |||
/** | |||
* Checks if a multi line string containes lines longer than the selected value. | |||
* | |||
* Useful when detecting if a mail message needs any processing at all – | |||
* if only plaintext characters are used and lines are short, then there is | |||
* no need to encode the values in any way. If the value is plaintext but has | |||
* longer lines then allowed, then use format=flowed | |||
* | |||
* @param {Number} lineLength Max line length to check for | |||
* @returns {Boolean} Returns true if there is at least one line longer than lineLength chars | |||
*/ | |||
hasLongerLines(str, lineLength) { | |||
if (str.length > 128 * 1024) { | |||
// do not test strings longer than 128kB | |||
return true; | |||
} | |||
return new RegExp('^.{' + (lineLength + 1) + ',}', 'm').test(str); | |||
}, | |||
/** | |||
* Encodes a string or an Buffer to an UTF-8 MIME Word (rfc2047) | |||
* | |||
* @param {String|Buffer} data String to be encoded | |||
* @param {String} mimeWordEncoding='Q' Encoding for the mime word, either Q or B | |||
* @param {Number} [maxLength=0] If set, split mime words into several chunks if needed | |||
* @return {String} Single or several mime words joined together | |||
*/ | |||
encodeWord(data, mimeWordEncoding, maxLength) { | |||
mimeWordEncoding = (mimeWordEncoding || 'Q') | |||
.toString() | |||
.toUpperCase() | |||
.trim() | |||
.charAt(0); | |||
maxLength = maxLength || 0; | |||
let encodedStr; | |||
let toCharset = 'UTF-8'; | |||
if (maxLength && maxLength > 7 + toCharset.length) { | |||
maxLength -= 7 + toCharset.length; | |||
} | |||
if (mimeWordEncoding === 'Q') { | |||
// https://tools.ietf.org/html/rfc2047#section-5 rule (3) | |||
encodedStr = qp.encode(data).replace(/[^a-z0-9!*+\-/=]/gi, chr => { | |||
let ord = chr | |||
.charCodeAt(0) | |||
.toString(16) | |||
.toUpperCase(); | |||
if (chr === ' ') { | |||
return '_'; | |||
} else { | |||
return '=' + (ord.length === 1 ? '0' + ord : ord); | |||
} | |||
}); | |||
} else if (mimeWordEncoding === 'B') { | |||
encodedStr = typeof data === 'string' ? data : base64.encode(data); | |||
maxLength = maxLength ? Math.max(3, ((maxLength - (maxLength % 4)) / 4) * 3) : 0; | |||
} | |||
if (maxLength && (mimeWordEncoding !== 'B' ? encodedStr : base64.encode(data)).length > maxLength) { | |||
if (mimeWordEncoding === 'Q') { | |||
encodedStr = this.splitMimeEncodedString(encodedStr, maxLength).join('?= =?' + toCharset + '?' + mimeWordEncoding + '?'); | |||
} else { | |||
// RFC2047 6.3 (2) states that encoded-word must include an integral number of characters, so no chopping unicode sequences | |||
let parts = []; | |||
let lpart = ''; | |||
for (let i = 0, len = encodedStr.length; i < len; i++) { | |||
let chr = encodedStr.charAt(i); | |||
// check if we can add this character to the existing string | |||
// without breaking byte length limit | |||
if (Buffer.byteLength(lpart + chr) <= maxLength || i === 0) { | |||
lpart += chr; | |||
} else { | |||
// we hit the length limit, so push the existing string and start over | |||
parts.push(base64.encode(lpart)); | |||
lpart = chr; | |||
} | |||
} | |||
if (lpart) { | |||
parts.push(base64.encode(lpart)); | |||
} | |||
if (parts.length > 1) { | |||
encodedStr = parts.join('?= =?' + toCharset + '?' + mimeWordEncoding + '?'); | |||
} else { | |||
encodedStr = parts.join(''); | |||
} | |||
} | |||
} else if (mimeWordEncoding === 'B') { | |||
encodedStr = base64.encode(data); | |||
} | |||
return '=?' + toCharset + '?' + mimeWordEncoding + '?' + encodedStr + (encodedStr.substr(-2) === '?=' ? '' : '?='); | |||
}, | |||
/** | |||
* Finds word sequences with non ascii text and converts these to mime words | |||
* | |||
* @param {String} value String to be encoded | |||
* @param {String} mimeWordEncoding='Q' Encoding for the mime word, either Q or B | |||
* @param {Number} [maxLength=0] If set, split mime words into several chunks if needed | |||
* @param {Boolean} [encodeAll=false] If true and the value needs encoding then encodes entire string, not just the smallest match | |||
* @return {String} String with possible mime words | |||
*/ | |||
encodeWords(value, mimeWordEncoding, maxLength, encodeAll) { | |||
maxLength = maxLength || 0; | |||
let encodedValue; | |||
// find first word with a non-printable ascii in it | |||
let firstMatch = value.match(/(?:^|\s)([^\s]*[\u0080-\uFFFF])/); | |||
if (!firstMatch) { | |||
return value; | |||
} | |||
if (encodeAll) { | |||
// if it is requested to encode everything or the string contains something that resebles encoded word, then encode everything | |||
return this.encodeWord(value, mimeWordEncoding, maxLength); | |||
} | |||
// find the last word with a non-printable ascii in it | |||
let lastMatch = value.match(/([\u0080-\uFFFF][^\s]*)[^\u0080-\uFFFF]*$/); | |||
if (!lastMatch) { | |||
// should not happen | |||
return value; | |||
} | |||
let startIndex = | |||
firstMatch.index + | |||
( | |||
firstMatch[0].match(/[^\s]/) || { | |||
index: 0 | |||
} | |||
).index; | |||
let endIndex = lastMatch.index + (lastMatch[1] || '').length; | |||
encodedValue = | |||
(startIndex ? value.substr(0, startIndex) : '') + | |||
this.encodeWord(value.substring(startIndex, endIndex), mimeWordEncoding || 'Q', maxLength) + | |||
(endIndex < value.length ? value.substr(endIndex) : ''); | |||
return encodedValue; | |||
}, | |||
/** | |||
* Joins parsed header value together as 'value; param1=value1; param2=value2' | |||
* PS: We are following RFC 822 for the list of special characters that we need to keep in quotes. | |||
* Refer: https://www.w3.org/Protocols/rfc1341/4_Content-Type.html | |||
* @param {Object} structured Parsed header value | |||
* @return {String} joined header value | |||
*/ | |||
buildHeaderValue(structured) { | |||
let paramsArray = []; | |||
Object.keys(structured.params || {}).forEach(param => { | |||
// filename might include unicode characters so it is a special case | |||
// other values probably do not | |||
let value = structured.params[param]; | |||
if (!this.isPlainText(value) || value.length >= 75) { | |||
this.buildHeaderParam(param, value, 50).forEach(encodedParam => { | |||
if (!/[\s"\\;:/=(),<>@[\]?]|^[-']|'$/.test(encodedParam.value) || encodedParam.key.substr(-1) === '*') { | |||
paramsArray.push(encodedParam.key + '=' + encodedParam.value); | |||
} else { | |||
paramsArray.push(encodedParam.key + '=' + JSON.stringify(encodedParam.value)); | |||
} | |||
}); | |||
} else if (/[\s'"\\;:/=(),<>@[\]?]|^-/.test(value)) { | |||
paramsArray.push(param + '=' + JSON.stringify(value)); | |||
} else { | |||
paramsArray.push(param + '=' + value); | |||
} | |||
}); | |||
return structured.value + (paramsArray.length ? '; ' + paramsArray.join('; ') : ''); | |||
}, | |||
/** | |||
* Encodes a string or an Buffer to an UTF-8 Parameter Value Continuation encoding (rfc2231) | |||
* Useful for splitting long parameter values. | |||
* | |||
* For example | |||
* title="unicode string" | |||
* becomes | |||
* title*0*=utf-8''unicode | |||
* title*1*=%20string | |||
* | |||
* @param {String|Buffer} data String to be encoded | |||
* @param {Number} [maxLength=50] Max length for generated chunks | |||
* @param {String} [fromCharset='UTF-8'] Source sharacter set | |||
* @return {Array} A list of encoded keys and headers | |||
*/ | |||
buildHeaderParam(key, data, maxLength) { | |||
let list = []; | |||
let encodedStr = typeof data === 'string' ? data : (data || '').toString(); | |||
let encodedStrArr; | |||
let chr, ord; | |||
let line; | |||
let startPos = 0; | |||
let i, len; | |||
maxLength = maxLength || 50; | |||
// process ascii only text | |||
if (this.isPlainText(data)) { | |||
// check if conversion is even needed | |||
if (encodedStr.length <= maxLength) { | |||
return [ | |||
{ | |||
key, | |||
value: encodedStr | |||
} | |||
]; | |||
} | |||
encodedStr = encodedStr.replace(new RegExp('.{' + maxLength + '}', 'g'), str => { | |||
list.push({ | |||
line: str | |||
}); | |||
return ''; | |||
}); | |||
if (encodedStr) { | |||
list.push({ | |||
line: encodedStr | |||
}); | |||
} | |||
} else { | |||
if (/[\uD800-\uDBFF]/.test(encodedStr)) { | |||
// string containts surrogate pairs, so normalize it to an array of bytes | |||
encodedStrArr = []; | |||
for (i = 0, len = encodedStr.length; i < len; i++) { | |||
chr = encodedStr.charAt(i); | |||
ord = chr.charCodeAt(0); | |||
if (ord >= 0xd800 && ord <= 0xdbff && i < len - 1) { | |||
chr += encodedStr.charAt(i + 1); | |||
encodedStrArr.push(chr); | |||
i++; | |||
} else { | |||
encodedStrArr.push(chr); | |||
} | |||
} | |||
encodedStr = encodedStrArr; | |||
} | |||
// first line includes the charset and language info and needs to be encoded | |||
// even if it does not contain any unicode characters | |||
line = 'utf-8\x27\x27'; | |||
let encoded = true; | |||
startPos = 0; | |||
// process text with unicode or special chars | |||
for (i = 0, len = encodedStr.length; i < len; i++) { | |||
chr = encodedStr[i]; | |||
if (encoded) { | |||
chr = this.safeEncodeURIComponent(chr); | |||
} else { | |||
// try to urlencode current char | |||
chr = chr === ' ' ? chr : this.safeEncodeURIComponent(chr); | |||
// By default it is not required to encode a line, the need | |||
// only appears when the string contains unicode or special chars | |||
// in this case we start processing the line over and encode all chars | |||
if (chr !== encodedStr[i]) { | |||
// Check if it is even possible to add the encoded char to the line | |||
// If not, there is no reason to use this line, just push it to the list | |||
// and start a new line with the char that needs encoding | |||
if ((this.safeEncodeURIComponent(line) + chr).length >= maxLength) { | |||
list.push({ | |||
line, | |||
encoded | |||
}); | |||
line = ''; | |||
startPos = i - 1; | |||
} else { | |||
encoded = true; | |||
i = startPos; | |||
line = ''; | |||
continue; | |||
} | |||
} | |||
} | |||
// if the line is already too long, push it to the list and start a new one | |||
if ((line + chr).length >= maxLength) { | |||
list.push({ | |||
line, | |||
encoded | |||
}); | |||
line = chr = encodedStr[i] === ' ' ? ' ' : this.safeEncodeURIComponent(encodedStr[i]); | |||
if (chr === encodedStr[i]) { | |||
encoded = false; | |||
startPos = i - 1; | |||
} else { | |||
encoded = true; | |||
} | |||
} else { | |||
line += chr; | |||
} | |||
} | |||
if (line) { | |||
list.push({ | |||
line, | |||
encoded | |||
}); | |||
} | |||
} | |||
return list.map((item, i) => ({ | |||
// encoded lines: {name}*{part}* | |||
// unencoded lines: {name}*{part} | |||
// if any line needs to be encoded then the first line (part==0) is always encoded | |||
key: key + '*' + i + (item.encoded ? '*' : ''), | |||
value: item.line | |||
})); | |||
}, | |||
/** | |||
* Parses a header value with key=value arguments into a structured | |||
* object. | |||
* | |||
* parseHeaderValue('content-type: text/plain; CHARSET='UTF-8'') -> | |||
* { | |||
* 'value': 'text/plain', | |||
* 'params': { | |||
* 'charset': 'UTF-8' | |||
* } | |||
* } | |||
* | |||
* @param {String} str Header value | |||
* @return {Object} Header value as a parsed structure | |||
*/ | |||
parseHeaderValue(str) { | |||
let response = { | |||
value: false, | |||
params: {} | |||
}; | |||
let key = false; | |||
let value = ''; | |||
let type = 'value'; | |||
let quote = false; | |||
let escaped = false; | |||
let chr; | |||
for (let i = 0, len = str.length; i < len; i++) { | |||
chr = str.charAt(i); | |||
if (type === 'key') { | |||
if (chr === '=') { | |||
key = value.trim().toLowerCase(); | |||
type = 'value'; | |||
value = ''; | |||
continue; | |||
} | |||
value += chr; | |||
} else { | |||
if (escaped) { | |||
value += chr; | |||
} else if (chr === '\\') { | |||
escaped = true; | |||
continue; | |||
} else if (quote && chr === quote) { | |||
quote = false; | |||
} else if (!quote && chr === '"') { | |||
quote = chr; | |||
} else if (!quote && chr === ';') { | |||
if (key === false) { | |||
response.value = value.trim(); | |||
} else { | |||
response.params[key] = value.trim(); | |||
} | |||
type = 'key'; | |||
value = ''; | |||
} else { | |||
value += chr; | |||
} | |||
escaped = false; | |||
} | |||
} | |||
if (type === 'value') { | |||
if (key === false) { | |||
response.value = value.trim(); | |||
} else { | |||
response.params[key] = value.trim(); | |||
} | |||
} else if (value.trim()) { | |||
response.params[value.trim().toLowerCase()] = ''; | |||
} | |||
// handle parameter value continuations | |||
// https://tools.ietf.org/html/rfc2231#section-3 | |||
// preprocess values | |||
Object.keys(response.params).forEach(key => { | |||
let actualKey, nr, match, value; | |||
if ((match = key.match(/(\*(\d+)|\*(\d+)\*|\*)$/))) { | |||
actualKey = key.substr(0, match.index); | |||
nr = Number(match[2] || match[3]) || 0; | |||
if (!response.params[actualKey] || typeof response.params[actualKey] !== 'object') { | |||
response.params[actualKey] = { | |||
charset: false, | |||
values: [] | |||
}; | |||
} | |||
value = response.params[key]; | |||
if (nr === 0 && match[0].substr(-1) === '*' && (match = value.match(/^([^']*)'[^']*'(.*)$/))) { | |||
response.params[actualKey].charset = match[1] || 'iso-8859-1'; | |||
value = match[2]; | |||
} | |||
response.params[actualKey].values[nr] = value; | |||
// remove the old reference | |||
delete response.params[key]; | |||
} | |||
}); | |||
// concatenate split rfc2231 strings and convert encoded strings to mime encoded words | |||
Object.keys(response.params).forEach(key => { | |||
let value; | |||
if (response.params[key] && Array.isArray(response.params[key].values)) { | |||
value = response.params[key].values.map(val => val || '').join(''); | |||
if (response.params[key].charset) { | |||
// convert "%AB" to "=?charset?Q?=AB?=" | |||
response.params[key] = | |||
'=?' + | |||
response.params[key].charset + | |||
'?Q?' + | |||
value | |||
// fix invalidly encoded chars | |||
.replace(/[=?_\s]/g, s => { | |||
let c = s.charCodeAt(0).toString(16); | |||
if (s === ' ') { | |||
return '_'; | |||
} else { | |||
return '%' + (c.length < 2 ? '0' : '') + c; | |||
} | |||
}) | |||
// change from urlencoding to percent encoding | |||
.replace(/%/g, '=') + | |||
'?='; | |||
} else { | |||
response.params[key] = value; | |||
} | |||
} | |||
}); | |||
return response; | |||
}, | |||
/** | |||
* Returns file extension for a content type string. If no suitable extensions | |||
* are found, 'bin' is used as the default extension | |||
* | |||
* @param {String} mimeType Content type to be checked for | |||
* @return {String} File extension | |||
*/ | |||
detectExtension: mimeType => mimeTypes.detectExtension(mimeType), | |||
/** | |||
* Returns content type for a file extension. If no suitable content types | |||
* are found, 'application/octet-stream' is used as the default content type | |||
* | |||
* @param {String} extension Extension to be checked for | |||
* @return {String} File extension | |||
*/ | |||
detectMimeType: extension => mimeTypes.detectMimeType(extension), | |||
/** | |||
* Folds long lines, useful for folding header lines (afterSpace=false) and | |||
* flowed text (afterSpace=true) | |||
* | |||
* @param {String} str String to be folded | |||
* @param {Number} [lineLength=76] Maximum length of a line | |||
* @param {Boolean} afterSpace If true, leave a space in th end of a line | |||
* @return {String} String with folded lines | |||
*/ | |||
foldLines(str, lineLength, afterSpace) { | |||
str = (str || '').toString(); | |||
lineLength = lineLength || 76; | |||
let pos = 0, | |||
len = str.length, | |||
result = '', | |||
line, | |||
match; | |||
while (pos < len) { | |||
line = str.substr(pos, lineLength); | |||
if (line.length < lineLength) { | |||
result += line; | |||
break; | |||
} | |||
if ((match = line.match(/^[^\n\r]*(\r?\n|\r)/))) { | |||
line = match[0]; | |||
result += line; | |||
pos += line.length; | |||
continue; | |||
} else if ((match = line.match(/(\s+)[^\s]*$/)) && match[0].length - (afterSpace ? (match[1] || '').length : 0) < line.length) { | |||
line = line.substr(0, line.length - (match[0].length - (afterSpace ? (match[1] || '').length : 0))); | |||
} else if ((match = str.substr(pos + line.length).match(/^[^\s]+(\s*)/))) { | |||
line = line + match[0].substr(0, match[0].length - (!afterSpace ? (match[1] || '').length : 0)); | |||
} | |||
result += line; | |||
pos += line.length; | |||
if (pos < len) { | |||
result += '\r\n'; | |||
} | |||
} | |||
return result; | |||
}, | |||
/** | |||
* Splits a mime encoded string. Needed for dividing mime words into smaller chunks | |||
* | |||
* @param {String} str Mime encoded string to be split up | |||
* @param {Number} maxlen Maximum length of characters for one part (minimum 12) | |||
* @return {Array} Split string | |||
*/ | |||
splitMimeEncodedString: (str, maxlen) => { | |||
let curLine, | |||
match, | |||
chr, | |||
done, | |||
lines = []; | |||
// require at least 12 symbols to fit possible 4 octet UTF-8 sequences | |||
maxlen = Math.max(maxlen || 0, 12); | |||
while (str.length) { | |||
curLine = str.substr(0, maxlen); | |||
// move incomplete escaped char back to main | |||
if ((match = curLine.match(/[=][0-9A-F]?$/i))) { | |||
curLine = curLine.substr(0, match.index); | |||
} | |||
done = false; | |||
while (!done) { | |||
done = true; | |||
// check if not middle of a unicode char sequence | |||
if ((match = str.substr(curLine.length).match(/^[=]([0-9A-F]{2})/i))) { | |||
chr = parseInt(match[1], 16); | |||
// invalid sequence, move one char back anc recheck | |||
if (chr < 0xc2 && chr > 0x7f) { | |||
curLine = curLine.substr(0, curLine.length - 3); | |||
done = false; | |||
} | |||
} | |||
} | |||
if (curLine.length) { | |||
lines.push(curLine); | |||
} | |||
str = str.substr(curLine.length); | |||
} | |||
return lines; | |||
}, | |||
encodeURICharComponent: chr => { | |||
let res = ''; | |||
let ord = chr | |||
.charCodeAt(0) | |||
.toString(16) | |||
.toUpperCase(); | |||
if (ord.length % 2) { | |||
ord = '0' + ord; | |||
} | |||
if (ord.length > 2) { | |||
for (let i = 0, len = ord.length / 2; i < len; i++) { | |||
res += '%' + ord.substr(i, 2); | |||
} | |||
} else { | |||
res += '%' + ord; | |||
} | |||
return res; | |||
}, | |||
safeEncodeURIComponent(str) { | |||
str = (str || '').toString(); | |||
try { | |||
// might throw if we try to encode invalid sequences, eg. partial emoji | |||
str = encodeURIComponent(str); | |||
} catch (E) { | |||
// should never run | |||
return str.replace(/[^\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]+/g, ''); | |||
} | |||
// ensure chars that are not handled by encodeURICompent are converted as well | |||
return str.replace(/[\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]/g, chr => this.encodeURICharComponent(chr)); | |||
} | |||
}; |
@ -1,33 +0,0 @@ | |||
'use strict'; | |||
const Transform = require('stream').Transform; | |||
class LastNewline extends Transform { | |||
constructor() { | |||
super(); | |||
this.lastByte = false; | |||
} | |||
_transform(chunk, encoding, done) { | |||
if (chunk.length) { | |||
this.lastByte = chunk[chunk.length - 1]; | |||
} | |||
this.push(chunk); | |||
done(); | |||
} | |||
_flush(done) { | |||
if (this.lastByte === 0x0a) { | |||
return done(); | |||
} | |||
if (this.lastByte === 0x0d) { | |||
this.push(Buffer.from('\n')); | |||
return done(); | |||
} | |||
this.push(Buffer.from('\r\n')); | |||
return done(); | |||
} | |||
} | |||
module.exports = LastNewline; |
@ -1,148 +0,0 @@ | |||
'use strict'; | |||
const Mailer = require('./mailer'); | |||
const shared = require('./shared'); | |||
const SMTPPool = require('./smtp-pool'); | |||
const SMTPTransport = require('./smtp-transport'); | |||
const SendmailTransport = require('./sendmail-transport'); | |||
const StreamTransport = require('./stream-transport'); | |||
const JSONTransport = require('./json-transport'); | |||
const SESTransport = require('./ses-transport'); | |||
const fetch = require('./fetch'); | |||
const packageData = require('../package.json'); | |||
const ETHEREAL_API = (process.env.ETHEREAL_API || 'https://api.nodemailer.com').replace(/\/+$/, ''); | |||
const ETHEREAL_WEB = (process.env.ETHEREAL_WEB || 'https://ethereal.email').replace(/\/+$/, ''); | |||
const ETHEREAL_CACHE = ['true', 'yes', 'y', '1'].includes( | |||
(process.env.ETHEREAL_CACHE || 'yes') | |||
.toString() | |||
.trim() | |||
.toLowerCase() | |||
); | |||
let testAccount = false; | |||
module.exports.createTransport = function(transporter, defaults) { | |||
let urlConfig; | |||
let options; | |||
let mailer; | |||
if ( | |||
// provided transporter is a configuration object, not transporter plugin | |||
(typeof transporter === 'object' && typeof transporter.send !== 'function') || | |||
// provided transporter looks like a connection url | |||
(typeof transporter === 'string' && /^(smtps?|direct):/i.test(transporter)) | |||
) { | |||
if ((urlConfig = typeof transporter === 'string' ? transporter : transporter.url)) { | |||
// parse a configuration URL into configuration options | |||
options = shared.parseConnectionUrl(urlConfig); | |||
} else { | |||
options = transporter; | |||
} | |||
if (options.pool) { | |||
transporter = new SMTPPool(options); | |||
} else if (options.sendmail) { | |||
transporter = new SendmailTransport(options); | |||
} else if (options.streamTransport) { | |||
transporter = new StreamTransport(options); | |||
} else if (options.jsonTransport) { | |||
transporter = new JSONTransport(options); | |||
} else if (options.SES) { | |||
transporter = new SESTransport(options); | |||
} else { | |||
transporter = new SMTPTransport(options); | |||
} | |||
} | |||
mailer = new Mailer(transporter, options, defaults); | |||
return mailer; | |||
}; | |||
module.exports.createTestAccount = function(apiUrl, callback) { | |||
let promise; | |||
if (!callback && typeof apiUrl === 'function') { | |||
callback = apiUrl; | |||
apiUrl = false; | |||
} | |||
if (!callback) { | |||
promise = new Promise((resolve, reject) => { | |||
callback = shared.callbackPromise(resolve, reject); | |||
}); | |||
} | |||
if (ETHEREAL_CACHE && testAccount) { | |||
setImmediate(() => callback(null, testAccount)); | |||
return promise; | |||
} | |||
apiUrl = apiUrl || ETHEREAL_API; | |||
let chunks = []; | |||
let chunklen = 0; | |||
let req = fetch(apiUrl + '/user', { | |||
contentType: 'application/json', | |||
method: 'POST', | |||
body: Buffer.from( | |||
JSON.stringify({ | |||
requestor: packageData.name, | |||
version: packageData.version | |||
}) | |||
) | |||
}); | |||
req.on('readable', () => { | |||
let chunk; | |||
while ((chunk = req.read()) !== null) { | |||
chunks.push(chunk); | |||
chunklen += chunk.length; | |||
} | |||
}); | |||
req.once('error', err => callback(err)); | |||
req.once('end', () => { | |||
let res = Buffer.concat(chunks, chunklen); | |||
let data; | |||
let err; | |||
try { | |||
data = JSON.parse(res.toString()); | |||
} catch (E) { | |||
err = E; | |||
} | |||
if (err) { | |||
return callback(err); | |||
} | |||
if (data.status !== 'success' || data.error) { | |||
return callback(new Error(data.error || 'Request failed')); | |||
} | |||
delete data.status; | |||
testAccount = data; | |||
callback(null, testAccount); | |||
}); | |||
return promise; | |||
}; | |||
module.exports.getTestMessageUrl = function(info) { | |||
if (!info || !info.response) { | |||
return false; | |||
} | |||
let infoProps = new Map(); | |||
info.response.replace(/\[([^\]]+)\]$/, (m, props) => { | |||
props.replace(/\b([A-Z0-9]+)=([^\s]+)/g, (m, key, value) => { | |||
infoProps.set(key, value); | |||
}); | |||
}); | |||
if (infoProps.has('STATUS') && infoProps.has('MSGID')) { | |||
return (testAccount.web || ETHEREAL_WEB) + '/message/' + infoProps.get('MSGID'); | |||
} | |||
return false; | |||
}; |
@ -1,219 +0,0 @@ | |||
'use strict'; | |||
const Transform = require('stream').Transform; | |||
/** | |||
* Encodes a Buffer into a Quoted-Printable encoded string | |||
* | |||
* @param {Buffer} buffer Buffer to convert | |||
* @returns {String} Quoted-Printable encoded string | |||
*/ | |||
function encode(buffer) { | |||
if (typeof buffer === 'string') { | |||
buffer = Buffer.from(buffer, 'utf-8'); | |||
} | |||
// usable characters that do not need encoding | |||
let ranges = [ | |||
// https://tools.ietf.org/html/rfc2045#section-6.7 | |||
[0x09], // <TAB> | |||
[0x0a], // <LF> | |||
[0x0d], // <CR> | |||
[0x20, 0x3c], // <SP>!"#$%&'()*+,-./0123456789:; | |||
[0x3e, 0x7e] // >?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|} | |||
]; | |||
let result = ''; | |||
let ord; | |||
for (let i = 0, len = buffer.length; i < len; i++) { | |||
ord = buffer[i]; | |||
// if the char is in allowed range, then keep as is, unless it is a WS in the end of a line | |||
if (checkRanges(ord, ranges) && !((ord === 0x20 || ord === 0x09) && (i === len - 1 || buffer[i + 1] === 0x0a || buffer[i + 1] === 0x0d))) { | |||
result += String.fromCharCode(ord); | |||
continue; | |||
} | |||
result += '=' + (ord < 0x10 ? '0' : '') + ord.toString(16).toUpperCase(); | |||
} | |||
return result; | |||
} | |||
/** | |||
* Adds soft line breaks to a Quoted-Printable string | |||
* | |||
* @param {String} str Quoted-Printable encoded string that might need line wrapping | |||
* @param {Number} [lineLength=76] Maximum allowed length for a line | |||
* @returns {String} Soft-wrapped Quoted-Printable encoded string | |||
*/ | |||
function wrap(str, lineLength) { | |||
str = (str || '').toString(); | |||
lineLength = lineLength || 76; | |||
if (str.length <= lineLength) { | |||
return str; | |||
} | |||
let pos = 0; | |||
let len = str.length; | |||
let match, code, line; | |||
let lineMargin = Math.floor(lineLength / 3); | |||
let result = ''; | |||
// insert soft linebreaks where needed | |||
while (pos < len) { | |||
line = str.substr(pos, lineLength); | |||
if ((match = line.match(/\r\n/))) { | |||
line = line.substr(0, match.index + match[0].length); | |||
result += line; | |||
pos += line.length; | |||
continue; | |||
} | |||
if (line.substr(-1) === '\n') { | |||
// nothing to change here | |||
result += line; | |||
pos += line.length; | |||
continue; | |||
} else if ((match = line.substr(-lineMargin).match(/\n.*?$/))) { | |||
// truncate to nearest line break | |||
line = line.substr(0, line.length - (match[0].length - 1)); | |||
result += line; | |||
pos += line.length; | |||
continue; | |||
} else if (line.length > lineLength - lineMargin && (match = line.substr(-lineMargin).match(/[ \t.,!?][^ \t.,!?]*$/))) { | |||
// truncate to nearest space | |||
line = line.substr(0, line.length - (match[0].length - 1)); | |||
} else if (line.match(/[=][\da-f]{0,2}$/i)) { | |||
// push incomplete encoding sequences to the next line | |||
if ((match = line.match(/[=][\da-f]{0,1}$/i))) { | |||
line = line.substr(0, line.length - match[0].length); | |||
} | |||
// ensure that utf-8 sequences are not split | |||
while (line.length > 3 && line.length < len - pos && !line.match(/^(?:=[\da-f]{2}){1,4}$/i) && (match = line.match(/[=][\da-f]{2}$/gi))) { | |||
code = parseInt(match[0].substr(1, 2), 16); | |||
if (code < 128) { | |||
break; | |||
} | |||
line = line.substr(0, line.length - 3); | |||
if (code >= 0xc0) { | |||
break; | |||
} | |||
} | |||
} | |||
if (pos + line.length < len && line.substr(-1) !== '\n') { | |||
if (line.length === lineLength && line.match(/[=][\da-f]{2}$/i)) { | |||
line = line.substr(0, line.length - 3); | |||
} else if (line.length === lineLength) { | |||
line = line.substr(0, line.length - 1); | |||
} | |||
pos += line.length; | |||
line += '=\r\n'; | |||
} else { | |||
pos += line.length; | |||
} | |||
result += line; | |||
} | |||
return result; | |||
} | |||
/** | |||
* Helper function to check if a number is inside provided ranges | |||
* | |||
* @param {Number} nr Number to check for | |||
* @param {Array} ranges An Array of allowed values | |||
* @returns {Boolean} True if the value was found inside allowed ranges, false otherwise | |||
*/ | |||
function checkRanges(nr, ranges) { | |||
for (let i = ranges.length - 1; i >= 0; i--) { | |||
if (!ranges[i].length) { | |||
continue; | |||
} | |||
if (ranges[i].length === 1 && nr === ranges[i][0]) { | |||
return true; | |||
} | |||
if (ranges[i].length === 2 && nr >= ranges[i][0] && nr <= ranges[i][1]) { | |||
return true; | |||
} | |||
} | |||
return false; | |||
} | |||
/** | |||
* Creates a transform stream for encoding data to Quoted-Printable encoding | |||
* | |||
* @constructor | |||
* @param {Object} options Stream options | |||
* @param {Number} [options.lineLength=76] Maximum lenght for lines, set to false to disable wrapping | |||
*/ | |||
class Encoder extends Transform { | |||
constructor(options) { | |||
super(); | |||
// init Transform | |||
this.options = options || {}; | |||
if (this.options.lineLength !== false) { | |||
this.options.lineLength = this.options.lineLength || 76; | |||
} | |||
this._curLine = ''; | |||
this.inputBytes = 0; | |||
this.outputBytes = 0; | |||
} | |||
_transform(chunk, encoding, done) { | |||
let qp; | |||
if (encoding !== 'buffer') { | |||
chunk = Buffer.from(chunk, encoding); | |||
} | |||
if (!chunk || !chunk.length) { | |||
return done(); | |||
} | |||
this.inputBytes += chunk.length; | |||
if (this.options.lineLength) { | |||
qp = this._curLine + encode(chunk); | |||
qp = wrap(qp, this.options.lineLength); | |||
qp = qp.replace(/(^|\n)([^\n]*)$/, (match, lineBreak, lastLine) => { | |||
this._curLine = lastLine; | |||
return lineBreak; | |||
}); | |||
if (qp) { | |||
this.outputBytes += qp.length; | |||
this.push(qp); | |||
} | |||
} else { | |||
qp = encode(chunk); | |||
this.outputBytes += qp.length; | |||
this.push(qp, 'ascii'); | |||
} | |||
done(); | |||
} | |||
_flush(done) { | |||
if (this._curLine) { | |||
this.outputBytes += this._curLine.length; | |||
this.push(this._curLine, 'ascii'); | |||
} | |||
done(); | |||
} | |||
} | |||
// expose to the world | |||
module.exports = { | |||
encode, | |||
wrap, | |||
Encoder | |||
}; |
@ -1,208 +0,0 @@ | |||
'use strict'; | |||
const spawn = require('child_process').spawn; | |||
const packageData = require('../../package.json'); | |||
const LeWindows = require('./le-windows'); | |||
const LeUnix = require('./le-unix'); | |||
const shared = require('../shared'); | |||
/** | |||
* Generates a Transport object for Sendmail | |||
* | |||
* Possible options can be the following: | |||
* | |||
* * **path** optional path to sendmail binary | |||
* * **newline** either 'windows' or 'unix' | |||
* * **args** an array of arguments for the sendmail binary | |||
* | |||
* @constructor | |||
* @param {Object} optional config parameter for Sendmail | |||
*/ | |||
class SendmailTransport { | |||
constructor(options) { | |||
options = options || {}; | |||
// use a reference to spawn for mocking purposes | |||
this._spawn = spawn; | |||
this.options = options || {}; | |||
this.name = 'Sendmail'; | |||
this.version = packageData.version; | |||
this.path = 'sendmail'; | |||
this.args = false; | |||
this.winbreak = false; | |||
this.logger = shared.getLogger(this.options, { | |||
component: this.options.component || 'sendmail' | |||
}); | |||
if (options) { | |||
if (typeof options === 'string') { | |||
this.path = options; | |||
} else if (typeof options === 'object') { | |||
if (options.path) { | |||
this.path = options.path; | |||
} | |||
if (Array.isArray(options.args)) { | |||
this.args = options.args; | |||
} | |||
this.winbreak = ['win', 'windows', 'dos', '\r\n'].includes((options.newline || '').toString().toLowerCase()); | |||
} | |||
} | |||
} | |||
/** | |||
* <p>Compiles a mailcomposer message and forwards it to handler that sends it.</p> | |||
* | |||
* @param {Object} emailMessage MailComposer object | |||
* @param {Function} callback Callback function to run when the sending is completed | |||
*/ | |||
send(mail, done) { | |||
// Sendmail strips this header line by itself | |||
mail.message.keepBcc = true; | |||
let envelope = mail.data.envelope || mail.message.getEnvelope(); | |||
let messageId = mail.message.messageId(); | |||
let args; | |||
let sendmail; | |||
let returned; | |||
let transform; | |||
if (this.args) { | |||
// force -i to keep single dots | |||
args = ['-i'].concat(this.args).concat(envelope.to); | |||
} else { | |||
args = ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to); | |||
} | |||
let callback = err => { | |||
if (returned) { | |||
// ignore any additional responses, already done | |||
return; | |||
} | |||
returned = true; | |||
if (typeof done === 'function') { | |||
if (err) { | |||
return done(err); | |||
} else { | |||
return done(null, { | |||
envelope: mail.data.envelope || mail.message.getEnvelope(), | |||
messageId, | |||
response: 'Messages queued for delivery' | |||
}); | |||
} | |||
} | |||
}; | |||
try { | |||
sendmail = this._spawn(this.path, args); | |||
} catch (E) { | |||
this.logger.error( | |||
{ | |||
err: E, | |||
tnx: 'spawn', | |||
messageId | |||
}, | |||
'Error occurred while spawning sendmail. %s', | |||
E.message | |||
); | |||
return callback(E); | |||
} | |||
if (sendmail) { | |||
sendmail.on('error', err => { | |||
this.logger.error( | |||
{ | |||
err, | |||
tnx: 'spawn', | |||
messageId | |||
}, | |||
'Error occurred when sending message %s. %s', | |||
messageId, | |||
err.message | |||
); | |||
callback(err); | |||
}); | |||
sendmail.once('exit', code => { | |||
if (!code) { | |||
return callback(); | |||
} | |||
let err; | |||
if (code === 127) { | |||
err = new Error('Sendmail command not found, process exited with code ' + code); | |||
} else { | |||
err = new Error('Sendmail exited with code ' + code); | |||
} | |||
this.logger.error( | |||
{ | |||
err, | |||
tnx: 'stdin', | |||
messageId | |||
}, | |||
'Error sending message %s to sendmail. %s', | |||
messageId, | |||
err.message | |||
); | |||
callback(err); | |||
}); | |||
sendmail.once('close', callback); | |||
sendmail.stdin.on('error', err => { | |||
this.logger.error( | |||
{ | |||
err, | |||
tnx: 'stdin', | |||
messageId | |||
}, | |||
'Error occurred when piping message %s to sendmail. %s', | |||
messageId, | |||
err.message | |||
); | |||
callback(err); | |||
}); | |||
let recipients = [].concat(envelope.to || []); | |||
if (recipients.length > 3) { | |||
recipients.push('...and ' + recipients.splice(2).length + ' more'); | |||
} | |||
this.logger.info( | |||
{ | |||
tnx: 'send', | |||
messageId | |||
}, | |||
'Sending message %s to <%s>', | |||
messageId, | |||
recipients.join(', ') | |||
); | |||
transform = this.winbreak ? new LeWindows() : new LeUnix(); | |||
let sourceStream = mail.message.createReadStream(); | |||
transform.once('error', err => { | |||
this.logger.error( | |||
{ | |||
err, | |||
tnx: 'stdin', | |||
messageId | |||
}, | |||
'Error occurred when generating message %s. %s', | |||
messageId, | |||
err.message | |||
); | |||
sendmail.kill('SIGINT'); // do not deliver the message | |||
callback(err); | |||
}); | |||
sourceStream.once('error', err => transform.emit('error', err)); | |||
sourceStream.pipe(transform).pipe(sendmail.stdin); | |||
} else { | |||
return callback(new Error('sendmail was not found')); | |||
} | |||
} | |||
} | |||
module.exports = SendmailTransport; |
@ -1,43 +0,0 @@ | |||
'use strict'; | |||
const stream = require('stream'); | |||
const Transform = stream.Transform; | |||
/** | |||
* Ensures that only <LF> is used for linebreaks | |||
* | |||
* @param {Object} options Stream options | |||
*/ | |||
class LeWindows extends Transform { | |||
constructor(options) { | |||
super(options); | |||
// init Transform | |||
this.options = options || {}; | |||
} | |||
/** | |||
* Escapes dots | |||
*/ | |||
_transform(chunk, encoding, done) { | |||
let buf; | |||
let lastPos = 0; | |||
for (let i = 0, len = chunk.length; i < len; i++) { | |||
if (chunk[i] === 0x0d) { | |||
// \n | |||
buf = chunk.slice(lastPos, i); | |||
lastPos = i + 1; | |||
this.push(buf); | |||
} | |||
} | |||
if (lastPos && lastPos < chunk.length) { | |||
buf = chunk.slice(lastPos); | |||
this.push(buf); | |||
} else if (!lastPos) { | |||
this.push(chunk); | |||
} | |||
done(); | |||
} | |||
} | |||
module.exports = LeWindows; |
@ -1,52 +0,0 @@ | |||
'use strict'; | |||
const stream = require('stream'); | |||
const Transform = stream.Transform; | |||
/** | |||
* Ensures that only <CR><LF> sequences are used for linebreaks | |||
* | |||
* @param {Object} options Stream options | |||
*/ | |||
class LeWindows extends Transform { | |||
constructor(options) { | |||
super(options); | |||
// init Transform | |||
this.options = options || {}; | |||
this.lastByte = false; | |||
} | |||
/** | |||
* Escapes dots | |||
*/ | |||
_transform(chunk, encoding, done) { | |||
let buf; | |||
let lastPos = 0; | |||
for (let i = 0, len = chunk.length; i < len; i++) { | |||
if (chunk[i] === 0x0a) { | |||
// \n | |||
if ((i && chunk[i - 1] !== 0x0d) || (!i && this.lastByte !== 0x0d)) { | |||
if (i > lastPos) { | |||
buf = chunk.slice(lastPos, i); | |||
this.push(buf); | |||
} | |||
this.push(Buffer.from('\r\n')); | |||
lastPos = i + 1; | |||
} | |||
} | |||
} | |||
if (lastPos && lastPos < chunk.length) { | |||
buf = chunk.slice(lastPos); | |||
this.push(buf); | |||
} else if (!lastPos) { | |||
this.push(chunk); | |||
} | |||
this.lastByte = chunk[chunk.length - 1]; | |||
done(); | |||
} | |||
} | |||
module.exports = LeWindows; |
@ -1,312 +0,0 @@ | |||
'use strict'; | |||
const EventEmitter = require('events'); | |||
const packageData = require('../../package.json'); | |||
const shared = require('../shared'); | |||
const LeWindows = require('../sendmail-transport/le-windows'); | |||
/** | |||
* Generates a Transport object for AWS SES | |||
* | |||
* Possible options can be the following: | |||
* | |||
* * **sendingRate** optional Number specifying how many messages per second should be delivered to SES | |||
* * **maxConnections** optional Number specifying max number of parallel connections to SES | |||
* | |||
* @constructor | |||
* @param {Object} optional config parameter | |||
*/ | |||
class SESTransport extends EventEmitter { | |||
constructor(options) { | |||
super(); | |||
options = options || {}; | |||
this.options = options || {}; | |||
this.ses = this.options.SES; | |||
this.name = 'SESTransport'; | |||
this.version = packageData.version; | |||
this.logger = shared.getLogger(this.options, { | |||
component: this.options.component || 'ses-transport' | |||
}); | |||
// parallel sending connections | |||
this.maxConnections = Number(this.options.maxConnections) || Infinity; | |||
this.connections = 0; | |||
// max messages per second | |||
this.sendingRate = Number(this.options.sendingRate) || Infinity; | |||
this.sendingRateTTL = null; | |||
this.rateInterval = 1000; | |||
this.rateMessages = []; | |||
this.pending = []; | |||
this.idling = true; | |||
setImmediate(() => { | |||
if (this.idling) { | |||
this.emit('idle'); | |||
} | |||
}); | |||
} | |||
/** | |||
* Schedules a sending of a message | |||
* | |||
* @param {Object} emailMessage MailComposer object | |||
* @param {Function} callback Callback function to run when the sending is completed | |||
*/ | |||
send(mail, callback) { | |||
if (this.connections >= this.maxConnections) { | |||
this.idling = false; | |||
return this.pending.push({ | |||
mail, | |||
callback | |||
}); | |||
} | |||
if (!this._checkSendingRate()) { | |||
this.idling = false; | |||
return this.pending.push({ | |||
mail, | |||
callback | |||
}); | |||
} | |||
this._send(mail, (...args) => { | |||
setImmediate(() => callback(...args)); | |||
this._sent(); | |||
}); | |||
} | |||
_checkRatedQueue() { | |||
if (this.connections >= this.maxConnections || !this._checkSendingRate()) { | |||
return; | |||
} | |||
if (!this.pending.length) { | |||
if (!this.idling) { | |||
this.idling = true; | |||
this.emit('idle'); | |||
} | |||
return; | |||
} | |||
let next = this.pending.shift(); | |||
this._send(next.mail, (...args) => { | |||
setImmediate(() => next.callback(...args)); | |||
this._sent(); | |||
}); | |||
} | |||
_checkSendingRate() { | |||
clearTimeout(this.sendingRateTTL); | |||
let now = Date.now(); | |||
let oldest = false; | |||
// delete older messages | |||
for (let i = this.rateMessages.length - 1; i >= 0; i--) { | |||
if (this.rateMessages[i].ts >= now - this.rateInterval && (!oldest || this.rateMessages[i].ts < oldest)) { | |||
oldest = this.rateMessages[i].ts; | |||
} | |||
if (this.rateMessages[i].ts < now - this.rateInterval && !this.rateMessages[i].pending) { | |||
this.rateMessages.splice(i, 1); | |||
} | |||
} | |||
if (this.rateMessages.length < this.sendingRate) { | |||
return true; | |||
} | |||
let delay = Math.max(oldest + 1001, now + 20); | |||
this.sendingRateTTL = setTimeout(() => this._checkRatedQueue(), now - delay); | |||
try { | |||
this.sendingRateTTL.unref(); | |||
} catch (E) { | |||
// Ignore. Happens on envs with non-node timer implementation | |||
} | |||
return false; | |||
} | |||
_sent() { | |||
this.connections--; | |||
this._checkRatedQueue(); | |||
} | |||
/** | |||
* Returns true if there are free slots in the queue | |||
*/ | |||
isIdle() { | |||
return this.idling; | |||
} | |||
/** | |||
* Compiles a mailcomposer message and forwards it to SES | |||
* | |||
* @param {Object} emailMessage MailComposer object | |||
* @param {Function} callback Callback function to run when the sending is completed | |||
*/ | |||
_send(mail, callback) { | |||
let statObject = { | |||
ts: Date.now(), | |||
pending: true | |||
}; | |||
this.connections++; | |||
this.rateMessages.push(statObject); | |||
let envelope = mail.data.envelope || mail.message.getEnvelope(); | |||
let messageId = mail.message.messageId(); | |||
let recipients = [].concat(envelope.to || []); | |||
if (recipients.length > 3) { | |||
recipients.push('...and ' + recipients.splice(2).length + ' more'); | |||
} | |||
this.logger.info( | |||
{ | |||
tnx: 'send', | |||
messageId | |||
}, | |||
'Sending message %s to <%s>', | |||
messageId, | |||
recipients.join(', ') | |||
); | |||
let getRawMessage = next => { | |||
// do not use Message-ID and Date in DKIM signature | |||
if (!mail.data._dkim) { | |||
mail.data._dkim = {}; | |||
} | |||
if (mail.data._dkim.skipFields && typeof mail.data._dkim.skipFields === 'string') { | |||
mail.data._dkim.skipFields += ':date:message-id'; | |||
} else { | |||
mail.data._dkim.skipFields = 'date:message-id'; | |||
} | |||
let sourceStream = mail.message.createReadStream(); | |||
let stream = sourceStream.pipe(new LeWindows()); | |||
let chunks = []; | |||
let chunklen = 0; | |||
stream.on('readable', () => { | |||
let chunk; | |||
while ((chunk = stream.read()) !== null) { | |||
chunks.push(chunk); | |||
chunklen += chunk.length; | |||
} | |||
}); | |||
sourceStream.once('error', err => stream.emit('error', err)); | |||
stream.once('error', err => { | |||
next(err); | |||
}); | |||
stream.once('end', () => next(null, Buffer.concat(chunks, chunklen))); | |||
}; | |||
setImmediate(() => | |||
getRawMessage((err, raw) => { | |||
if (err) { | |||
this.logger.error( | |||
{ | |||
err, | |||
tnx: 'send', | |||
messageId | |||
}, | |||
'Failed creating message for %s. %s', | |||
messageId, | |||
err.message | |||
); | |||
statObject.pending = false; | |||
return callback(err); | |||
} | |||
let sesMessage = { | |||
RawMessage: { | |||
// required | |||
Data: raw // required | |||
}, | |||
Source: envelope.from, | |||
Destinations: envelope.to | |||
}; | |||
Object.keys(mail.data.ses || {}).forEach(key => { | |||
sesMessage[key] = mail.data.ses[key]; | |||
}); | |||
this.ses.sendRawEmail(sesMessage, (err, data) => { | |||
if (err) { | |||
this.logger.error( | |||
{ | |||
err, | |||
tnx: 'send' | |||
}, | |||
'Send error for %s: %s', | |||
messageId, | |||
err.message | |||
); | |||
statObject.pending = false; | |||
return callback(err); | |||
} | |||
let region = (this.ses.config && this.ses.config.region) || 'us-east-1'; | |||
if (region === 'us-east-1') { | |||
region = 'email'; | |||
} | |||
statObject.pending = false; | |||
callback(null, { | |||
envelope: { | |||
from: envelope.from, | |||
to: envelope.to | |||
}, | |||
messageId: '<' + data.MessageId + (!/@/.test(data.MessageId) ? '@' + region + '.amazonses.com' : '') + '>', | |||
response: data.MessageId, | |||
raw | |||
}); | |||
}); | |||
}) | |||
); | |||
} | |||
/** | |||
* Verifies SES configuration | |||
* | |||
* @param {Function} callback Callback function | |||
*/ | |||
verify(callback) { | |||
let promise; | |||
if (!callback) { | |||
promise = new Promise((resolve, reject) => { | |||
callback = shared.callbackPromise(resolve, reject); | |||
}); | |||
} | |||
this.ses.sendRawEmail( | |||
{ | |||
RawMessage: { | |||
// required | |||
Data: 'From: invalid@invalid\r\nTo: invalid@invalid\r\n Subject: Invalid\r\n\r\nInvalid' | |||
}, | |||
Source: 'invalid@invalid', | |||
Destinations: ['invalid@invalid'] | |||
}, | |||
err => { | |||
if (err && err.code !== 'InvalidParameterValue') { | |||
return callback(err); | |||
} | |||
return callback(null, true); | |||
} | |||
); | |||
return promise; | |||
} | |||
} | |||
module.exports = SESTransport; |
@ -1,510 +0,0 @@ | |||
/* eslint no-console: 0 */ | |||
'use strict'; | |||
const urllib = require('url'); | |||
const util = require('util'); | |||
const fs = require('fs'); | |||
const fetch = require('../fetch'); | |||
const dns = require('dns'); | |||
const net = require('net'); | |||
const DNS_TTL = 5 * 60 * 1000; | |||
const resolver = (family, hostname, callback) => { | |||
dns['resolve' + family](hostname, (err, addresses) => { | |||
if (err) { | |||
switch (err.code) { | |||
case dns.NODATA: | |||
case dns.NOTFOUND: | |||
case dns.NOTIMP: | |||
case dns.SERVFAIL: | |||
case dns.CONNREFUSED: | |||
case 'EAI_AGAIN': | |||
return callback(null, []); | |||
} | |||
return callback(err); | |||
} | |||
return callback(null, Array.isArray(addresses) ? addresses : [].concat(addresses || [])); | |||
}); | |||
}; | |||
const dnsCache = (module.exports.dnsCache = new Map()); | |||
module.exports.resolveHostname = (options, callback) => { | |||
options = options || {}; | |||
if (!options.host || net.isIP(options.host)) { | |||
// nothing to do here | |||
let value = { | |||
host: options.host, | |||
servername: options.servername || false | |||
}; | |||
return callback(null, value); | |||
} | |||
let cached; | |||
if (dnsCache.has(options.host)) { | |||
cached = dnsCache.get(options.host); | |||
if (!cached.expires || cached.expires >= Date.now()) { | |||
return callback(null, { | |||
host: cached.value.host, | |||
servername: cached.value.servername, | |||
_cached: true | |||
}); | |||
} | |||
} | |||
resolver(4, options.host, (err, addresses) => { | |||
if (err) { | |||
if (cached) { | |||
// ignore error, use expired value | |||
return callback(null, cached.value); | |||
} | |||
return callback(err); | |||
} | |||
if (addresses && addresses.length) { | |||
let value = { | |||
host: addresses[0] || options.host, | |||
servername: options.servername || options.host | |||
}; | |||
dnsCache.set(options.host, { | |||
value, | |||
expires: Date.now() + DNS_TTL | |||
}); | |||
return callback(null, value); | |||
} | |||
resolver(6, options.host, (err, addresses) => { | |||
if (err) { | |||
if (cached) { | |||
// ignore error, use expired value | |||
return callback(null, cached.value); | |||
} | |||
return callback(err); | |||
} | |||
if (addresses && addresses.length) { | |||
let value = { | |||
host: addresses[0] || options.host, | |||
servername: options.servername || options.host | |||
}; | |||
dnsCache.set(options.host, { | |||
value, | |||
expires: Date.now() + DNS_TTL | |||
}); | |||
return callback(null, value); | |||
} | |||
try { | |||
dns.lookup(options.host, {}, (err, address) => { | |||
if (err) { | |||
if (cached) { | |||
// ignore error, use expired value | |||
return callback(null, cached.value); | |||
} | |||
return callback(err); | |||
} | |||
if (!address && cached) { | |||
// nothing was found, fallback to cached value | |||
return callback(null, cached.value); | |||
} | |||
let value = { | |||
host: address || options.host, | |||
servername: options.servername || options.host | |||
}; | |||
dnsCache.set(options.host, { | |||
value, | |||
expires: Date.now() + DNS_TTL | |||
}); | |||
return callback(null, value); | |||
}); | |||
} catch (err) { | |||
if (cached) { | |||
// ignore error, use expired value | |||
return callback(null, cached.value); | |||
} | |||
return callback(err); | |||
} | |||
}); | |||
}); | |||
}; | |||
/** | |||
* Parses connection url to a structured configuration object | |||
* | |||
* @param {String} str Connection url | |||
* @return {Object} Configuration object | |||
*/ | |||
module.exports.parseConnectionUrl = str => { | |||
str = str || ''; | |||
let options = {}; | |||
[urllib.parse(str, true)].forEach(url => { | |||
let auth; | |||
switch (url.protocol) { | |||
case 'smtp:': | |||
options.secure = false; | |||
break; | |||
case 'smtps:': | |||
options.secure = true; | |||
break; | |||
case 'direct:': | |||
options.direct = true; | |||
break; | |||
} | |||
if (!isNaN(url.port) && Number(url.port)) { | |||
options.port = Number(url.port); | |||
} | |||
if (url.hostname) { | |||
options.host = url.hostname; | |||
} | |||
if (url.auth) { | |||
auth = url.auth.split(':'); | |||
if (!options.auth) { | |||
options.auth = {}; | |||
} | |||
options.auth.user = auth.shift(); | |||
options.auth.pass = auth.join(':'); | |||
} | |||
Object.keys(url.query || {}).forEach(key => { | |||
let obj = options; | |||
let lKey = key; | |||
let value = url.query[key]; | |||
if (!isNaN(value)) { | |||
value = Number(value); | |||
} | |||
switch (value) { | |||
case 'true': | |||
value = true; | |||
break; | |||
case 'false': | |||
value = false; | |||
break; | |||
} | |||
// tls is nested object | |||
if (key.indexOf('tls.') === 0) { | |||
lKey = key.substr(4); | |||
if (!options.tls) { | |||
options.tls = {}; | |||
} | |||
obj = options.tls; | |||
} else if (key.indexOf('.') >= 0) { | |||
// ignore nested properties besides tls | |||
return; | |||
} | |||
if (!(lKey in obj)) { | |||
obj[lKey] = value; | |||
} | |||
}); | |||
}); | |||
return options; | |||
}; | |||
module.exports._logFunc = (logger, level, defaults, data, message, ...args) => { | |||
let entry = {}; | |||
Object.keys(defaults || {}).forEach(key => { | |||
if (key !== 'level') { | |||
entry[key] = defaults[key]; | |||
} | |||
}); | |||
Object.keys(data || {}).forEach(key => { | |||
if (key !== 'level') { | |||
entry[key] = data[key]; | |||
} | |||
}); | |||
logger[level](entry, message, ...args); | |||
}; | |||
/** | |||
* Returns a bunyan-compatible logger interface. Uses either provided logger or | |||
* creates a default console logger | |||
* | |||
* @param {Object} [options] Options object that might include 'logger' value | |||
* @return {Object} bunyan compatible logger | |||
*/ | |||
module.exports.getLogger = (options, defaults) => { | |||
options = options || {}; | |||
let response = {}; | |||
let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal']; | |||
if (!options.logger) { | |||
// use vanity logger | |||
levels.forEach(level => { | |||
response[level] = () => false; | |||
}); | |||
return response; | |||
} | |||
let logger = options.logger; | |||
if (options.logger === true) { | |||
// create console logger | |||
logger = createDefaultLogger(levels); | |||
} | |||
levels.forEach(level => { | |||
response[level] = (data, message, ...args) => { | |||
module.exports._logFunc(logger, level, defaults, data, message, ...args); | |||
}; | |||
}); | |||
return response; | |||
}; | |||
/** | |||
* Wrapper for creating a callback that either resolves or rejects a promise | |||
* based on input | |||
* | |||
* @param {Function} resolve Function to run if callback is called | |||
* @param {Function} reject Function to run if callback ends with an error | |||
*/ | |||
module.exports.callbackPromise = (resolve, reject) => | |||
function() { | |||
let args = Array.from(arguments); | |||
let err = args.shift(); | |||
if (err) { | |||
reject(err); | |||
} else { | |||
resolve(...args); | |||
} | |||
}; | |||
/** | |||
* Resolves a String or a Buffer value for content value. Useful if the value | |||
* is a Stream or a file or an URL. If the value is a Stream, overwrites | |||
* the stream object with the resolved value (you can't stream a value twice). | |||
* | |||
* This is useful when you want to create a plugin that needs a content value, | |||
* for example the `html` or `text` value as a String or a Buffer but not as | |||
* a file path or an URL. | |||
* | |||
* @param {Object} data An object or an Array you want to resolve an element for | |||
* @param {String|Number} key Property name or an Array index | |||
* @param {Function} callback Callback function with (err, value) | |||
*/ | |||
module.exports.resolveContent = (data, key, callback) => { | |||
let promise; | |||
if (!callback) { | |||
promise = new Promise((resolve, reject) => { | |||
callback = module.exports.callbackPromise(resolve, reject); | |||
}); | |||
} | |||
let content = (data && data[key] && data[key].content) || data[key]; | |||
let contentStream; | |||
let encoding = ((typeof data[key] === 'object' && data[key].encoding) || 'utf8') | |||
.toString() | |||
.toLowerCase() | |||
.replace(/[-_\s]/g, ''); | |||
if (!content) { | |||
return callback(null, content); | |||
} | |||
if (typeof content === 'object') { | |||
if (typeof content.pipe === 'function') { | |||
return resolveStream(content, (err, value) => { | |||
if (err) { | |||
return callback(err); | |||
} | |||
// we can't stream twice the same content, so we need | |||
// to replace the stream object with the streaming result | |||
data[key] = value; | |||
callback(null, value); | |||
}); | |||
} else if (/^https?:\/\//i.test(content.path || content.href)) { | |||
contentStream = fetch(content.path || content.href); | |||
return resolveStream(contentStream, callback); | |||
} else if (/^data:/i.test(content.path || content.href)) { | |||
let parts = (content.path || content.href).match(/^data:((?:[^;]*;)*(?:[^,]*)),(.*)$/i); | |||
if (!parts) { | |||
return callback(null, Buffer.from(0)); | |||
} | |||
return callback(null, /\bbase64$/i.test(parts[1]) ? Buffer.from(parts[2], 'base64') : Buffer.from(decodeURIComponent(parts[2]))); | |||
} else if (content.path) { | |||
return resolveStream(fs.createReadStream(content.path), callback); | |||
} | |||
} | |||
if (typeof data[key].content === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) { | |||
content = Buffer.from(data[key].content, encoding); | |||
} | |||
// default action, return as is | |||
setImmediate(() => callback(null, content)); | |||
return promise; | |||
}; | |||
/** | |||
* Copies properties from source objects to target objects | |||
*/ | |||
module.exports.assign = function(/* target, ... sources */) { | |||
let args = Array.from(arguments); | |||
let target = args.shift() || {}; | |||
args.forEach(source => { | |||
Object.keys(source || {}).forEach(key => { | |||
if (['tls', 'auth'].includes(key) && source[key] && typeof source[key] === 'object') { | |||
// tls and auth are special keys that need to be enumerated separately | |||
// other objects are passed as is | |||
if (!target[key]) { | |||
// ensure that target has this key | |||
target[key] = {}; | |||
} | |||
Object.keys(source[key]).forEach(subKey => { | |||
target[key][subKey] = source[key][subKey]; | |||
}); | |||
} else { | |||
target[key] = source[key]; | |||
} | |||
}); | |||
}); | |||
return target; | |||
}; | |||
module.exports.encodeXText = str => { | |||
// ! 0x21 | |||
// + 0x2B | |||
// = 0x3D | |||
// ~ 0x7E | |||
if (!/[^\x21-\x2A\x2C-\x3C\x3E-\x7E]/.test(str)) { | |||
return str; | |||
} | |||
let buf = Buffer.from(str); | |||
let result = ''; | |||
for (let i = 0, len = buf.length; i < len; i++) { | |||
let c = buf[i]; | |||
if (c < 0x21 || c > 0x7e || c === 0x2b || c === 0x3d) { | |||
result += '+' + (c < 0x10 ? '0' : '') + c.toString(16).toUpperCase(); | |||
} else { | |||
result += String.fromCharCode(c); | |||
} | |||
} | |||
return result; | |||
}; | |||
/** | |||
* Streams a stream value into a Buffer | |||
* | |||
* @param {Object} stream Readable stream | |||
* @param {Function} callback Callback function with (err, value) | |||
*/ | |||
function resolveStream(stream, callback) { | |||
let responded = false; | |||
let chunks = []; | |||
let chunklen = 0; | |||
stream.on('error', err => { | |||
if (responded) { | |||
return; | |||
} | |||
responded = true; | |||
callback(err); | |||
}); | |||
stream.on('readable', () => { | |||
let chunk; | |||
while ((chunk = stream.read()) !== null) { | |||
chunks.push(chunk); | |||
chunklen += chunk.length; | |||
} | |||
}); | |||
stream.on('end', () => { | |||
if (responded) { | |||
return; | |||
} | |||
responded = true; | |||
let value; | |||
try { | |||
value = Buffer.concat(chunks, chunklen); | |||
} catch (E) { | |||
return callback(E); | |||
} | |||
callback(null, value); | |||
}); | |||
} | |||
/** | |||
* Generates a bunyan-like logger that prints to console | |||
* | |||
* @returns {Object} Bunyan logger instance | |||
*/ | |||
function createDefaultLogger(levels) { | |||
let levelMaxLen = 0; | |||
let levelNames = new Map(); | |||
levels.forEach(level => { | |||
if (level.length > levelMaxLen) { | |||
levelMaxLen = level.length; | |||
} | |||
}); | |||
levels.forEach(level => { | |||
let levelName = level.toUpperCase(); | |||
if (levelName.length < levelMaxLen) { | |||
levelName += ' '.repeat(levelMaxLen - levelName.length); | |||
} | |||
levelNames.set(level, levelName); | |||
}); | |||
let print = (level, entry, message, ...args) => { | |||
let prefix = ''; | |||
if (entry) { | |||
if (entry.tnx === 'server') { | |||
prefix = 'S: '; | |||
} else if (entry.tnx === 'client') { | |||
prefix = 'C: '; | |||
} | |||
if (entry.sid) { | |||
prefix = '[' + entry.sid + '] ' + prefix; | |||
} | |||
if (entry.cid) { | |||
prefix = '[#' + entry.cid + '] ' + prefix; | |||
} | |||
} | |||
message = util.format(message, ...args); | |||
message.split(/\r?\n/).forEach(line => { | |||
console.log( | |||
'[%s] %s %s', | |||
new Date() | |||
.toISOString() | |||
.substr(0, 19) | |||
.replace(/T/, ' '), | |||
levelNames.get(level), | |||
prefix + line | |||
); | |||
}); | |||
}; | |||
let logger = {}; | |||
levels.forEach(level => { | |||
logger[level] = print.bind(null, level); | |||
}); | |||
return logger; | |||
} |
@ -1,108 +0,0 @@ | |||
'use strict'; | |||
const stream = require('stream'); | |||
const Transform = stream.Transform; | |||
/** | |||
* Escapes dots in the beginning of lines. Ends the stream with <CR><LF>.<CR><LF> | |||
* Also makes sure that only <CR><LF> sequences are used for linebreaks | |||
* | |||
* @param {Object} options Stream options | |||
*/ | |||
class DataStream extends Transform { | |||
constructor(options) { | |||
super(options); | |||
// init Transform | |||
this.options = options || {}; | |||
this._curLine = ''; | |||
this.inByteCount = 0; | |||
this.outByteCount = 0; | |||
this.lastByte = false; | |||
} | |||
/** | |||
* Escapes dots | |||
*/ | |||
_transform(chunk, encoding, done) { | |||
let chunks = []; | |||
let chunklen = 0; | |||
let i, | |||
len, | |||
lastPos = 0; | |||
let buf; | |||
if (!chunk || !chunk.length) { | |||
return done(); | |||
} | |||
if (typeof chunk === 'string') { | |||
chunk = Buffer.from(chunk); | |||
} | |||
this.inByteCount += chunk.length; | |||
for (i = 0, len = chunk.length; i < len; i++) { | |||
if (chunk[i] === 0x2e) { | |||
// . | |||
if ((i && chunk[i - 1] === 0x0a) || (!i && (!this.lastByte || this.lastByte === 0x0a))) { | |||
buf = chunk.slice(lastPos, i + 1); | |||
chunks.push(buf); | |||
chunks.push(Buffer.from('.')); | |||
chunklen += buf.length + 1; | |||
lastPos = i + 1; | |||
} | |||
} else if (chunk[i] === 0x0a) { | |||
// . | |||
if ((i && chunk[i - 1] !== 0x0d) || (!i && this.lastByte !== 0x0d)) { | |||
if (i > lastPos) { | |||
buf = chunk.slice(lastPos, i); | |||
chunks.push(buf); | |||
chunklen += buf.length + 2; | |||
} else { | |||
chunklen += 2; | |||
} | |||
chunks.push(Buffer.from('\r\n')); | |||
lastPos = i + 1; | |||
} | |||
} | |||
} | |||
if (chunklen) { | |||
// add last piece | |||
if (lastPos < chunk.length) { | |||
buf = chunk.slice(lastPos); | |||
chunks.push(buf); | |||
chunklen += buf.length; | |||
} | |||
this.outByteCount += chunklen; | |||
this.push(Buffer.concat(chunks, chunklen)); | |||
} else { | |||
this.outByteCount += chunk.length; | |||
this.push(chunk); | |||
} | |||
this.lastByte = chunk[chunk.length - 1]; | |||
done(); | |||
} | |||
/** | |||
* Finalizes the stream with a dot on a single line | |||
*/ | |||
_flush(done) { | |||
let buf; | |||
if (this.lastByte === 0x0a) { | |||
buf = Buffer.from('.\r\n'); | |||
} else if (this.lastByte === 0x0d) { | |||
buf = Buffer.from('\n.\r\n'); | |||
} else { | |||
buf = Buffer.from('\r\n.\r\n'); | |||
} | |||
this.outByteCount += buf.length; | |||
this.push(buf); | |||
done(); | |||
} | |||
} | |||
module.exports = DataStream; |
@ -1,131 +0,0 @@ | |||
'use strict'; | |||
/** | |||
* Minimal HTTP/S proxy client | |||
*/ | |||
const net = require('net'); | |||
const tls = require('tls'); | |||
const urllib = require('url'); | |||
/** | |||
* Establishes proxied connection to destinationPort | |||
* | |||
* httpProxyClient("http://localhost:3128/", 80, "google.com", function(err, socket){ | |||
* socket.write("GET / HTTP/1.0\r\n\r\n"); | |||
* }); | |||
* | |||
* @param {String} proxyUrl proxy configuration, etg "http://proxy.host:3128/" | |||
* @param {Number} destinationPort Port to open in destination host | |||
* @param {String} destinationHost Destination hostname | |||
* @param {Function} callback Callback to run with the rocket object once connection is established | |||
*/ | |||
function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) { | |||
let proxy = urllib.parse(proxyUrl); | |||
// create a socket connection to the proxy server | |||
let options; | |||
let connect; | |||
let socket; | |||
options = { | |||
host: proxy.hostname, | |||
port: Number(proxy.port) ? Number(proxy.port) : proxy.protocol === 'https:' ? 443 : 80 | |||
}; | |||
if (proxy.protocol === 'https:') { | |||
// we can use untrusted proxies as long as we verify actual SMTP certificates | |||
options.rejectUnauthorized = false; | |||
connect = tls.connect.bind(tls); | |||
} else { | |||
connect = net.connect.bind(net); | |||
} | |||
// Error harness for initial connection. Once connection is established, the responsibility | |||
// to handle errors is passed to whoever uses this socket | |||
let finished = false; | |||
let tempSocketErr = function(err) { | |||
if (finished) { | |||
return; | |||
} | |||
finished = true; | |||
try { | |||
socket.destroy(); | |||
} catch (E) { | |||
// ignore | |||
} | |||
callback(err); | |||
}; | |||
socket = connect(options, () => { | |||
if (finished) { | |||
return; | |||
} | |||
let reqHeaders = { | |||
Host: destinationHost + ':' + destinationPort, | |||
Connection: 'close' | |||
}; | |||
if (proxy.auth) { | |||
reqHeaders['Proxy-Authorization'] = 'Basic ' + Buffer.from(proxy.auth).toString('base64'); | |||
} | |||
socket.write( | |||
// HTTP method | |||
'CONNECT ' + | |||
destinationHost + | |||
':' + | |||
destinationPort + | |||
' HTTP/1.1\r\n' + | |||
// HTTP request headers | |||
Object.keys(reqHeaders) | |||
.map(key => key + ': ' + reqHeaders[key]) | |||
.join('\r\n') + | |||
// End request | |||
'\r\n\r\n' | |||
); | |||
let headers = ''; | |||
let onSocketData = chunk => { | |||
let match; | |||
let remainder; | |||
if (finished) { | |||
return; | |||
} | |||
headers += chunk.toString('binary'); | |||
if ((match = headers.match(/\r\n\r\n/))) { | |||
socket.removeListener('data', onSocketData); | |||
remainder = headers.substr(match.index + match[0].length); | |||
headers = headers.substr(0, match.index); | |||
if (remainder) { | |||
socket.unshift(Buffer.from(remainder, 'binary')); | |||
} | |||
// proxy connection is now established | |||
finished = true; | |||
// check response code | |||
match = headers.match(/^HTTP\/\d+\.\d+ (\d+)/i); | |||
if (!match || (match[1] || '').charAt(0) !== '2') { | |||
try { | |||
socket.destroy(); | |||
} catch (E) { | |||
// ignore | |||
} | |||
return callback(new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || ''))); | |||
} | |||
socket.removeListener('error', tempSocketErr); | |||
return callback(null, socket); | |||
} | |||
}; | |||
socket.on('data', onSocketData); | |||
}); | |||
socket.once('error', tempSocketErr); | |||
} | |||
module.exports = httpProxyClient; |
@ -1,641 +0,0 @@ | |||
'use strict'; | |||
const EventEmitter = require('events'); | |||
const PoolResource = require('./pool-resource'); | |||
const SMTPConnection = require('../smtp-connection'); | |||
const wellKnown = require('../well-known'); | |||
const shared = require('../shared'); | |||
const packageData = require('../../package.json'); | |||
/** | |||
* Creates a SMTP pool transport object for Nodemailer | |||
* | |||
* @constructor | |||
* @param {Object} options SMTP Connection options | |||
*/ | |||
class SMTPPool extends EventEmitter { | |||
constructor(options) { | |||
super(); | |||
options = options || {}; | |||
if (typeof options === 'string') { | |||
options = { | |||
url: options | |||
}; | |||
} | |||
let urlData; | |||
let service = options.service; | |||
if (typeof options.getSocket === 'function') { | |||
this.getSocket = options.getSocket; | |||
} | |||
if (options.url) { | |||
urlData = shared.parseConnectionUrl(options.url); | |||
service = service || urlData.service; | |||
} | |||
this.options = shared.assign( | |||
false, // create new object | |||
options, // regular options | |||
urlData, // url options | |||
service && wellKnown(service) // wellknown options | |||
); | |||
this.options.maxConnections = this.options.maxConnections || 5; | |||
this.options.maxMessages = this.options.maxMessages || 100; | |||
this.logger = shared.getLogger(this.options, { | |||
component: this.options.component || 'smtp-pool' | |||
}); | |||
// temporary object | |||
let connection = new SMTPConnection(this.options); | |||
this.name = 'SMTP (pool)'; | |||
this.version = packageData.version + '[client:' + connection.version + ']'; | |||
this._rateLimit = { | |||
counter: 0, | |||
timeout: null, | |||
waiting: [], | |||
checkpoint: false, | |||
delta: Number(this.options.rateDelta) || 1000, | |||
limit: Number(this.options.rateLimit) || 0 | |||
}; | |||
this._closed = false; | |||
this._queue = []; | |||
this._connections = []; | |||
this._connectionCounter = 0; | |||
this.idling = true; | |||
setImmediate(() => { | |||
if (this.idling) { | |||
this.emit('idle'); | |||
} | |||
}); | |||
} | |||
/** | |||
* Placeholder function for creating proxy sockets. This method immediatelly returns | |||
* without a socket | |||
* | |||
* @param {Object} options Connection options | |||
* @param {Function} callback Callback function to run with the socket keys | |||
*/ | |||
getSocket(options, callback) { | |||
// return immediatelly | |||
return setImmediate(() => callback(null, false)); | |||
} | |||
/** | |||
* Queues an e-mail to be sent using the selected settings | |||
* | |||
* @param {Object} mail Mail object | |||
* @param {Function} callback Callback function | |||
*/ | |||
send(mail, callback) { | |||
if (this._closed) { | |||
return false; | |||
} | |||
this._queue.push({ | |||
mail, | |||
requeueAttempts: 0, | |||
callback | |||
}); | |||
if (this.idling && this._queue.length >= this.options.maxConnections) { | |||
this.idling = false; | |||
} | |||
setImmediate(() => this._processMessages()); | |||
return true; | |||
} | |||
/** | |||
* Closes all connections in the pool. If there is a message being sent, the connection | |||
* is closed later | |||
*/ | |||
close() { | |||
let connection; | |||
let len = this._connections.length; | |||
this._closed = true; | |||
// clear rate limit timer if it exists | |||
clearTimeout(this._rateLimit.timeout); | |||
if (!len && !this._queue.length) { | |||
return; | |||
} | |||
// remove all available connections | |||
for (let i = len - 1; i >= 0; i--) { | |||
if (this._connections[i] && this._connections[i].available) { | |||
connection = this._connections[i]; | |||
connection.close(); | |||
this.logger.info( | |||
{ | |||
tnx: 'connection', | |||
cid: connection.id, | |||
action: 'removed' | |||
}, | |||
'Connection #%s removed', | |||
connection.id | |||
); | |||
} | |||
} | |||
if (len && !this._connections.length) { | |||
this.logger.debug( | |||
{ | |||
tnx: 'connection' | |||
}, | |||
'All connections removed' | |||
); | |||
} | |||
if (!this._queue.length) { | |||
return; | |||
} | |||
// make sure that entire queue would be cleaned | |||
let invokeCallbacks = () => { | |||
if (!this._queue.length) { | |||
this.logger.debug( | |||
{ | |||
tnx: 'connection' | |||
}, | |||
'Pending queue entries cleared' | |||
); | |||
return; | |||
} | |||
let entry = this._queue.shift(); | |||
if (entry && typeof entry.callback === 'function') { | |||
try { | |||
entry.callback(new Error('Connection pool was closed')); | |||
} catch (E) { | |||
this.logger.error( | |||
{ | |||
err: E, | |||
tnx: 'callback', | |||
cid: connection.id | |||
}, | |||
'Callback error for #%s: %s', | |||
connection.id, | |||
E.message | |||
); | |||
} | |||
} | |||
setImmediate(invokeCallbacks); | |||
}; | |||
setImmediate(invokeCallbacks); | |||
} | |||
/** | |||
* Check the queue and available connections. If there is a message to be sent and there is | |||
* an available connection, then use this connection to send the mail | |||
*/ | |||
_processMessages() { | |||
let connection; | |||
let i, len; | |||
// do nothing if already closed | |||
if (this._closed) { | |||
return; | |||
} | |||
// do nothing if queue is empty | |||
if (!this._queue.length) { | |||
if (!this.idling) { | |||
// no pending jobs | |||
this.idling = true; | |||
this.emit('idle'); | |||
} | |||
return; | |||
} | |||
// find first available connection | |||
for (i = 0, len = this._connections.length; i < len; i++) { | |||
if (this._connections[i].available) { | |||
connection = this._connections[i]; | |||
break; | |||
} | |||
} | |||
if (!connection && this._connections.length < this.options.maxConnections) { | |||
connection = this._createConnection(); | |||
} | |||
if (!connection) { | |||
// no more free connection slots available | |||
this.idling = false; | |||
return; | |||
} | |||
// check if there is free space in the processing queue | |||
if (!this.idling && this._queue.length < this.options.maxConnections) { | |||
this.idling = true; | |||
this.emit('idle'); | |||
} | |||
let entry = (connection.queueEntry = this._queue.shift()); | |||
entry.messageId = (connection.queueEntry.mail.message.getHeader('message-id') || '').replace(/[<>\s]/g, ''); | |||
connection.available = false; | |||
this.logger.debug( | |||
{ | |||
tnx: 'pool', | |||
cid: connection.id, | |||
messageId: entry.messageId, | |||
action: 'assign' | |||
}, | |||
'Assigned message <%s> to #%s (%s)', | |||
entry.messageId, | |||
connection.id, | |||
connection.messages + 1 | |||
); | |||
if (this._rateLimit.limit) { | |||
this._rateLimit.counter++; | |||
if (!this._rateLimit.checkpoint) { | |||
this._rateLimit.checkpoint = Date.now(); | |||
} | |||
} | |||
connection.send(entry.mail, (err, info) => { | |||
// only process callback if current handler is not changed | |||
if (entry === connection.queueEntry) { | |||
try { | |||
entry.callback(err, info); | |||
} catch (E) { | |||
this.logger.error( | |||
{ | |||
err: E, | |||
tnx: 'callback', | |||
cid: connection.id | |||
}, | |||
'Callback error for #%s: %s', | |||
connection.id, | |||
E.message | |||
); | |||
} | |||
connection.queueEntry = false; | |||
} | |||
}); | |||
} | |||
/** | |||
* Creates a new pool resource | |||
*/ | |||
_createConnection() { | |||
let connection = new PoolResource(this); | |||
connection.id = ++this._connectionCounter; | |||
this.logger.info( | |||
{ | |||
tnx: 'pool', | |||
cid: connection.id, | |||
action: 'conection' | |||
}, | |||
'Created new pool resource #%s', | |||
connection.id | |||
); | |||
// resource comes available | |||
connection.on('available', () => { | |||
this.logger.debug( | |||
{ | |||
tnx: 'connection', | |||
cid: connection.id, | |||
action: 'available' | |||
}, | |||
'Connection #%s became available', | |||
connection.id | |||
); | |||
if (this._closed) { | |||
// if already closed run close() that will remove this connections from connections list | |||
this.close(); | |||
} else { | |||
// check if there's anything else to send | |||
this._processMessages(); | |||
} | |||
}); | |||
// resource is terminated with an error | |||
connection.once('error', err => { | |||
if (err.code !== 'EMAXLIMIT') { | |||
this.logger.error( | |||
{ | |||
err, | |||
tnx: 'pool', | |||
cid: connection.id | |||
}, | |||
'Pool Error for #%s: %s', | |||
connection.id, | |||
err.message | |||
); | |||
} else { | |||
this.logger.debug( | |||
{ | |||
tnx: 'pool', | |||
cid: connection.id, | |||
action: 'maxlimit' | |||
}, | |||
'Max messages limit exchausted for #%s', | |||
connection.id | |||
); | |||
} | |||
if (connection.queueEntry) { | |||
try { | |||
connection.queueEntry.callback(err); | |||
} catch (E) { | |||
this.logger.error( | |||
{ | |||
err: E, | |||
tnx: 'callback', | |||
cid: connection.id | |||
}, | |||
'Callback error for #%s: %s', | |||
connection.id, | |||
E.message | |||
); | |||
} | |||
connection.queueEntry = false; | |||
} | |||
// remove the erroneus connection from connections list | |||
this._removeConnection(connection); | |||
this._continueProcessing(); | |||
}); | |||
connection.once('close', () => { | |||
this.logger.info( | |||
{ | |||
tnx: 'connection', | |||
cid: connection.id, | |||
action: 'closed' | |||
}, | |||
'Connection #%s was closed', | |||
connection.id | |||
); | |||
this._removeConnection(connection); | |||
if (connection.queueEntry) { | |||
// If the connection closed when sending, add the message to the queue again | |||
// if max number of requeues is not reached yet | |||
// Note that we must wait a bit.. because the callback of the 'error' handler might be called | |||
// in the next event loop | |||
setTimeout(() => { | |||
if (connection.queueEntry) { | |||
if (this._shouldRequeuOnConnectionClose(connection.queueEntry)) { | |||
this._requeueEntryOnConnectionClose(connection); | |||
} else { | |||
this._failDeliveryOnConnectionClose(connection); | |||
} | |||
} | |||
this._continueProcessing(); | |||
}, 50); | |||
} else { | |||
this._continueProcessing(); | |||
} | |||
}); | |||
this._connections.push(connection); | |||
return connection; | |||
} | |||
_shouldRequeuOnConnectionClose(queueEntry) { | |||
if (this.options.maxRequeues === undefined || this.options.maxRequeues < 0) { | |||
return true; | |||
} | |||
return queueEntry.requeueAttempts < this.options.maxRequeues; | |||
} | |||
_failDeliveryOnConnectionClose(connection) { | |||
if (connection.queueEntry && connection.queueEntry.callback) { | |||
try { | |||
connection.queueEntry.callback(new Error('Reached maximum number of retries after connection was closed')); | |||
} catch (E) { | |||
this.logger.error( | |||
{ | |||
err: E, | |||
tnx: 'callback', | |||
messageId: connection.queueEntry.messageId, | |||
cid: connection.id | |||
}, | |||
'Callback error for #%s: %s', | |||
connection.id, | |||
E.message | |||
); | |||
} | |||
connection.queueEntry = false; | |||
} | |||
} | |||
_requeueEntryOnConnectionClose(connection) { | |||
connection.queueEntry.requeueAttempts = connection.queueEntry.requeueAttempts + 1; | |||
this.logger.debug( | |||
{ | |||
tnx: 'pool', | |||
cid: connection.id, | |||
messageId: connection.queueEntry.messageId, | |||
action: 'requeue' | |||
}, | |||
'Re-queued message <%s> for #%s. Attempt: #%s', | |||
connection.queueEntry.messageId, | |||
connection.id, | |||
connection.queueEntry.requeueAttempts | |||
); | |||
this._queue.unshift(connection.queueEntry); | |||
connection.queueEntry = false; | |||
} | |||
/** | |||
* Continue to process message if the pool hasn't closed | |||
*/ | |||
_continueProcessing() { | |||
if (this._closed) { | |||
this.close(); | |||
} else { | |||
setTimeout(() => this._processMessages(), 100); | |||
} | |||
} | |||
/** | |||
* Remove resource from pool | |||
* | |||
* @param {Object} connection The PoolResource to remove | |||
*/ | |||
_removeConnection(connection) { | |||
let index = this._connections.indexOf(connection); | |||
if (index !== -1) { | |||
this._connections.splice(index, 1); | |||
} | |||
} | |||
/** | |||
* Checks if connections have hit current rate limit and if so, queues the availability callback | |||
* | |||
* @param {Function} callback Callback function to run once rate limiter has been cleared | |||
*/ | |||
_checkRateLimit(callback) { | |||
if (!this._rateLimit.limit) { | |||
return callback(); | |||
} | |||
let now = Date.now(); | |||
if (this._rateLimit.counter < this._rateLimit.limit) { | |||
return callback(); | |||
} | |||
this._rateLimit.waiting.push(callback); | |||
if (this._rateLimit.checkpoint <= now - this._rateLimit.delta) { | |||
return this._clearRateLimit(); | |||
} else if (!this._rateLimit.timeout) { | |||
this._rateLimit.timeout = setTimeout(() => this._clearRateLimit(), this._rateLimit.delta - (now - this._rateLimit.checkpoint)); | |||
this._rateLimit.checkpoint = now; | |||
} | |||
} | |||
/** | |||
* Clears current rate limit limitation and runs paused callback | |||
*/ | |||
_clearRateLimit() { | |||
clearTimeout(this._rateLimit.timeout); | |||
this._rateLimit.timeout = null; | |||
this._rateLimit.counter = 0; | |||
this._rateLimit.checkpoint = false; | |||
// resume all paused connections | |||
while (this._rateLimit.waiting.length) { | |||
let cb = this._rateLimit.waiting.shift(); | |||
setImmediate(cb); | |||
} | |||
} | |||
/** | |||
* Returns true if there are free slots in the queue | |||
*/ | |||
isIdle() { | |||
return this.idling; | |||
} | |||
/** | |||
* Verifies SMTP configuration | |||
* | |||
* @param {Function} callback Callback function | |||
*/ | |||
verify(callback) { | |||
let promise; | |||
if (!callback) { | |||
promise = new Promise((resolve, reject) => { | |||
callback = shared.callbackPromise(resolve, reject); | |||
}); | |||
} | |||
let auth = new PoolResource(this).auth; | |||
this.getSocket(this.options, (err, socketOptions) => { | |||
if (err) { | |||
return callback(err); | |||
} | |||
let options = this.options; | |||
if (socketOptions && socketOptions.connection) { | |||
this.logger.info( | |||
{ | |||
tnx: 'proxy', | |||
remoteAddress: socketOptions.connection.remoteAddress, | |||
remotePort: socketOptions.connection.remotePort, | |||
destHost: options.host || '', | |||
destPort: options.port || '', | |||
action: 'connected' | |||
}, | |||
'Using proxied socket from %s:%s to %s:%s', | |||
socketOptions.connection.remoteAddress, | |||
socketOptions.connection.remotePort, | |||
options.host || '', | |||
options.port || '' | |||
); | |||
options = shared.assign(false, options); | |||
Object.keys(socketOptions).forEach(key => { | |||
options[key] = socketOptions[key]; | |||
}); | |||
} | |||
let connection = new SMTPConnection(options); | |||
let returned = false; | |||
connection.once('error', err => { | |||
if (returned) { | |||
return; | |||
} | |||
returned = true; | |||
connection.close(); | |||
return callback(err); | |||
}); | |||
connection.once('end', () => { | |||
if (returned) { | |||
return; | |||
} | |||
returned = true; | |||
return callback(new Error('Connection closed')); | |||
}); | |||
let finalize = () => { | |||
if (returned) { | |||
return; | |||
} | |||
returned = true; | |||
connection.quit(); | |||
return callback(null, true); | |||
}; | |||
connection.connect(() => { | |||
if (returned) { | |||
return; | |||
} | |||
if (auth && (connection.allowsAuth || options.forceAuth)) { | |||
connection.login(auth, err => { | |||
if (returned) { | |||
return; | |||
} | |||
if (err) { | |||
returned = true; | |||
connection.close(); | |||
return callback(err); | |||
} | |||
finalize(); | |||
}); | |||
} else { | |||
finalize(); | |||
} | |||
}); | |||
}); | |||
return promise; | |||
} | |||
} | |||
// expose to the world | |||
module.exports = SMTPPool; |
@ -1,253 +0,0 @@ | |||
'use strict'; | |||
const SMTPConnection = require('../smtp-connection'); | |||
const assign = require('../shared').assign; | |||
const XOAuth2 = require('../xoauth2'); | |||
const EventEmitter = require('events'); | |||
/** | |||
* Creates an element for the pool | |||
* | |||
* @constructor | |||
* @param {Object} options SMTPPool instance | |||
*/ | |||
class PoolResource extends EventEmitter { | |||
constructor(pool) { | |||
super(); | |||
this.pool = pool; | |||
this.options = pool.options; | |||
this.logger = this.pool.logger; | |||
if (this.options.auth) { | |||
switch ((this.options.auth.type || '').toString().toUpperCase()) { | |||
case 'OAUTH2': { | |||
let oauth2 = new XOAuth2(this.options.auth, this.logger); | |||
oauth2.provisionCallback = (this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback; | |||
this.auth = { | |||
type: 'OAUTH2', | |||
user: this.options.auth.user, | |||
oauth2, | |||
method: 'XOAUTH2' | |||
}; | |||
oauth2.on('token', token => this.pool.mailer.emit('token', token)); | |||
oauth2.on('error', err => this.emit('error', err)); | |||
break; | |||
} | |||
default: | |||
if (!this.options.auth.user && !this.options.auth.pass) { | |||
break; | |||
} | |||
this.auth = { | |||
type: (this.options.auth.type || '').toString().toUpperCase() || 'LOGIN', | |||
user: this.options.auth.user, | |||
credentials: { | |||
user: this.options.auth.user || '', | |||
pass: this.options.auth.pass, | |||
options: this.options.auth.options | |||
}, | |||
method: (this.options.auth.method || '').trim().toUpperCase() || this.options.authMethod || false | |||
}; | |||
} | |||
} | |||
this._connection = false; | |||
this._connected = false; | |||
this.messages = 0; | |||
this.available = true; | |||
} | |||
/** | |||
* Initiates a connection to the SMTP server | |||
* | |||
* @param {Function} callback Callback function to run once the connection is established or failed | |||
*/ | |||
connect(callback) { | |||
this.pool.getSocket(this.options, (err, socketOptions) => { | |||
if (err) { | |||
return callback(err); | |||
} | |||
let returned = false; | |||
let options = this.options; | |||
if (socketOptions && socketOptions.connection) { | |||
this.logger.info( | |||
{ | |||
tnx: 'proxy', | |||
remoteAddress: socketOptions.connection.remoteAddress, | |||
remotePort: socketOptions.connection.remotePort, | |||
destHost: options.host || '', | |||
destPort: options.port || '', | |||
action: 'connected' | |||
}, | |||
'Using proxied socket from %s:%s to %s:%s', | |||
socketOptions.connection.remoteAddress, | |||
socketOptions.connection.remotePort, | |||
options.host || '', | |||
options.port || '' | |||
); | |||
options = assign(false, options); | |||
Object.keys(socketOptions).forEach(key => { | |||
options[key] = socketOptions[key]; | |||
}); | |||
} | |||
this.connection = new SMTPConnection(options); | |||
this.connection.once('error', err => { | |||
this.emit('error', err); | |||
if (returned) { | |||
return; | |||
} | |||
returned = true; | |||
return callback(err); | |||
}); | |||
this.connection.once('end', () => { | |||
this.close(); | |||
if (returned) { | |||
return; | |||
} | |||
returned = true; | |||
let timer = setTimeout(() => { | |||
if (returned) { | |||
return; | |||
} | |||
// still have not returned, this means we have an unexpected connection close | |||
let err = new Error('Unexpected socket close'); | |||
if (this.connection && this.connection._socket && this.connection._socket.upgrading) { | |||
// starttls connection errors | |||
err.code = 'ETLS'; | |||
} | |||
callback(err); | |||
}, 1000); | |||
try { | |||
timer.unref(); | |||
} catch (E) { | |||
// Ignore. Happens on envs with non-node timer implementation | |||
} | |||
}); | |||
this.connection.connect(() => { | |||
if (returned) { | |||
return; | |||
} | |||
if (this.auth && (this.connection.allowsAuth || options.forceAuth)) { | |||
this.connection.login(this.auth, err => { | |||
if (returned) { | |||
return; | |||
} | |||
returned = true; | |||
if (err) { | |||
this.connection.close(); | |||
this.emit('error', err); | |||
return callback(err); | |||
} | |||
this._connected = true; | |||
callback(null, true); | |||
}); | |||
} else { | |||
returned = true; | |||
this._connected = true; | |||
return callback(null, true); | |||
} | |||
}); | |||
}); | |||
} | |||
/** | |||
* Sends an e-mail to be sent using the selected settings | |||
* | |||
* @param {Object} mail Mail object | |||
* @param {Function} callback Callback function | |||
*/ | |||
send(mail, callback) { | |||
if (!this._connected) { | |||
return this.connect(err => { | |||
if (err) { | |||
return callback(err); | |||
} | |||
return this.send(mail, callback); | |||
}); | |||
} | |||
let envelope = mail.message.getEnvelope(); | |||
let messageId = mail.message.messageId(); | |||
let recipients = [].concat(envelope.to || []); | |||
if (recipients.length > 3) { | |||
recipients.push('...and ' + recipients.splice(2).length + ' more'); | |||
} | |||
this.logger.info( | |||
{ | |||
tnx: 'send', | |||
messageId, | |||
cid: this.id | |||
}, | |||
'Sending message %s using #%s to <%s>', | |||
messageId, | |||
this.id, | |||
recipients.join(', ') | |||
); | |||
if (mail.data.dsn) { | |||
envelope.dsn = mail.data.dsn; | |||
} | |||
this.connection.send(envelope, mail.message.createReadStream(), (err, info) => { | |||
this.messages++; | |||
if (err) { | |||
this.connection.close(); | |||
this.emit('error', err); | |||
return callback(err); | |||
} | |||
info.envelope = { | |||
from: envelope.from, | |||
to: envelope.to | |||
}; | |||
info.messageId = messageId; | |||
setImmediate(() => { | |||
let err; | |||
if (this.messages >= this.options.maxMessages) { | |||
err = new Error('Resource exhausted'); | |||
err.code = 'EMAXLIMIT'; | |||
this.connection.close(); | |||
this.emit('error', err); | |||
} else { | |||
this.pool._checkRateLimit(() => { | |||
this.available = true; | |||
this.emit('available'); | |||
}); | |||
} | |||
}); | |||
callback(null, info); | |||
}); | |||
} | |||
/** | |||
* Closes the connection | |||
*/ | |||
close() { | |||
this._connected = false; | |||
if (this.auth && this.auth.oauth2) { | |||
this.auth.oauth2.removeAllListeners(); | |||
} | |||
if (this.connection) { | |||
this.connection.close(); | |||
} | |||
this.emit('close'); | |||
} | |||
} | |||
module.exports = PoolResource; |
@ -1,408 +0,0 @@ | |||
'use strict'; | |||
const EventEmitter = require('events'); | |||
const SMTPConnection = require('../smtp-connection'); | |||
const wellKnown = require('../well-known'); | |||
const shared = require('../shared'); | |||
const XOAuth2 = require('../xoauth2'); | |||
const packageData = require('../../package.json'); | |||
/** | |||
* Creates a SMTP transport object for Nodemailer | |||
* | |||
* @constructor | |||
* @param {Object} options Connection options | |||
*/ | |||
class SMTPTransport extends EventEmitter { | |||
constructor(options) { | |||
super(); | |||
options = options || {}; | |||
if (typeof options === 'string') { | |||
options = { | |||
url: options | |||
}; | |||
} | |||
let urlData; | |||
let service = options.service; | |||
if (typeof options.getSocket === 'function') { | |||
this.getSocket = options.getSocket; | |||
} | |||
if (options.url) { | |||
urlData = shared.parseConnectionUrl(options.url); | |||
service = service || urlData.service; | |||
} | |||
this.options = shared.assign( | |||
false, // create new object | |||
options, // regular options | |||
urlData, // url options | |||
service && wellKnown(service) // wellknown options | |||
); | |||
this.logger = shared.getLogger(this.options, { | |||
component: this.options.component || 'smtp-transport' | |||
}); | |||
// temporary object | |||
let connection = new SMTPConnection(this.options); | |||
this.name = 'SMTP'; | |||
this.version = packageData.version + '[client:' + connection.version + ']'; | |||
if (this.options.auth) { | |||
this.auth = this.getAuth({}); | |||
} | |||
} | |||
/** | |||
* Placeholder function for creating proxy sockets. This method immediatelly returns | |||
* without a socket | |||
* | |||
* @param {Object} options Connection options | |||
* @param {Function} callback Callback function to run with the socket keys | |||
*/ | |||
getSocket(options, callback) { | |||
// return immediatelly | |||
return setImmediate(() => callback(null, false)); | |||
} | |||
getAuth(authOpts) { | |||
if (!authOpts) { | |||
return this.auth; | |||
} | |||
let hasAuth = false; | |||
let authData = {}; | |||
if (this.options.auth && typeof this.options.auth === 'object') { | |||
Object.keys(this.options.auth).forEach(key => { | |||
hasAuth = true; | |||
authData[key] = this.options.auth[key]; | |||
}); | |||
} | |||
if (authOpts && typeof authOpts === 'object') { | |||
Object.keys(authOpts).forEach(key => { | |||
hasAuth = true; | |||
authData[key] = authOpts[key]; | |||
}); | |||
} | |||
if (!hasAuth) { | |||
return false; | |||
} | |||
switch ((authData.type || '').toString().toUpperCase()) { | |||
case 'OAUTH2': { | |||
if (!authData.service && !authData.user) { | |||
return false; | |||
} | |||
let oauth2 = new XOAuth2(authData, this.logger); | |||
oauth2.provisionCallback = (this.mailer && this.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback; | |||
oauth2.on('token', token => this.mailer.emit('token', token)); | |||
oauth2.on('error', err => this.emit('error', err)); | |||
return { | |||
type: 'OAUTH2', | |||
user: authData.user, | |||
oauth2, | |||
method: 'XOAUTH2' | |||
}; | |||
} | |||
default: | |||
return { | |||
type: (authData.type || '').toString().toUpperCase() || 'LOGIN', | |||
user: authData.user, | |||
credentials: { | |||
user: authData.user || '', | |||
pass: authData.pass, | |||
options: authData.options | |||
}, | |||
method: (authData.method || '').trim().toUpperCase() || this.options.authMethod || false | |||
}; | |||
} | |||
} | |||
/** | |||
* Sends an e-mail using the selected settings | |||
* | |||
* @param {Object} mail Mail object | |||
* @param {Function} callback Callback function | |||
*/ | |||
send(mail, callback) { | |||
this.getSocket(this.options, (err, socketOptions) => { | |||
if (err) { | |||
return callback(err); | |||
} | |||
let returned = false; | |||
let options = this.options; | |||
if (socketOptions && socketOptions.connection) { | |||
this.logger.info( | |||
{ | |||
tnx: 'proxy', | |||
remoteAddress: socketOptions.connection.remoteAddress, | |||
remotePort: socketOptions.connection.remotePort, | |||
destHost: options.host || '', | |||
destPort: options.port || '', | |||
action: 'connected' | |||
}, | |||
'Using proxied socket from %s:%s to %s:%s', | |||
socketOptions.connection.remoteAddress, | |||
socketOptions.connection.remotePort, | |||
options.host || '', | |||
options.port || '' | |||
); | |||
// only copy options if we need to modify it | |||
options = shared.assign(false, options); | |||
Object.keys(socketOptions).forEach(key => { | |||
options[key] = socketOptions[key]; | |||
}); | |||
} | |||
let connection = new SMTPConnection(options); | |||
connection.once('error', err => { | |||
if (returned) { | |||
return; | |||
} | |||
returned = true; | |||
connection.close(); | |||
return callback(err); | |||
}); | |||
connection.once('end', () => { | |||
if (returned) { | |||
return; | |||
} | |||
let timer = setTimeout(() => { | |||
if (returned) { | |||
return; | |||
} | |||
returned = true; | |||
// still have not returned, this means we have an unexpected connection close | |||
let err = new Error('Unexpected socket close'); | |||
if (connection && connection._socket && connection._socket.upgrading) { | |||
// starttls connection errors | |||
err.code = 'ETLS'; | |||
} | |||
callback(err); | |||
}, 1000); | |||
try { | |||
timer.unref(); | |||
} catch (E) { | |||
// Ignore. Happens on envs with non-node timer implementation | |||
} | |||
}); | |||
let sendMessage = () => { | |||
let envelope = mail.message.getEnvelope(); | |||
let messageId = mail.message.messageId(); | |||
let recipients = [].concat(envelope.to || []); | |||
if (recipients.length > 3) { | |||
recipients.push('...and ' + recipients.splice(2).length + ' more'); | |||
} | |||
if (mail.data.dsn) { | |||
envelope.dsn = mail.data.dsn; | |||
} | |||
this.logger.info( | |||
{ | |||
tnx: 'send', | |||
messageId | |||
}, | |||
'Sending message %s to <%s>', | |||
messageId, | |||
recipients.join(', ') | |||
); | |||
connection.send(envelope, mail.message.createReadStream(), (err, info) => { | |||
returned = true; | |||
connection.close(); | |||
if (err) { | |||
this.logger.error( | |||
{ | |||
err, | |||
tnx: 'send' | |||
}, | |||
'Send error for %s: %s', | |||
messageId, | |||
err.message | |||
); | |||
return callback(err); | |||
} | |||
info.envelope = { | |||
from: envelope.from, | |||
to: envelope.to | |||
}; | |||
info.messageId = messageId; | |||
try { | |||
return callback(null, info); | |||
} catch (E) { | |||
this.logger.error( | |||
{ | |||
err: E, | |||
tnx: 'callback' | |||
}, | |||
'Callback error for %s: %s', | |||
messageId, | |||
E.message | |||
); | |||
} | |||
}); | |||
}; | |||
connection.connect(() => { | |||
if (returned) { | |||
return; | |||
} | |||
let auth = this.getAuth(mail.data.auth); | |||
if (auth && (connection.allowsAuth || options.forceAuth)) { | |||
connection.login(auth, err => { | |||
if (auth && auth !== this.auth && auth.oauth2) { | |||
auth.oauth2.removeAllListeners(); | |||
} | |||
if (returned) { | |||
return; | |||
} | |||
if (err) { | |||
returned = true; | |||
connection.close(); | |||
return callback(err); | |||
} | |||
sendMessage(); | |||
}); | |||
} else { | |||
sendMessage(); | |||
} | |||
}); | |||
}); | |||
} | |||
/** | |||
* Verifies SMTP configuration | |||
* | |||
* @param {Function} callback Callback function | |||
*/ | |||
verify(callback) { | |||
let promise; | |||
if (!callback) { | |||
promise = new Promise((resolve, reject) => { | |||
callback = shared.callbackPromise(resolve, reject); | |||
}); | |||
} | |||
this.getSocket(this.options, (err, socketOptions) => { | |||
if (err) { | |||
return callback(err); | |||
} | |||
let options = this.options; | |||
if (socketOptions && socketOptions.connection) { | |||
this.logger.info( | |||
{ | |||
tnx: 'proxy', | |||
remoteAddress: socketOptions.connection.remoteAddress, | |||
remotePort: socketOptions.connection.remotePort, | |||
destHost: options.host || '', | |||
destPort: options.port || '', | |||
action: 'connected' | |||
}, | |||
'Using proxied socket from %s:%s to %s:%s', | |||
socketOptions.connection.remoteAddress, | |||
socketOptions.connection.remotePort, | |||
options.host || '', | |||
options.port || '' | |||
); | |||
options = shared.assign(false, options); | |||
Object.keys(socketOptions).forEach(key => { | |||
options[key] = socketOptions[key]; | |||
}); | |||
} | |||
let connection = new SMTPConnection(options); | |||
let returned = false; | |||
connection.once('error', err => { | |||
if (returned) { | |||
return; | |||
} | |||
returned = true; | |||
connection.close(); | |||
return callback(err); | |||
}); | |||
connection.once('end', () => { | |||
if (returned) { | |||
return; | |||
} | |||
returned = true; | |||
return callback(new Error('Connection closed')); | |||
}); | |||
let finalize = () => { | |||
if (returned) { | |||
return; | |||
} | |||
returned = true; | |||
connection.quit(); | |||
return callback(null, true); | |||
}; | |||
connection.connect(() => { | |||
if (returned) { | |||
return; | |||
} | |||
let authData = this.getAuth({}); | |||
if (authData && (connection.allowsAuth || options.forceAuth)) { | |||
connection.login(authData, err => { | |||
if (returned) { | |||
return; | |||
} | |||
if (err) { | |||
returned = true; | |||
connection.close(); | |||
return callback(err); | |||
} | |||
finalize(); | |||
}); | |||
} else { | |||
finalize(); | |||
} | |||
}); | |||
}); | |||
return promise; | |||
} | |||
/** | |||
* Releases resources | |||
*/ | |||
close() { | |||
if (this.auth && this.auth.oauth2) { | |||
this.auth.oauth2.removeAllListeners(); | |||
} | |||
this.emit('close'); | |||
} | |||
} | |||
// expose to the world | |||
module.exports = SMTPTransport; |
@ -1,142 +0,0 @@ | |||
'use strict'; | |||
const packageData = require('../../package.json'); | |||
const shared = require('../shared'); | |||
const LeWindows = require('../sendmail-transport/le-windows'); | |||
const LeUnix = require('../sendmail-transport/le-unix'); | |||
/** | |||
* Generates a Transport object for streaming | |||
* | |||
* Possible options can be the following: | |||
* | |||
* * **buffer** if true, then returns the message as a Buffer object instead of a stream | |||
* * **newline** either 'windows' or 'unix' | |||
* | |||
* @constructor | |||
* @param {Object} optional config parameter | |||
*/ | |||
class StreamTransport { | |||
constructor(options) { | |||
options = options || {}; | |||
this.options = options || {}; | |||
this.name = 'StreamTransport'; | |||
this.version = packageData.version; | |||
this.logger = shared.getLogger(this.options, { | |||
component: this.options.component || 'stream-transport' | |||
}); | |||
this.winbreak = ['win', 'windows', 'dos', '\r\n'].includes((options.newline || '').toString().toLowerCase()); | |||
} | |||
/** | |||
* Compiles a mailcomposer message and forwards it to handler that sends it | |||
* | |||
* @param {Object} emailMessage MailComposer object | |||
* @param {Function} callback Callback function to run when the sending is completed | |||
*/ | |||
send(mail, done) { | |||
// We probably need this in the output | |||
mail.message.keepBcc = true; | |||
let envelope = mail.data.envelope || mail.message.getEnvelope(); | |||
let messageId = mail.message.messageId(); | |||
let recipients = [].concat(envelope.to || []); | |||
if (recipients.length > 3) { | |||
recipients.push('...and ' + recipients.splice(2).length + ' more'); | |||
} | |||
this.logger.info( | |||
{ | |||
tnx: 'send', | |||
messageId | |||
}, | |||
'Sending message %s to <%s> using %s line breaks', | |||
messageId, | |||
recipients.join(', '), | |||
this.winbreak ? '<CR><LF>' : '<LF>' | |||
); | |||
setImmediate(() => { | |||
let sourceStream; | |||
let stream; | |||
let transform; | |||
try { | |||
transform = this.winbreak ? new LeWindows() : new LeUnix(); | |||
sourceStream = mail.message.createReadStream(); | |||
stream = sourceStream.pipe(transform); | |||
sourceStream.on('error', err => stream.emit('error', err)); | |||
} catch (E) { | |||
this.logger.error( | |||
{ | |||
err: E, | |||
tnx: 'send', | |||
messageId | |||
}, | |||
'Creating send stream failed for %s. %s', | |||
messageId, | |||
E.message | |||
); | |||
return done(E); | |||
} | |||
if (!this.options.buffer) { | |||
stream.once('error', err => { | |||
this.logger.error( | |||
{ | |||
err, | |||
tnx: 'send', | |||
messageId | |||
}, | |||
'Failed creating message for %s. %s', | |||
messageId, | |||
err.message | |||
); | |||
}); | |||
return done(null, { | |||
envelope: mail.data.envelope || mail.message.getEnvelope(), | |||
messageId, | |||
message: stream | |||
}); | |||
} | |||
let chunks = []; | |||
let chunklen = 0; | |||
stream.on('readable', () => { | |||
let chunk; | |||
while ((chunk = stream.read()) !== null) { | |||
chunks.push(chunk); | |||
chunklen += chunk.length; | |||
} | |||
}); | |||
stream.once('error', err => { | |||
this.logger.error( | |||
{ | |||
err, | |||
tnx: 'send', | |||
messageId | |||
}, | |||
'Failed creating message for %s. %s', | |||
messageId, | |||
err.message | |||
); | |||
return done(err); | |||
}); | |||
stream.on('end', () => | |||
done(null, { | |||
envelope: mail.data.envelope || mail.message.getEnvelope(), | |||
messageId, | |||
message: Buffer.concat(chunks, chunklen) | |||
}) | |||
); | |||
}); | |||
} | |||
} | |||
module.exports = StreamTransport; |
@ -1,47 +0,0 @@ | |||
'use strict'; | |||
const services = require('./services.json'); | |||
const normalized = {}; | |||
Object.keys(services).forEach(key => { | |||
let service = services[key]; | |||
normalized[normalizeKey(key)] = normalizeService(service); | |||
[].concat(service.aliases || []).forEach(alias => { | |||
normalized[normalizeKey(alias)] = normalizeService(service); | |||
}); | |||
[].concat(service.domains || []).forEach(domain => { | |||
normalized[normalizeKey(domain)] = normalizeService(service); | |||
}); | |||
}); | |||
function normalizeKey(key) { | |||
return key.replace(/[^a-zA-Z0-9.-]/g, '').toLowerCase(); | |||
} | |||
function normalizeService(service) { | |||
let filter = ['domains', 'aliases']; | |||
let response = {}; | |||
Object.keys(service).forEach(key => { | |||
if (filter.indexOf(key) < 0) { | |||
response[key] = service[key]; | |||
} | |||
}); | |||
return response; | |||
} | |||
/** | |||
* Resolves SMTP config for given key. Key can be a name (like 'Gmail'), alias (like 'Google Mail') or | |||
* an email address (like 'test@googlemail.com'). | |||
* | |||
* @param {String} key [description] | |||
* @returns {Object} SMTP config or false if not found | |||
*/ | |||
module.exports = function(key) { | |||
key = normalizeKey(key.split('@').pop()); | |||
return normalized[key] || false; | |||
}; |
@ -1,262 +0,0 @@ | |||
{ | |||
"1und1": { | |||
"host": "smtp.1und1.de", | |||
"port": 465, | |||
"secure": true, | |||
"authMethod": "LOGIN" | |||
}, | |||
"AOL": { | |||
"domains": ["aol.com"], | |||
"host": "smtp.aol.com", | |||
"port": 587 | |||
}, | |||
"DebugMail": { | |||
"host": "debugmail.io", | |||
"port": 25 | |||
}, | |||
"DynectEmail": { | |||
"aliases": ["Dynect"], | |||
"host": "smtp.dynect.net", | |||
"port": 25 | |||
}, | |||
"FastMail": { | |||
"domains": ["fastmail.fm"], | |||
"host": "smtp.fastmail.com", | |||
"port": 465, | |||
"secure": true | |||
}, | |||
"GandiMail": { | |||
"aliases": ["Gandi", "Gandi Mail"], | |||
"host": "mail.gandi.net", | |||
"port": 587 | |||
}, | |||
"Gmail": { | |||
"aliases": ["Google Mail"], | |||
"domains": ["gmail.com", "googlemail.com"], | |||
"host": "smtp.gmail.com", | |||
"port": 465, | |||
"secure": true | |||
}, | |||
"Godaddy": { | |||
"host": "smtpout.secureserver.net", | |||
"port": 25 | |||
}, | |||
"GodaddyAsia": { | |||
"host": "smtp.asia.secureserver.net", | |||
"port": 25 | |||
}, | |||
"GodaddyEurope": { | |||
"host": "smtp.europe.secureserver.net", | |||
"port": 25 | |||
}, | |||
"hot.ee": { | |||
"host": "mail.hot.ee" | |||
}, | |||
"Hotmail": { | |||
"aliases": ["Outlook", "Outlook.com", "Hotmail.com"], | |||
"domains": ["hotmail.com", "outlook.com"], | |||
"host": "smtp.live.com", | |||
"port": 587 | |||
}, | |||
"iCloud": { | |||
"aliases": ["Me", "Mac"], | |||
"domains": ["me.com", "mac.com"], | |||
"host": "smtp.mail.me.com", | |||
"port": 587 | |||
}, | |||
"mail.ee": { | |||
"host": "smtp.mail.ee" | |||
}, | |||
"Mail.ru": { | |||
"host": "smtp.mail.ru", | |||
"port": 465, | |||
"secure": true | |||
}, | |||
"Maildev": { | |||
"port": 1025, | |||
"ignoreTLS": true | |||
}, | |||
"Mailgun": { | |||
"host": "smtp.mailgun.org", | |||
"port": 465, | |||
"secure": true | |||
}, | |||
"Mailjet": { | |||
"host": "in.mailjet.com", | |||
"port": 587 | |||
}, | |||
"Mailosaur": { | |||
"host": "mailosaur.io", | |||
"port": 25 | |||
}, | |||
"Mailtrap": { | |||
"host": "smtp.mailtrap.io", | |||
"port": 2525 | |||
}, | |||
"Mandrill": { | |||
"host": "smtp.mandrillapp.com", | |||
"port": 587 | |||
}, | |||
"Naver": { | |||
"host": "smtp.naver.com", | |||
"port": 587 | |||
}, | |||
"One": { | |||
"host": "send.one.com", | |||
"port": 465, | |||
"secure": true | |||
}, | |||
"OpenMailBox": { | |||
"aliases": ["OMB", "openmailbox.org"], | |||
"host": "smtp.openmailbox.org", | |||
"port": 465, | |||
"secure": true | |||
}, | |||
"Outlook365": { | |||
"host": "smtp.office365.com", | |||
"port": 587, | |||
"secure": false | |||
}, | |||
"Postmark": { | |||
"aliases": ["PostmarkApp"], | |||
"host": "smtp.postmarkapp.com", | |||
"port": 2525 | |||
}, | |||
"qiye.aliyun": { | |||
"host": "smtp.mxhichina.com", | |||
"port": "465", | |||
"secure": true | |||
}, | |||
"QQ": { | |||
"domains": ["qq.com"], | |||
"host": "smtp.qq.com", | |||
"port": 465, | |||
"secure": true | |||
}, | |||
"QQex": { | |||
"aliases": ["QQ Enterprise"], | |||
"domains": ["exmail.qq.com"], | |||
"host": "smtp.exmail.qq.com", | |||
"port": 465, | |||
"secure": true | |||
}, | |||
"SendCloud": { | |||
"host": "smtpcloud.sohu.com", | |||
"port": 25 | |||
}, | |||
"SendGrid": { | |||
"host": "smtp.sendgrid.net", | |||
"port": 587 | |||
}, | |||
"SendinBlue": { | |||
"host": "smtp-relay.sendinblue.com", | |||
"port": 587 | |||
}, | |||
"SendPulse": { | |||
"host": "smtp-pulse.com", | |||
"port": 465, | |||
"secure": true | |||
}, | |||
"SES": { | |||
"host": "email-smtp.us-east-1.amazonaws.com", | |||
"port": 465, | |||
"secure": true | |||
}, | |||
"SES-US-EAST-1": { | |||
"host": "email-smtp.us-east-1.amazonaws.com", | |||
"port": 465, | |||
"secure": true | |||
}, | |||
"SES-US-WEST-2": { | |||
"host": "email-smtp.us-west-2.amazonaws.com", | |||
"port": 465, | |||
"secure": true | |||
}, | |||
"SES-EU-WEST-1": { | |||
"host": "email-smtp.eu-west-1.amazonaws.com", | |||
"port": 465, | |||
"secure": true | |||
}, | |||
"Sparkpost": { | |||
"aliases": ["SparkPost", "SparkPost Mail"], | |||
"domains": ["sparkpost.com"], | |||
"host": "smtp.sparkpostmail.com", | |||
"port": 587, | |||
"secure": false | |||
}, | |||
"Tipimail": { | |||
"host": "smtp.tipimail.com", | |||
"port": 587 | |||
}, | |||
"Yahoo": { | |||
"domains": ["yahoo.com"], | |||
"host": "smtp.mail.yahoo.com", | |||
"port": 465, | |||
"secure": true | |||
}, | |||
"Yandex": { | |||
"domains": ["yandex.ru"], | |||
"host": "smtp.yandex.ru", | |||
"port": 465, | |||
"secure": true | |||
}, | |||
"Zoho": { | |||
"host": "smtp.zoho.com", | |||
"port": 465, | |||
"secure": true, | |||
"authMethod": "LOGIN" | |||
}, | |||
"126": { | |||
"host": "smtp.126.com", | |||
"port": 465, | |||
"secure": true | |||
}, | |||
"163": { | |||
"host": "smtp.163.com", | |||
"port": 465, | |||
"secure": true | |||
} | |||
} |