node_modules: update (#290)

Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
Dawid Dziurla
2026-04-28 12:50:45 +02:00
committed by GitHub
parent 63e1562580
commit 42942bc2f8
1077 changed files with 12540 additions and 33773 deletions

39
node_modules/nodemailer/CHANGELOG.md generated vendored
View File

@@ -1,5 +1,44 @@
# CHANGELOG
## [8.0.7](https://github.com/nodemailer/nodemailer/compare/v8.0.6...v8.0.7) (2026-04-27)
### Bug Fixes
* keep domain as UTF-8 when local part is non-ASCII ([#1814](https://github.com/nodemailer/nodemailer/issues/1814)) ([66d4ecb](https://github.com/nodemailer/nodemailer/commit/66d4ecb5aa431f3614a26b3c08b9c63cdf32a9ea))
## [8.0.6](https://github.com/nodemailer/nodemailer/compare/v8.0.5...v8.0.6) (2026-04-24)
### Bug Fixes
* restore base64 wrap() trim behavior to prevent trailing CRLF ([#1810](https://github.com/nodemailer/nodemailer/issues/1810)) ([#1811](https://github.com/nodemailer/nodemailer/issues/1811)) ([b1ae6c1](https://github.com/nodemailer/nodemailer/commit/b1ae6c1c2927240737d9f68f316f0c84042b8adb))
## [8.0.5](https://github.com/nodemailer/nodemailer/compare/v8.0.4...v8.0.5) (2026-04-07)
### Bug Fixes
* decode SMTP server responses as UTF-8 at line boundary ([95876b1](https://github.com/nodemailer/nodemailer/commit/95876b103e587e49583e43f88cb2c3a61556f3ac))
* sanitize CRLF in transport name option to prevent SMTP command injection (GHSA-vvjj-xcjg-gr5g) ([0a43876](https://github.com/nodemailer/nodemailer/commit/0a43876801a420ca528f492eaa01bfc421cc306e))
## [8.0.4](https://github.com/nodemailer/nodemailer/compare/v8.0.3...v8.0.4) (2026-03-25)
### Bug Fixes
* sanitize envelope size to prevent SMTP command injection ([2d7b971](https://github.com/nodemailer/nodemailer/commit/2d7b9710e63555a1eb13d721296c51186d4b5651))
## [8.0.3](https://github.com/nodemailer/nodemailer/compare/v8.0.2...v8.0.3) (2026-03-18)
### Bug Fixes
* clean up addressparser and fix group name fallback producing undefined ([9d55877](https://github.com/nodemailer/nodemailer/commit/9d55877f8ed15a6aefd7ba76cbb6b6a6cdbcc4fd))
* fix cookie bugs, remove dead code, and improve hot-path efficiency ([e8c8b92](https://github.com/nodemailer/nodemailer/commit/e8c8b92f46f2a82d06d49cc9a6ffc26067f68524))
* refactor smtp-connection for clarity and add Node.js 6 syntax compat test ([c5b48ea](https://github.com/nodemailer/nodemailer/commit/c5b48ea61c28eabf347972f4198a12cdab226ff7))
* remove familySupportCache that broke DNS resolution tests ([c803d90](https://github.com/nodemailer/nodemailer/commit/c803d901f195a21edbb2c276b2e116564467aaaa))
## [8.0.2](https://github.com/nodemailer/nodemailer/compare/v8.0.1...v8.0.2) (2026-03-09)

55
node_modules/nodemailer/CLAUDE.md generated vendored Normal file
View File

@@ -0,0 +1,55 @@
# Nodemailer
E-mail sending library for Node.js. Zero runtime dependencies. Entry point is `lib/nodemailer.js`, which exposes `createTransport(transporter, defaults)` and routes to one of the bundled transports based on the options object.
## Layout
- `lib/nodemailer.js` — public entry, transport dispatch (`createTransport`).
- `lib/mailer/``Mail` class: the user-facing transport wrapper that normalizes messages, runs the DKIM signer, and hands off to the underlying transport's `.send()`.
- `lib/mail-composer/` + `lib/mime-node/` — message → MIME tree → raw RFC822 stream.
- `lib/smtp-connection/` — low-level SMTP/LMTP/ESMTP client. Hot path; security-sensitive. Used by `smtp-transport` and `smtp-pool`.
- `lib/smtp-transport/` — single-shot SMTP transport.
- `lib/smtp-pool/` — pooled SMTP transport with rate limiting.
- `lib/sendmail-transport/`, `lib/ses-transport/`, `lib/stream-transport/`, `lib/json-transport/` — alternate transports.
- `lib/dkim/`, `lib/addressparser/`, `lib/mime-funcs/`, `lib/base64/`, `lib/qp/`, `lib/punycode/`, `lib/well-known/`, `lib/xoauth2/`, `lib/fetch/`, `lib/shared/` — supporting modules.
- `test/` — mirrors `lib/` structure. Most suites spin up real `smtp-server` instances on ephemeral ports; raw `net` servers are used when byte-exact reply control is needed (e.g. injecting non-ASCII or invalid UTF-8).
Each transport must implement `name`, `version`, and `send(mail, callback)`. `Mail` discovers them via duck typing.
## Engine target
`engines.node = ">=6.0.0"`. The library is shipped as ES2017 script-mode CommonJS — no `import`, no top-level `await`, no optional chaining, no nullish coalescing, no class fields. ESLint enforces `ecmaVersion: 2017` and `sourceType: 'script'`. There is a Node 6 syntax-compat check (`npm run test:syntax`, runs `test/syntax-compat.js` inside `node:6-alpine`) that must keep passing — do not introduce syntax that breaks it. `'use strict';` directive at the top of every file.
## Conventions
- CommonJS only: `const x = require('...')`, `module.exports = ...`.
- Callback-first style throughout the public API. Many internals are still callback-based — match the style of the file you are editing rather than introducing promises mid-module.
- Prettier handles formatting; ESLint handles correctness. Run `npm run format` and `npm run lint` before sending changes. The lint config disables Prettier-overlapping rules.
- Snake_case is not used; camelCase for variables and methods, PascalCase for classes.
- Prefer small, surgical diffs. The codebase is mature and load-bearing — avoid drive-by refactors, comment churn, or "improvements" outside the scope of the change.
- Every change to security-sensitive code (anything in `lib/smtp-connection/`, address parsing, header generation, DKIM) needs tests that exercise the failure mode, not just the happy path.
## Testing
- `npm test` — full suite via `node --test` (~150s, 480+ tests, runs serially).
- `npm run test:coverage` — same suite under `c8`.
- `npm run test:syntax` — Node 6 syntax compatibility check in Docker.
- `npm run lint` / `npm run lint:fix`.
- `npm run format` / `npm run format:check`.
Always run `npm test` and `npm run lint` before considering a change done. Tests are required to pass on every commit because release-please cuts releases directly from `master`.
## Releases
Releases, version numbers, the `version` field in `package.json`, git tags, `CHANGELOG.md` entries, and npm publication are all managed automatically by the release-please GitHub Action (`.github/workflows/release.yaml`, configured by `.release-please-config.json`). **Never edit any of these manually and never propose manual edits to them.**
Release-please derives the next version and changelog from Conventional Commit messages on `master`, opens a release PR (`chore: release X.Y.Z [skip-ci]`), and publishes to npm with provenance when that PR is merged. The only thing that should land on `master` between releases is normal commits with Conventional Commit prefixes — release-please takes care of the rest.
Conventional Commit prefixes used in this repo: `fix:`, `feat:`, `chore:`, `docs:`, `refactor:`, `test:`. Use `fix:` for anything users would benefit from seeing in the changelog, including security fixes (reference the GHSA in the body).
## Security
This is a widely-deployed library — security-sensitive changes get extra scrutiny:
- SMTP command injection: any user-controllable value that flows into a written SMTP command (envelope addresses, sizes, the `name`/EHLO option, headers) must be CRLF-stripped or rejected at the boundary. Sanitize at the assignment, not at every call site.
- Server reply parsing in `lib/smtp-connection/index.js` uses a `'binary'` byte-container intermediate to reassemble multi-byte UTF-8 across socket chunks; the actual decode happens at line boundaries via `decodeServerResponse`. Don't change the chunk-buffering encoding without understanding why.
- Reference the GHSA ID in commit messages for advisories.

View File

@@ -10,23 +10,20 @@
function _handleAddress(tokens, depth) {
let isGroup = false;
let state = 'text';
let address;
let addresses = [];
let data = {
const addresses = [];
const data = {
address: [],
comment: [],
group: [],
text: [],
textWasQuoted: [] // Track which text tokens came from inside quotes
textWasQuoted: []
};
let i;
let len;
let insideQuotes = false; // Track if we're currently inside a quoted string
let insideQuotes = false;
// Filter out <addresses>, (comments) and regular text
for (i = 0, len = tokens.length; i < len; i++) {
let token = tokens[i];
let prevToken = i ? tokens[i - 1] : null;
for (let i = 0, len = tokens.length; i < len; i++) {
const token = tokens[i];
const prevToken = i ? tokens[i - 1] : null;
if (token.type === 'operator') {
switch (token.value) {
case '<':
@@ -43,7 +40,6 @@ function _handleAddress(tokens, depth) {
insideQuotes = false;
break;
case '"':
// Track quote state for text tokens
insideQuotes = !insideQuotes;
state = 'text';
break;
@@ -54,14 +50,12 @@ function _handleAddress(tokens, depth) {
}
} 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
// Handle unquoted name that includes a "<".
// Apple Mail truncates everything between an unexpected < and an address.
token.value = token.value.replace(/^[^<]*<\s*/, '');
}
if (prevToken && prevToken.noBreak && data[state].length) {
// join values
data[state][data[state].length - 1] += token.value;
if (state === 'text' && insideQuotes) {
data.textWasQuoted[data.textWasQuoted.length - 1] = true;
@@ -88,11 +82,9 @@ function _handleAddress(tokens, depth) {
// Parse group members, but flatten any nested groups (RFC 5322 doesn't allow nesting)
let groupMembers = [];
if (data.group.length) {
let parsedGroup = addressparser(data.group.join(','), { _depth: depth + 1 });
// Flatten: if any member is itself a group, extract its members into the sequence
const parsedGroup = addressparser(data.group.join(','), { _depth: depth + 1 });
parsedGroup.forEach(member => {
if (member.group) {
// Nested group detected - flatten it by adding its members directly
groupMembers = groupMembers.concat(member.group);
} else {
groupMembers.push(member);
@@ -101,40 +93,40 @@ function _handleAddress(tokens, depth) {
}
addresses.push({
name: data.text || (address && address.name),
name: data.text || '',
group: groupMembers
});
} 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--) {
// Security fix: Do not extract email addresses from quoted strings
// RFC 5321 allows @ inside quoted local-parts like "user@domain"@example.com
// Extracting emails from quoted text leads to misrouting vulnerabilities
if (!data.textWasQuoted[i] && data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
for (let i = data.text.length - 1; i >= 0; i--) {
// Security: Do not extract email addresses from quoted strings.
// RFC 5321 allows @ inside quoted local-parts like "user@domain"@example.com.
// Extracting emails from quoted text leads to misrouting vulnerabilities.
if (!data.textWasQuoted[i] && /^[^@\s]+@[^@\s]+$/.test(data.text[i])) {
data.address = data.text.splice(i, 1);
data.textWasQuoted.splice(i, 1);
break;
}
}
let _regexHandler = function (address) {
if (!data.address.length) {
data.address = [address.trim()];
return ' ';
} else {
return address;
}
};
// still no address
// Try a looser regex match if strict match found nothing
if (!data.address.length) {
for (i = data.text.length - 1; i >= 0; i--) {
// Security fix: Do not extract email addresses from quoted strings
let extracted = false;
for (let i = data.text.length - 1; i >= 0; i--) {
// Security: Do not extract email addresses from quoted strings
if (!data.textWasQuoted[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) {
data.text[i] = data.text[i]
.replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, match => {
if (!extracted) {
data.address = [match.trim()];
extracted = true;
return ' ';
}
return match;
})
.trim();
if (extracted) {
break;
}
}
@@ -142,13 +134,13 @@ function _handleAddress(tokens, depth) {
}
}
// If there's still is no text but a comment exixts, replace the two
// If there's still no text but a comment exists, 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
// Keep only the first address occurrence, push others to regular text
if (data.address.length > 1) {
data.text = data.text.concat(data.address.splice(1));
}
@@ -157,24 +149,20 @@ function _handleAddress(tokens, depth) {
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 || ''
};
const 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 = '';
}
if (address.address === address.name) {
if (/@/.test(address.address || '')) {
address.name = '';
} else {
address.address = '';
}
addresses.push(address);
}
addresses.push(address);
}
return addresses;
@@ -220,11 +208,11 @@ class Tokenizer {
* @return {Array} An array of operator|text tokens
*/
tokenize() {
let list = [];
const list = [];
for (let i = 0, len = this.str.length; i < len; i++) {
let chr = this.str.charAt(i);
let nextChr = i < len - 1 ? this.str.charAt(i + 1) : null;
const chr = this.str.charAt(i);
const nextChr = i < len - 1 ? this.str.charAt(i + 1) : null;
this.checkChar(chr, nextChr);
}
@@ -325,17 +313,17 @@ const MAX_NESTED_GROUP_DEPTH = 50;
*/
function addressparser(str, options) {
options = options || {};
let depth = options._depth || 0;
const depth = options._depth || 0;
// Prevent stack overflow from deeply nested groups (DoS protection)
if (depth > MAX_NESTED_GROUP_DEPTH) {
return [];
}
let tokenizer = new Tokenizer(str);
let tokens = tokenizer.tokenize();
const tokenizer = new Tokenizer(str);
const tokens = tokenizer.tokenize();
let addresses = [];
const addresses = [];
let address = [];
let parsedAddresses = [];
@@ -354,44 +342,41 @@ function addressparser(str, options) {
addresses.push(address);
}
addresses.forEach(address => {
address = _handleAddress(address, depth);
if (address.length) {
parsedAddresses = parsedAddresses.concat(address);
addresses.forEach(addr => {
const handled = _handleAddress(addr, depth);
if (handled.length) {
parsedAddresses = parsedAddresses.concat(handled);
}
});
// Merge fragments from unquoted display names containing commas/semicolons.
// When "Joe Foo, PhD <joe@example.com>" is split on the comma, it produces
// Merge fragments produced when unquoted display names contain commas.
// "Joe Foo, PhD <joe@example.com>" is split on the comma into
// [{name:"Joe Foo", address:""}, {name:"PhD", address:"joe@example.com"}].
// Detect this pattern and recombine: a name-only entry followed by an entry
// that has both a name and an address (from angle-bracket notation).
// Recombine: a name-only entry followed by an entry with both name and address.
for (let i = parsedAddresses.length - 2; i >= 0; i--) {
let current = parsedAddresses[i];
let next = parsedAddresses[i + 1];
if (current.address === '' && current.name && !current.group && next.address && next.name && !next.group) {
const current = parsedAddresses[i];
const next = parsedAddresses[i + 1];
if (current.address === '' && current.name && !current.group && next.address && next.name) {
next.name = current.name + ', ' + next.name;
parsedAddresses.splice(i, 1);
}
}
if (options.flatten) {
let addresses = [];
let walkAddressList = list => {
list.forEach(address => {
if (address.group) {
return walkAddressList(address.group);
} else {
addresses.push(address);
const flatAddresses = [];
const walkAddressList = list => {
list.forEach(entry => {
if (entry.group) {
return walkAddressList(entry.group);
}
flatAddresses.push(entry);
});
};
walkAddressList(parsedAddresses);
return addresses;
return flatAddresses;
}
return parsedAddresses;
}
// expose to the world
module.exports = addressparser;

View File

@@ -1,6 +1,6 @@
'use strict';
const Transform = require('stream').Transform;
const { Transform } = require('stream');
/**
* Encodes a Buffer into a base64 encoded string
@@ -31,16 +31,17 @@ function wrap(str, lineLength) {
return str;
}
let result = [];
const result = [];
let pos = 0;
let chunkLength = lineLength * 1024;
const chunkLength = lineLength * 1024;
const wrapRegex = new RegExp('.{' + lineLength + '}', 'g');
while (pos < str.length) {
let wrappedLines = str.substr(pos, chunkLength).replace(new RegExp('.{' + lineLength + '}', 'g'), '$&\r\n');
const wrappedLines = str.substr(pos, chunkLength).replace(wrapRegex, '$&\r\n').trim();
result.push(wrappedLines);
pos += chunkLength;
}
return result.join('');
return result.join('\r\n').trim();
}
/**
@@ -94,20 +95,17 @@ class Encoder extends Transform {
if (this.options.lineLength) {
b64 = wrap(b64, this.options.lineLength);
let lastLF = b64.lastIndexOf('\n');
// remove last line as it is still most probably incomplete
const lastLF = b64.lastIndexOf('\n');
if (lastLF < 0) {
this._curLine = b64;
b64 = '';
} else if (lastLF === b64.length - 1) {
this._curLine = '';
} else {
this._curLine = b64.substring(lastLF + 1);
b64 = b64.substring(0, lastLF + 1);
if (b64 && !b64.endsWith('\r\n')) {
b64 += '\r\n';
}
}
} else {
this._curLine = '';
}
if (b64) {
@@ -124,6 +122,7 @@ class Encoder extends Transform {
}
if (this._curLine) {
this._curLine = wrap(this._curLine, this.options.lineLength);
this.outputBytes += this._curLine.length;
this.push(Buffer.from(this._curLine, 'ascii'));
this._curLine = '';

View File

@@ -6,7 +6,7 @@
const MessageParser = require('./message-parser');
const RelaxedBody = require('./relaxed-body');
const sign = require('./sign');
const PassThrough = require('stream').PassThrough;
const { PassThrough } = require('stream');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
@@ -96,7 +96,7 @@ class DKIMSigner {
}
return this.createReadCache();
}
let chunk = this.chunks[this.readPos++];
const chunk = this.chunks[this.readPos++];
if (this.output.write(chunk) === false) {
return this.output.once('drain', () => {
this.sendNextChunk();
@@ -107,13 +107,13 @@ class DKIMSigner {
sendSignedOutput() {
let keyPos = 0;
let signNextKey = () => {
const 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, {
const key = this.keys[keyPos++];
const dkimField = sign(this.headers, this.hashAlgo, this.bodyHash, {
domainName: key.domainName,
keySelector: key.keySelector,
privateKey: key.privateKey,
@@ -211,7 +211,7 @@ class DKIM {
}
sign(input, extraOptions) {
let output = new PassThrough();
const output = new PassThrough();
let inputStream = input;
let writeValue = false;
@@ -225,18 +225,10 @@ class DKIM {
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];
}
});
options = Object.assign({}, extraOptions, this.options);
}
let signer = new DKIMSigner(options, this.keys, inputStream, output);
const signer = new DKIMSigner(options, this.keys, inputStream, output);
setImmediate(() => {
signer.signStream();
if (writeValue) {

View File

@@ -1,6 +1,6 @@
'use strict';
const Transform = require('stream').Transform;
const { Transform } = require('stream');
/**
* MessageParser instance is a transform stream that separates message headers
@@ -24,8 +24,8 @@ class MessageParser extends Transform {
* @param {Buffer} data Next data chunk from the stream
*/
updateLastBytes(data) {
let lblen = this.lastBytes.length;
let nblen = Math.min(data.length, lblen);
const lblen = this.lastBytes.length;
const nblen = Math.min(data.length, lblen);
// shift existing bytes
for (let i = 0, len = lblen - nblen; i < len; i++) {
@@ -50,9 +50,8 @@ class MessageParser extends Transform {
return true;
}
let lblen = this.lastBytes.length;
const 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) {
@@ -61,8 +60,8 @@ class MessageParser extends Transform {
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;
const pr1 = i - 1 < lblen ? this.lastBytes[i - 1] : data[i - 1 - lblen];
const 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;
@@ -83,17 +82,17 @@ class MessageParser extends Transform {
this.headerChunks = null;
this.emit('headers', this.parseHeaders());
if (data.length - 1 > headerPos) {
let chunk = data.slice(headerPos);
const 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);
}
this.headerBytes += data.length;
this.headerChunks.push(data);
// store last 4 bytes to catch header break
this.updateLastBytes(data);
@@ -127,7 +126,7 @@ class MessageParser extends Transform {
_flush(callback) {
if (this.headerChunks) {
let chunk = Buffer.concat(this.headerChunks, this.headerBytes);
const chunk = Buffer.concat(this.headerChunks, this.headerBytes);
this.bodySize += chunk.length;
this.push(chunk);
this.headerChunks = null;
@@ -136,7 +135,7 @@ class MessageParser extends Transform {
}
parseHeaders() {
let lines = (this.rawHeaders || '').toString().split(/\r?\n/);
const 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];

View File

@@ -2,7 +2,7 @@
// streams through a message body and calculates relaxed body hash
const Transform = require('stream').Transform;
const { Transform } = require('stream');
const crypto = require('crypto');
class RelaxedBody extends Transform {
@@ -29,7 +29,7 @@ class RelaxedBody extends Transform {
// 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];
const 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

View File

@@ -20,7 +20,7 @@ module.exports = (headers, hashAlgo, bodyHash, options) => {
options = options || {};
// all listed fields from RFC4871 #5.5
let defaultFieldNames =
const 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:' +
@@ -28,17 +28,16 @@ module.exports = (headers, hashAlgo, bodyHash, options) => {
'List-Id:List-Help:List-Unsubscribe:List-Subscribe:List-Post:' +
'List-Owner:List-Archive';
let fieldNames = options.headerFieldNames || defaultFieldNames;
const 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;
const canonicalizedHeaderData = relaxedHeaders(headers, fieldNames, options.skipFields);
const dkimHeader = generateDKIMHeader(options.domainName, options.keySelector, canonicalizedHeaderData.fieldNames, hashAlgo, bodyHash);
canonicalizedHeaderData.headers += 'dkim-signature:' + relaxedHeaderLine(dkimHeader);
signer = crypto.createSign(('rsa-' + hashAlgo).toUpperCase());
const signer = crypto.createSign(('rsa-' + hashAlgo).toUpperCase());
signer.update(canonicalizedHeaderData.headers);
let signature;
try {
signature = signer.sign(options.privateKey, 'base64');
} catch (_E) {
@@ -51,7 +50,7 @@ module.exports = (headers, hashAlgo, bodyHash, options) => {
module.exports.relaxedHeaders = relaxedHeaders;
function generateDKIMHeader(domainName, keySelector, fieldNames, hashAlgo, bodyHash) {
let dkim = [
const dkim = [
'v=1',
'a=rsa-' + hashAlgo,
'c=relaxed/relaxed',
@@ -66,9 +65,9 @@ function generateDKIMHeader(domainName, keySelector, fieldNames, hashAlgo, bodyH
}
function relaxedHeaders(headers, fieldNames, skipFields) {
let includedFields = new Set();
let skip = new Set();
let headerFields = new Map();
const includedFields = new Set();
const skip = new Set();
const headerFields = new Map();
(skipFields || '')
.toLowerCase()
@@ -86,15 +85,15 @@ function relaxedHeaders(headers, fieldNames, skipFields) {
});
for (let i = headers.length - 1; i >= 0; i--) {
let line = headers[i];
const 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 = [];
const headersList = [];
const fields = [];
includedFields.forEach(field => {
if (headerFields.has(field)) {
fields.push(field);

View File

@@ -52,10 +52,7 @@ const ERROR_CODES = {
};
// Export error codes as string constants and the full definitions object
module.exports = Object.keys(ERROR_CODES).reduce(
(exports, code) => {
exports[code] = code;
return exports;
},
{ ERROR_CODES }
);
module.exports = { ERROR_CODES };
for (const code of Object.keys(ERROR_CODES)) {
module.exports[code] = code;
}

View File

@@ -25,8 +25,8 @@ class Cookies {
* @param {String} url Current URL
*/
set(cookieStr, url) {
let urlparts = urllib.parse(url || '');
let cookie = this.parse(cookieStr);
const urlparts = urllib.parse(url || '');
const cookie = this.parse(cookieStr);
let domain;
if (cookie.domain) {
@@ -76,15 +76,13 @@ class Cookies {
* @returns {Array} An array of cookie objects
*/
list(url) {
let result = [];
let i;
let cookie;
const result = [];
for (i = this.cookies.length - 1; i >= 0; i--) {
cookie = this.cookies[i];
for (let i = this.cookies.length - 1; i >= 0; i--) {
const cookie = this.cookies[i];
if (this.isExpired(cookie)) {
this.cookies.splice(i, i);
this.cookies.splice(i, 1);
continue;
}
@@ -103,14 +101,14 @@ class Cookies {
* @returns {Object} Cookie object
*/
parse(cookieStr) {
let cookie = {};
const cookie = {};
(cookieStr || '')
.toString()
.split(';')
.forEach(cookiePart => {
let valueParts = cookiePart.split('=');
let key = valueParts.shift().trim().toLowerCase();
const valueParts = cookiePart.split('=');
const key = valueParts.shift().trim().toLowerCase();
let value = valueParts.join('=').trim();
let domain;
@@ -171,7 +169,7 @@ class Cookies {
* @returns {Boolean} true if cookie is valid for specifiec URL
*/
match(cookie, url) {
let urlparts = urllib.parse(url || '');
const urlparts = urllib.parse(url || '');
// check if hostname matches
// .foo.com also matches subdomains, foo.com does not
@@ -183,7 +181,7 @@ class Cookies {
}
// check if path matches
let path = this.getPath(urlparts.pathname);
const path = this.getPath(urlparts.pathname);
if (path.substr(0, cookie.path.length) !== cookie.path) {
return false;
}
@@ -202,16 +200,13 @@ class Cookies {
* @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++) {
for (let 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)) {
@@ -240,7 +235,7 @@ class Cookies {
* @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;
return a.name === b.name && a.path === b.path && a.domain === b.domain && a.secure === b.secure && a.httponly === b.httponly;
}
/**

View File

@@ -4,7 +4,7 @@ const http = require('http');
const https = require('https');
const urllib = require('url');
const zlib = require('zlib');
const PassThrough = require('stream').PassThrough;
const { PassThrough } = require('stream');
const Cookies = require('./cookies');
const packageData = require('../../package.json');
const net = require('net');
@@ -33,16 +33,16 @@ function nmfetch(url, options) {
options.cookie = false;
}
let fetchRes = options.fetchRes;
let parsed = urllib.parse(url);
const fetchRes = options.fetchRes;
const 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;
const handler = parsed.protocol === 'https:' ? https : http;
let headers = {
const headers = {
'accept-encoding': 'gzip,deflate',
'user-agent': 'nodemailer/' + packageData.version
};
@@ -90,7 +90,7 @@ function nmfetch(url, options) {
body = Buffer.from(
Object.keys(options.body)
.map(key => {
let value = options.body[key].toString().trim();
const value = options.body[key].toString().trim();
return encodeURIComponent(key) + '=' + encodeURIComponent(value);
})
.join('&')
@@ -117,7 +117,7 @@ function nmfetch(url, options) {
}
let req;
let reqOptions = {
const reqOptions = {
method,
host: parsed.hostname,
path: parsed.path,
@@ -128,9 +128,7 @@ function nmfetch(url, options) {
};
if (options.tls) {
Object.keys(options.tls).forEach(key => {
reqOptions[key] = options.tls[key];
});
Object.assign(reqOptions, options.tls);
}
if (
@@ -162,7 +160,7 @@ function nmfetch(url, options) {
}
finished = true;
req.abort();
let err = new Error('Request Timeout');
const err = new Error('Request Timeout');
err.code = errors.EFETCH;
err.sourceUrl = url;
fetchRes.emit('error', err);
@@ -204,7 +202,7 @@ function nmfetch(url, options) {
options.redirects++;
if (options.redirects > options.maxRedirects) {
finished = true;
let err = new Error('Maximum redirect count exceeded');
const err = new Error('Maximum redirect count exceeded');
err.code = errors.EFETCH;
err.sourceUrl = url;
fetchRes.emit('error', err);
@@ -222,7 +220,7 @@ function nmfetch(url, options) {
if (res.statusCode >= 300 && !options.allowErrorResponse) {
finished = true;
let err = new Error('Invalid status code ' + res.statusCode);
const err = new Error('Invalid status code ' + res.statusCode);
err.code = errors.EFETCH;
err.sourceUrl = url;
fetchRes.emit('error', err);
@@ -263,9 +261,8 @@ function nmfetch(url, options) {
try {
if (typeof body.pipe === 'function') {
return body.pipe(req);
} else {
req.write(body);
}
req.write(body);
} catch (err) {
finished = true;
err.code = errors.EFETCH;

View File

@@ -13,7 +13,7 @@ class JSONTransport {
constructor(options) {
options = options || {};
this.options = options || {};
this.options = options;
this.name = 'JSONTransport';
this.version = packageData.version;
@@ -33,10 +33,10 @@ class JSONTransport {
// Sendmail strips this header line by itself
mail.message.keepBcc = true;
let envelope = mail.data.envelope || mail.message.getEnvelope();
let messageId = mail.message.messageId();
const envelope = mail.data.envelope || mail.message.getEnvelope();
const messageId = mail.message.messageId();
let recipients = [].concat(envelope.to || []);
const recipients = [].concat(envelope.to || []);
if (recipients.length > 3) {
recipients.push('...and ' + recipients.splice(2).length + ' more');
}

View File

@@ -4,7 +4,7 @@
const MimeNode = require('../mime-node');
const mimeFuncs = require('../mime-funcs');
const parseDataURI = require('../shared').parseDataURI;
const { parseDataURI } = require('../shared');
/**
* Creates the object for composing a MimeNode instance out from the mail options
@@ -59,7 +59,7 @@ class MailComposer {
// 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());
const key = header.replace(/-(\w)/g, (o, c) => c.toUpperCase());
if (this.mail[key]) {
this.message.setHeader(header, this.mail[key]);
}
@@ -84,20 +84,18 @@ class MailComposer {
*/
getAttachments(findRelated) {
let icalEvent, eventObject;
let attachments = [].concat(this.mail.attachments || []).map((attachment, i) => {
let data;
const attachments = [].concat(this.mail.attachments || []).map((attachment, i) => {
if (/^data:/i.test(attachment.path || attachment.href)) {
attachment = this._processDataUrl(attachment);
}
let contentType =
const contentType =
attachment.contentType || mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin');
let isImage = /^image\//i.test(contentType);
let isMessageNode = /^message\//i.test(contentType);
const isImage = /^image\//i.test(contentType);
const isMessageNode = /^message\//i.test(contentType);
let contentDisposition =
const contentDisposition =
attachment.contentDisposition || (isMessageNode || (isImage && attachment.cid) ? 'inline' : 'attachment');
let contentTransferEncoding;
@@ -111,7 +109,7 @@ class MailComposer {
contentTransferEncoding = 'base64'; // the default
}
data = {
const data = {
contentType,
contentDisposition,
contentTransferEncoding
@@ -173,10 +171,7 @@ class MailComposer {
};
}
eventObject = {};
Object.keys(icalEvent).forEach(key => {
eventObject[key] = icalEvent[key];
});
eventObject = Object.assign({}, icalEvent);
eventObject.contentType = 'application/ics';
if (!eventObject.headers) {
@@ -192,12 +187,12 @@ class MailComposer {
attached: attachments.concat(eventObject || []),
related: []
};
} else {
return {
attached: attachments.filter(attachment => !attachment.cid).concat(eventObject || []),
related: attachments.filter(attachment => !!attachment.cid)
};
}
return {
attached: attachments.filter(attachment => !attachment.cid).concat(eventObject || []),
related: attachments.filter(attachment => !!attachment.cid)
};
}
/**
@@ -206,13 +201,8 @@ class MailComposer {
* @returns {Array} An array of alternative elements. Includes the `text` and `html` values as well
*/
getAlternatives() {
let alternatives = [],
text,
html,
watchHtml,
amp,
icalEvent,
eventObject;
const alternatives = [];
let text, html, watchHtml, amp, icalEvent, eventObject;
if (this.mail.text) {
if (
@@ -269,10 +259,7 @@ class MailComposer {
};
}
eventObject = {};
Object.keys(icalEvent).forEach(key => {
eventObject[key] = icalEvent[key];
});
eventObject = Object.assign({}, icalEvent);
if (eventObject.content && typeof eventObject.content === 'object') {
// we are going to have the same attachment twice, so mark this to be
@@ -310,13 +297,11 @@ class MailComposer {
.concat(eventObject || [])
.concat(this.mail.alternatives || [])
.forEach(alternative => {
let data;
if (/^data:/i.test(alternative.path || alternative.href)) {
alternative = this._processDataUrl(alternative);
}
data = {
const data = {
contentType:
alternative.contentType ||
mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
@@ -368,26 +353,22 @@ class MailComposer {
* @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,
newline: this.mail.newline
});
} else {
node = parentNode.createChild('multipart/mixed', {
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
});
}
const node = parentNode
? parentNode.createChild('multipart/mixed', {
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
})
: 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,
newline: this.mail.newline
});
if (this._useAlternative) {
this._createAlternative(node);
@@ -416,26 +397,22 @@ class MailComposer {
* @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,
newline: this.mail.newline
});
} else {
node = parentNode.createChild('multipart/alternative', {
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
});
}
const node = parentNode
? parentNode.createChild('multipart/alternative', {
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
})
: 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,
newline: this.mail.newline
});
this._alternatives.forEach(alternative => {
if (this._useRelated && this._htmlNode === alternative) {
@@ -455,26 +432,22 @@ class MailComposer {
* @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,
newline: this.mail.newline
});
} else {
node = parentNode.createChild('multipart/related; type="text/html"', {
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
});
}
const node = parentNode
? parentNode.createChild('multipart/related; type="text/html"', {
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
})
: 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,
newline: this.mail.newline
});
this._createContentNode(node, this._htmlNode);
@@ -494,33 +467,30 @@ class MailComposer {
element = element || {};
element.content = element.content || '';
let node;
let encoding = (element.encoding || 'utf8')
const 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,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
});
} else {
node = parentNode.createChild(element.contentType, {
filename: element.filename,
textEncoding: this.mail.textEncoding,
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
});
}
const node = parentNode
? parentNode.createChild(element.contentType, {
filename: element.filename,
textEncoding: this.mail.textEncoding,
disableUrlAccess: this.mail.disableUrlAccess,
disableFileAccess: this.mail.disableFileAccess,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
})
: 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,
normalizeHeaderKey: this.mail.normalizeHeaderKey,
newline: this.mail.newline
});
// add custom headers
if (element.headers) {

View File

@@ -106,17 +106,17 @@ class Mail extends EventEmitter {
this.getSocket = false;
}
return this.transporter[method](...args);
} else {
this.logger.warn(
{
tnx: 'transport',
methodName: method
},
'Non existing method %s called for transport',
method
);
return false;
}
this.logger.warn(
{
tnx: 'transport',
methodName: method
},
'Non existing method %s called for transport',
method
);
return false;
};
});
@@ -157,7 +157,7 @@ class Mail extends EventEmitter {
this.getSocket = false;
}
let mail = new MailMessage(this, data);
const mail = new MailMessage(this, data);
this.logger.debug(
{
@@ -207,7 +207,7 @@ class Mail extends EventEmitter {
if (mail.data.dkim || this.dkim) {
mail.message.processFunc(input => {
let dkim = mail.data.dkim ? new DKIM(mail.data.dkim) : this.dkim;
const dkim = mail.data.dkim ? new DKIM(mail.data.dkim) : this.dkim;
this.logger.debug(
{
tnx: 'DKIM',
@@ -259,8 +259,8 @@ class Mail extends EventEmitter {
return callback();
}
let userPlugins = this._userPlugins[step] || [];
let defaultPlugins = this._defaultPlugins[step] || [];
const userPlugins = this._userPlugins[step] || [];
const defaultPlugins = this._defaultPlugins[step] || [];
if (userPlugins.length) {
this.logger.debug(
@@ -281,7 +281,7 @@ class Mail extends EventEmitter {
let pos = 0;
let block = 'default';
let processPlugins = () => {
const processPlugins = () => {
let curplugins = block === 'default' ? defaultPlugins : userPlugins;
if (pos >= curplugins.length) {
if (block === 'default' && userPlugins.length) {
@@ -292,7 +292,7 @@ class Mail extends EventEmitter {
return callback();
}
}
let plugin = curplugins[pos++];
const plugin = curplugins[pos++];
plugin(mail, err => {
if (err) {
return callback(err);
@@ -310,11 +310,11 @@ class Mail extends EventEmitter {
* @param {String} proxyUrl Proxy configuration url
*/
setupProxy(proxyUrl) {
let proxy = urllib.parse(proxyUrl);
const proxy = urllib.parse(proxyUrl);
// setup socket handler for the mailer object
this.getSocket = (options, callback) => {
let protocol = proxy.protocol.replace(/:$/, '').toLowerCase();
const protocol = proxy.protocol.replace(/:$/, '').toLowerCase();
if (this.meta.has('proxy_handler_' + protocol)) {
return this.meta.get('proxy_handler_' + protocol)(proxy, options, callback);
@@ -342,11 +342,11 @@ class Mail extends EventEmitter {
err.code = errors.EPROXY;
return callback(err);
}
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 = {
const connect = ipaddress => {
const proxyV2 = !!this.meta.get('proxy_socks_module').SocksClient;
const socksClient = proxyV2 ? this.meta.get('proxy_socks_module').SocksClient : this.meta.get('proxy_socks_module');
const proxyType = Number(proxy.protocol.replace(/\D/g, '')) || 5;
const connectionOpts = {
proxy: {
ipaddress,
port: Number(proxy.port),
@@ -360,8 +360,8 @@ class Mail extends EventEmitter {
};
if (proxy.auth) {
let username = decodeURIComponent(proxy.auth.split(':').shift());
let password = decodeURIComponent(proxy.auth.split(':').pop());
const username = decodeURIComponent(proxy.auth.split(':').shift());
const password = decodeURIComponent(proxy.auth.split(':').pop());
if (proxyV2) {
connectionOpts.proxy.userId = username;
connectionOpts.proxy.password = password;
@@ -415,7 +415,7 @@ class Mail extends EventEmitter {
html = (html || '')
.toString()
.replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
let cid = crypto.randomBytes(10).toString('hex') + '@localhost';
const cid = crypto.randomBytes(10).toString('hex') + '@localhost';
if (!mail.data.attachments) {
mail.data.attachments = [];
}

View File

@@ -11,12 +11,10 @@ class MailMessage {
this.message = null;
data = data || {};
let options = mailer.options || {};
let defaults = mailer._defaults || {};
const options = mailer.options || {};
const defaults = mailer._defaults || {};
Object.keys(data).forEach(key => {
this.data[key] = data[key];
});
Object.assign(this.data, data);
this.data.headers = this.data.headers || {};
@@ -47,7 +45,7 @@ class MailMessage {
}
resolveAll(callback) {
let keys = [
const keys = [
[this.data, 'html'],
[this.data, 'text'],
[this.data, 'watchHtml'],
@@ -79,9 +77,9 @@ class MailMessage {
});
}
let mimeNode = new MimeNode();
const mimeNode = new MimeNode();
let addressKeys = ['from', 'to', 'cc', 'bcc', 'sender', 'replyTo'];
const addressKeys = ['from', 'to', 'cc', 'bcc', 'sender', 'replyTo'];
addressKeys.forEach(address => {
let value;
@@ -97,7 +95,7 @@ class MailMessage {
}
});
let singleKeys = ['from', 'sender'];
const singleKeys = ['from', 'sender'];
singleKeys.forEach(address => {
if (this.data[address]) {
this.data[address] = this.data[address].shift();
@@ -105,11 +103,11 @@ class MailMessage {
});
let pos = 0;
let resolveNext = () => {
const resolveNext = () => {
if (pos >= keys.length) {
return callback(null, this.data);
}
let args = keys[pos++];
const args = keys[pos++];
if (!args[0] || !args[0][args[1]]) {
return resolveNext();
}
@@ -118,7 +116,7 @@ class MailMessage {
return callback(err);
}
let node = {
const node = {
content: value
};
if (args[0][args[1]] && typeof args[0][args[1]] === 'object' && !Buffer.isBuffer(args[0][args[1]])) {
@@ -138,8 +136,8 @@ class MailMessage {
}
normalize(callback) {
let envelope = this.data.envelope || this.message.getEnvelope();
let messageId = this.message.messageId();
const envelope = this.data.envelope || this.message.getEnvelope();
const messageId = this.message.messageId();
this.resolveAll((err, data) => {
if (err) {
@@ -195,7 +193,7 @@ class MailMessage {
});
if (data.list && typeof data.list === 'object') {
let listHeaders = this._getListHeaders(data.list);
const listHeaders = this._getListHeaders(data.list);
listHeaders.forEach(entry => {
data.normalizedHeaders[entry.key] = entry.value.map(val => (val && val.value) || val).join(', ');
});
@@ -245,13 +243,11 @@ class MailMessage {
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);
});
this._getListHeaders(this.data.list).forEach(listHeader => {
listHeader.value.forEach(value => {
this.message.addHeader(listHeader.key, value);
});
}
});
}
_getListHeaders(listData) {

View File

@@ -15,11 +15,7 @@ module.exports = {
*/
isPlainText(value, isParam) {
const re = isParam ? /[\x00-\x08\x0b\x0c\x0e-\x1f"\u0080-\uFFFF]/ : /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/;
if (typeof value !== 'string' || re.test(value)) {
return false;
} else {
return true;
}
return typeof value === 'string' && !re.test(value);
},
/**
@@ -54,7 +50,7 @@ module.exports = {
maxLength = maxLength || 0;
let encodedStr;
let toCharset = 'UTF-8';
const toCharset = 'UTF-8';
if (maxLength && maxLength > 7 + toCharset.length) {
maxLength -= 7 + toCharset.length;
@@ -63,12 +59,11 @@ module.exports = {
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();
const ord = chr.charCodeAt(0).toString(16).toUpperCase();
if (chr === ' ') {
return '_';
} else {
return '=' + (ord.length === 1 ? '0' + ord : ord);
}
return '=' + (ord.length === 1 ? '0' + ord : ord);
});
} else if (mimeWordEncoding === 'B') {
encodedStr = typeof data === 'string' ? data : base64.encode(data);
@@ -80,7 +75,7 @@ module.exports = {
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 = [];
const parts = [];
let lpart = '';
for (let i = 0, len = encodedStr.length; i < len; i++) {
let chr = encodedStr.charAt(i);
@@ -129,42 +124,38 @@ module.exports = {
encodeWords(value, mimeWordEncoding, maxLength, encodeAll) {
maxLength = maxLength || 0;
let encodedValue;
// find first word with a non-printable ascii or special symbol in it
let firstMatch = value.match(/(?:^|\s)([^\s]*["\u0080-\uFFFF])/);
const 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]*$/);
const lastMatch = value.match(/(["\u0080-\uFFFF][^\s]*)[^"\u0080-\uFFFF]*$/);
if (!lastMatch) {
// should not happen
return value;
}
let startIndex =
const startIndex =
firstMatch.index +
(
firstMatch[0].match(/[^\s]/) || {
index: 0
}
).index;
let endIndex = lastMatch.index + (lastMatch[1] || '').length;
const endIndex = lastMatch.index + (lastMatch[1] || '').length;
encodedValue =
return (
(startIndex ? value.substr(0, startIndex) : '') +
this.encodeWord(value.substring(startIndex, endIndex), mimeWordEncoding || 'Q', maxLength) +
(endIndex < value.length ? value.substr(endIndex) : '');
return encodedValue;
(endIndex < value.length ? value.substr(endIndex) : '')
);
},
/**
@@ -175,12 +166,12 @@ module.exports = {
* @return {String} joined header value
*/
buildHeaderValue(structured) {
let paramsArray = [];
const 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];
const value = structured.params[param];
if (!this.isPlainText(value, true) || value.length >= 75) {
this.buildHeaderParam(param, value, 50).forEach(encodedParam => {
if (!/[\s"\\;:/=(),<>@[\]?]|^[-']|'$/.test(encodedParam.value) || encodedParam.key.substr(-1) === '*') {
@@ -215,9 +206,8 @@ module.exports = {
* @return {Array} A list of encoded keys and headers
*/
buildHeaderParam(key, data, maxLength) {
let list = [];
const list = [];
let encodedStr = typeof data === 'string' ? data : (data || '').toString();
let encodedStrArr;
let chr, ord;
let line;
let startPos = 0;
@@ -252,7 +242,7 @@ module.exports = {
} else {
if (/[\uD800-\uDBFF]/.test(encodedStr)) {
// string containts surrogate pairs, so normalize it to an array of bytes
encodedStrArr = [];
const encodedStrArr = [];
for (i = 0, len = encodedStr.length; i < len; i++) {
chr = encodedStr.charAt(i);
ord = chr.charCodeAt(0);
@@ -356,7 +346,7 @@ module.exports = {
* @return {Object} Header value as a parsed structure
*/
parseHeaderValue(str) {
let response = {
const response = {
value: false,
params: {}
};
@@ -458,12 +448,11 @@ module.exports = {
value
// fix invalidly encoded chars
.replace(/[=?_\s]/g, s => {
let c = s.charCodeAt(0).toString(16);
const c = s.charCodeAt(0).toString(16);
if (s === ' ') {
return '_';
} else {
return '%' + (c.length < 2 ? '0' : '') + c;
}
return '%' + (c.length < 2 ? '0' : '') + c;
})
// change from urlencoding to percent encoding
.replace(/%/g, '=') +
@@ -508,11 +497,10 @@ module.exports = {
str = (str || '').toString();
lineLength = lineLength || 76;
let pos = 0,
len = str.length,
result = '',
line,
match;
let pos = 0;
const len = str.length;
let result = '';
let line, match;
while (pos < len) {
line = str.substr(pos, lineLength);
@@ -549,11 +537,8 @@ module.exports = {
* @return {Array} Split string
*/
splitMimeEncodedString: (str, maxlen) => {
let curLine,
match,
chr,
done,
lines = [];
const lines = [];
let curLine, match, chr, done;
// require at least 12 symbols to fit possible 4 octet UTF-8 sequences
maxlen = Math.max(maxlen || 0, 12);

View File

@@ -2073,13 +2073,9 @@ module.exports = {
return defaultMimeType;
}
let parsed = path.parse(filename);
let extension = (parsed.ext.substr(1) || parsed.name || '').split('?').shift().trim().toLowerCase();
let value = defaultMimeType;
if (extensions.has(extension)) {
value = extensions.get(extension);
}
const parsed = path.parse(filename);
const extension = (parsed.ext.substr(1) || parsed.name || '').split('?').shift().trim().toLowerCase();
const value = extensions.has(extension) ? extensions.get(extension) : defaultMimeType;
if (Array.isArray(value)) {
return value[0];
@@ -2091,12 +2087,12 @@ module.exports = {
if (!mimeType) {
return defaultExtension;
}
let parts = (mimeType || '').toLowerCase().trim().split('/');
let rootType = parts.shift().trim();
let subType = parts.join('/').trim();
const parts = (mimeType || '').toLowerCase().trim().split('/');
const rootType = parts.shift().trim();
const subType = parts.join('/').trim();
if (mimeTypes.has(rootType + '/' + subType)) {
let value = mimeTypes.get(rootType + '/' + subType);
const value = mimeTypes.get(rootType + '/' + subType);
if (Array.isArray(value)) {
return value[0];
}

View File

@@ -5,7 +5,7 @@
const crypto = require('crypto');
const fs = require('fs');
const punycode = require('../punycode');
const PassThrough = require('stream').PassThrough;
const { PassThrough } = require('stream');
const shared = require('../shared');
const mimeFuncs = require('../mime-funcs');
@@ -19,6 +19,8 @@ const LastNewline = require('./last-newline');
const LeWindows = require('./le-windows');
const LeUnix = require('./le-unix');
const FORMATTED_HEADERS = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
/**
* Creates a new mime tree node. Assumes 'multipart/*' as the content type
* if it is a branch, anything else counts as leaf. If rootNode is missing from
@@ -54,7 +56,7 @@ class MimeNode {
/**
* If date headers is missing and current node is the root, this value is used instead
*/
this.date = new Date();
this.date = options.parentNode ? null : new Date();
/**
* Root node for current mime tree
@@ -175,7 +177,7 @@ class MimeNode {
options = contentType;
contentType = undefined;
}
let node = new MimeNode(contentType, options);
const node = new MimeNode(contentType, options);
this.appendChild(node);
return node;
}
@@ -256,8 +258,7 @@ class MimeNode {
* @return {Object} current node
*/
setHeader(key, value) {
let added = false,
headerValue;
let added = false;
// Allow setting multiple headers at once
if (!value && key && typeof key === 'object') {
@@ -280,7 +281,7 @@ class MimeNode {
key = this._normalizeHeaderKey(key);
headerValue = {
const headerValue = {
key,
value
};
@@ -404,8 +405,8 @@ class MimeNode {
});
}
let stream = this.createReadStream();
let buf = [];
const stream = this.createReadStream();
const buf = [];
let buflen = 0;
let returned = false;
@@ -445,7 +446,7 @@ class MimeNode {
getTransferEncoding() {
let transferEncoding = false;
let contentType = (this.getHeader('Content-Type') || '').toString().toLowerCase().trim();
const contentType = (this.getHeader('Content-Type') || '').toString().toLowerCase().trim();
if (this.content) {
transferEncoding = (this.getHeader('Content-Transfer-Encoding') || '').toString().toLowerCase().trim();
@@ -475,8 +476,8 @@ class MimeNode {
* @returns {String} Headers
*/
buildHeaders() {
let transferEncoding = this.getTransferEncoding();
let headers = [];
const transferEncoding = this.getTransferEncoding();
const headers = [];
if (transferEncoding) {
this.setHeader('Content-Transfer-Encoding', transferEncoding);
@@ -501,7 +502,7 @@ class MimeNode {
// Ensure that Content-Type is the last header for the root node
for (let i = this._headers.length - 2; i >= 0; i--) {
let header = this._headers[i];
const header = this._headers[i];
if (header.key === 'Content-Type') {
this._headers.splice(i, 1);
this._headers.push(header);
@@ -514,8 +515,8 @@ class MimeNode {
let value = header.value;
let structured;
let param;
let options = {};
let formattedHeaders = ['From', 'Sender', 'To', 'Cc', 'Bcc', 'Reply-To', 'Date', 'References'];
const options = {};
const formattedHeaders = FORMATTED_HEADERS;
if (value && typeof value === 'object' && !formattedHeaders.includes(key)) {
Object.keys(value).forEach(key => {
@@ -593,7 +594,7 @@ class MimeNode {
}
if (typeof this.normalizeHeaderKey === 'function') {
let normalized = this.normalizeHeaderKey(key, value);
const normalized = this.normalizeHeaderKey(key, value);
if (normalized && typeof normalized === 'string' && normalized.length) {
key = normalized;
}
@@ -614,7 +615,7 @@ class MimeNode {
createReadStream(options) {
options = options || {};
let stream = new PassThrough(options);
const stream = new PassThrough(options);
let outputStream = stream;
let transform;
@@ -682,13 +683,13 @@ class MimeNode {
}
stream(outputStream, options, done) {
let transferEncoding = this.getTransferEncoding();
const transferEncoding = this.getTransferEncoding();
let contentStream;
let localStream;
// protect actual callback against multiple triggering
let returned = false;
let callback = err => {
const callback = err => {
if (returned) {
return;
}
@@ -698,14 +699,14 @@ class MimeNode {
// for multipart nodes, push child nodes
// for content nodes end the stream
let finalize = () => {
const finalize = () => {
let childId = 0;
let processChildNode = () => {
const processChildNode = () => {
if (childId >= this.childNodes.length) {
outputStream.write('\r\n--' + this.boundary + '--\r\n');
return callback();
}
let child = this.childNodes[childId++];
const child = this.childNodes[childId++];
outputStream.write((childId > 1 ? '\r\n' : '') + '--' + this.boundary + '\r\n');
child.stream(outputStream, options, err => {
if (err) {
@@ -723,7 +724,7 @@ class MimeNode {
};
// pushes node content
let sendContent = () => {
const sendContent = () => {
if (this.content) {
if (Object.prototype.toString.call(this.content) === '[object Error]') {
// content is already errored
@@ -736,7 +737,7 @@ class MimeNode {
this.content.once('error', this._contentErrorHandler);
}
let createStream = () => {
const createStream = () => {
if (['quoted-printable', 'base64'].includes(transferEncoding)) {
contentStream = new (transferEncoding === 'base64' ? base64 : qp).Encoder(options);
@@ -761,10 +762,10 @@ class MimeNode {
};
if (this.content._resolve) {
let chunks = [];
const chunks = [];
let chunklen = 0;
let returned = false;
let sourceStream = this._getStream(this.content);
const sourceStream = this._getStream(this.content);
sourceStream.on('error', err => {
if (returned) {
return;
@@ -792,9 +793,8 @@ class MimeNode {
setImmediate(createStream);
}
return;
} else {
return setImmediate(finalize);
}
return setImmediate(finalize);
};
if (this._raw) {
@@ -809,7 +809,7 @@ class MimeNode {
this._raw.removeListener('error', this._contentErrorHandler);
}
let raw = this._getStream(this._raw);
const raw = this._getStream(this._raw);
raw.pipe(outputStream, {
end: false
});
@@ -851,7 +851,7 @@ class MimeNode {
this._envelope.to = this._envelope.to.map(to => to.address).filter(address => address);
let standardFields = ['to', 'cc', 'bcc', 'from'];
const standardFields = ['to', 'cc', 'bcc', 'from'];
Object.keys(envelope).forEach(key => {
if (!standardFields.includes(key)) {
this._envelope[key] = envelope[key];
@@ -867,10 +867,10 @@ class MimeNode {
* @return {Object} Address object
*/
getAddresses() {
let addresses = {};
const addresses = {};
this._headers.forEach(header => {
let key = header.key.toLowerCase();
const key = header.key.toLowerCase();
if (['from', 'sender', 'reply-to', 'to', 'cc', 'bcc'].includes(key)) {
if (!Array.isArray(addresses[key])) {
addresses[key] = [];
@@ -893,12 +893,12 @@ class MimeNode {
return this._envelope;
}
let envelope = {
const envelope = {
from: false,
to: []
};
this._headers.forEach(header => {
let list = [];
const list = [];
if (header.key === 'From' || (!envelope.from && ['Reply-To', 'Sender'].includes(header.key))) {
this._convertAddresses(this._parseAddresses(header.value), list);
if (list.length && list[0]) {
@@ -974,14 +974,18 @@ class MimeNode {
});
return contentStream;
} else if (typeof content.pipe === 'function') {
}
if (typeof content.pipe === 'function') {
// assume as stream
return content;
} else if (content && typeof content.path === 'string' && !content.href) {
}
if (content && typeof content.path === 'string' && !content.href) {
if (this.disableFileAccess) {
contentStream = new PassThrough();
setImmediate(() => {
let err = new Error('File access rejected for ' + content.path);
const err = new Error('File access rejected for ' + content.path);
err.code = errors.EFILEACCESS;
contentStream.emit('error', err);
});
@@ -989,11 +993,13 @@ class MimeNode {
}
// read file
return fs.createReadStream(content.path);
} else if (content && typeof content.href === 'string') {
}
if (content && typeof content.href === 'string') {
if (this.disableUrlAccess) {
contentStream = new PassThrough();
setImmediate(() => {
let err = new Error('Url access rejected for ' + content.href);
const err = new Error('Url access rejected for ' + content.href);
err.code = errors.EURLACCESS;
contentStream.emit('error', err);
});
@@ -1001,19 +1007,19 @@ class MimeNode {
}
// fetch URL
return nmfetch(content.href, { headers: content.httpHeaders });
} else {
// pass string or buffer content as a stream
contentStream = new PassThrough();
setImmediate(() => {
try {
contentStream.end(content || '');
} catch (_err) {
contentStream.emit('error', _err);
}
});
return contentStream;
}
// pass string or buffer content as a stream
contentStream = new PassThrough();
setImmediate(() => {
try {
contentStream.end(content || '');
} catch (_err) {
contentStream.emit('error', _err);
}
});
return contentStream;
}
/**
@@ -1172,7 +1178,7 @@ class MimeNode {
* @return {String} address string
*/
_convertAddresses(addresses, uniqueList) {
let values = [];
const values = [];
uniqueList = uniqueList || [];
@@ -1182,17 +1188,15 @@ class MimeNode {
if (!address.name) {
values.push(address.address.indexOf(' ') >= 0 ? `<${address.address}>` : `${address.address}`);
} else if (address.name) {
} else {
values.push(`${this._encodeAddressName(address.name)} <${address.address}>`);
}
if (address.address) {
if (!uniqueList.filter(a => a.address === address.address).length) {
uniqueList.push(address);
}
if (!uniqueList.some(a => a.address === address.address)) {
uniqueList.push(address);
}
} else if (address.group) {
let groupListAddresses = (address.group.length ? this._convertAddresses(address.group, uniqueList) : '').trim();
const groupListAddresses = (address.group.length ? this._convertAddresses(address.group, uniqueList) : '').trim();
values.push(`${this._encodeAddressName(address.name)}:${groupListAddresses};`);
}
});
@@ -1212,26 +1216,30 @@ class MimeNode {
.replace(/[\x00-\x1F<>]+/g, ' ') // remove unallowed characters
.trim();
let lastAt = address.lastIndexOf('@');
const lastAt = address.lastIndexOf('@');
if (lastAt < 0) {
// Bare username
return address;
}
let user = address.substr(0, lastAt);
let domain = address.substr(lastAt + 1);
const domain = address.substr(lastAt + 1);
// Usernames are not touched and are kept as is even if these include unicode
// Domains are punycoded by default
// 'jõgeva.ee' will be converted to 'xn--jgeva-dua.ee'
// non-unicode domains are left as is
// Usernames are not touched and are kept as is even if these include unicode.
// Domains are punycoded when the local part is ASCII ('safe@jõgeva.ee' -> 'safe@xn--jgeva-dua.ee').
// When the local part contains non-ASCII bytes the address already requires SMTPUTF8,
// so the domain is kept (or decoded back) as UTF-8 for symmetry on both sides of '@'.
let encodedDomain;
let encodedDomain = domain;
try {
encodedDomain = punycode.toASCII(domain.toLowerCase());
if (/[\x80-\uFFFF]/.test(user)) {
encodedDomain = punycode.toUnicode(domain.toLowerCase());
} else {
encodedDomain = punycode.toASCII(domain.toLowerCase());
}
} catch (_err) {
// keep as is?
// keep domain as supplied
}
if (user.indexOf(' ') >= 0) {
@@ -1285,20 +1293,25 @@ class MimeNode {
_getTextEncoding(value) {
value = (value || '').toString();
let encoding = this.textEncoding;
let latinLen;
let nonLatinLen;
if (!encoding) {
// count latin alphabet symbols and 8-bit range symbols + control symbols
// if there are more latin characters, then use quoted-printable
// encoding, otherwise use base64
nonLatinLen = (value.match(/[\x00-\x08\x0B\x0C\x0E-\x1F\u0080-\uFFFF]/g) || []).length;
latinLen = (value.match(/[a-z]/gi) || []).length;
// if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
encoding = nonLatinLen < latinLen ? 'Q' : 'B';
if (this.textEncoding) {
return this.textEncoding;
}
return encoding;
// count latin alphabet symbols and 8-bit range symbols + control symbols
// if there are more latin characters, then use quoted-printable
// encoding, otherwise use base64
let nonLatinLen = 0;
let latinLen = 0;
for (let i = 0, len = value.length; i < len; i++) {
const code = value.charCodeAt(i);
if ((code >= 0x00 && code <= 0x08) || code === 0x0b || code === 0x0c || (code >= 0x0e && code <= 0x1f) || code >= 0x80) {
nonLatinLen++;
} else if ((code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a)) {
latinLen++;
}
}
// if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
return nonLatinLen < latinLen ? 'Q' : 'B';
}
/**

View File

@@ -1,6 +1,6 @@
'use strict';
const Transform = require('stream').Transform;
const { Transform } = require('stream');
class LastNewline extends Transform {
constructor() {

View File

@@ -1,18 +1,15 @@
'use strict';
const stream = require('stream');
const Transform = stream.Transform;
const { Transform } = require('stream');
/**
* Ensures that only <LF> is used for linebreaks
*
* @param {Object} options Stream options
*/
class LeWindows extends Transform {
class LeUnix extends Transform {
constructor(options) {
super(options);
// init Transform
this.options = options || {};
}
/**
@@ -24,7 +21,7 @@ class LeWindows extends Transform {
for (let i = 0, len = chunk.length; i < len; i++) {
if (chunk[i] === 0x0d) {
// \n
// \r
buf = chunk.slice(lastPos, i);
lastPos = i + 1;
this.push(buf);
@@ -40,4 +37,4 @@ class LeWindows extends Transform {
}
}
module.exports = LeWindows;
module.exports = LeUnix;

View File

@@ -1,7 +1,6 @@
'use strict';
const stream = require('stream');
const Transform = stream.Transform;
const { Transform } = require('stream');
/**
* Ensures that only <CR><LF> sequences are used for linebreaks
@@ -11,8 +10,6 @@ const Transform = stream.Transform;
class LeWindows extends Transform {
constructor(options) {
super(options);
// init Transform
this.options = options || {};
this.lastByte = false;
}

View File

@@ -20,9 +20,7 @@ const ETHEREAL_CACHE = ['true', 'yes', 'y', '1'].includes((process.env.ETHEREAL_
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
@@ -30,7 +28,8 @@ module.exports.createTransport = function (transporter, defaults) {
// provided transporter looks like a connection url
(typeof transporter === 'string' && /^(smtps?|direct):/i.test(transporter))
) {
if ((urlConfig = typeof transporter === 'string' ? transporter : transporter.url)) {
const urlConfig = typeof transporter === 'string' ? transporter : transporter.url;
if (urlConfig) {
// parse a configuration URL into configuration options
options = shared.parseConnectionUrl(urlConfig);
} else {
@@ -47,7 +46,7 @@ module.exports.createTransport = function (transporter, defaults) {
transporter = new JSONTransport(options);
} else if (options.SES) {
if (options.SES.ses && options.SES.aws) {
let error = new Error(
const error = new Error(
'Using legacy SES configuration, expecting @aws-sdk/client-sesv2, see https://nodemailer.com/transports/ses/'
);
error.code = errors.ECONFIG;
@@ -59,9 +58,7 @@ module.exports.createTransport = function (transporter, defaults) {
}
}
mailer = new Mailer(transporter, options, defaults);
return mailer;
return new Mailer(transporter, options, defaults);
};
module.exports.createTestAccount = function (apiUrl, callback) {
@@ -85,11 +82,11 @@ module.exports.createTestAccount = function (apiUrl, callback) {
apiUrl = apiUrl || ETHEREAL_API;
let chunks = [];
const chunks = [];
let chunklen = 0;
let requestHeaders = {};
let requestBody = {
const requestHeaders = {};
const requestBody = {
requestor: packageData.name,
version: packageData.version
};
@@ -98,7 +95,7 @@ module.exports.createTestAccount = function (apiUrl, callback) {
requestHeaders.Authorization = 'Bearer ' + ETHEREAL_API_KEY;
}
let req = nmfetch(apiUrl + '/user', {
const req = nmfetch(apiUrl + '/user', {
contentType: 'application/json',
method: 'POST',
headers: requestHeaders,
@@ -116,16 +113,12 @@ module.exports.createTestAccount = function (apiUrl, callback) {
req.once('error', err => callback(err));
req.once('end', () => {
let res = Buffer.concat(chunks, chunklen);
const res = Buffer.concat(chunks, chunklen);
let data;
let err;
try {
data = JSON.parse(res.toString());
} catch (E) {
err = E;
}
if (err) {
return callback(err);
return callback(E);
}
if (data.status !== 'success' || data.error) {
return callback(new Error(data.error || 'Request failed'));
@@ -143,7 +136,7 @@ module.exports.getTestMessageUrl = function (info) {
return false;
}
let infoProps = new Map();
const infoProps = new Map();
info.response.replace(/\[([^\]]+)\]$/, (m, props) => {
props.replace(/\b([A-Z0-9]+)=([^\s]+)/g, (m, key, value) => {
infoProps.set(key, value);

View File

@@ -1,6 +1,6 @@
'use strict';
const Transform = require('stream').Transform;
const { Transform } = require('stream');
/**
* Encodes a Buffer into a Quoted-Printable encoded string
@@ -8,20 +8,21 @@ const Transform = require('stream').Transform;
* @param {Buffer} buffer Buffer to convert
* @returns {String} Quoted-Printable encoded string
*/
// usable characters that do not need encoding
// https://tools.ietf.org/html/rfc2045#section-6.7
const QP_RANGES = [
[0x09], // <TAB>
[0x0a], // <LF>
[0x0d], // <CR>
[0x20, 0x3c], // <SP>!"#$%&'()*+,-./0123456789:;
[0x3e, 0x7e] // >?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}
];
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;
@@ -29,7 +30,7 @@ function encode(buffer) {
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) &&
checkRanges(ord, QP_RANGES) &&
!((ord === 0x20 || ord === 0x09) && (i === len - 1 || buffer[i + 1] === 0x0a || buffer[i + 1] === 0x0d))
) {
result += String.fromCharCode(ord);
@@ -57,9 +58,9 @@ function wrap(str, lineLength) {
}
let pos = 0;
let len = str.length;
const len = str.length;
let match, code, line;
let lineMargin = Math.floor(lineLength / 3);
const lineMargin = Math.floor(lineLength / 3);
let result = '';
// insert soft linebreaks where needed
@@ -73,17 +74,20 @@ function wrap(str, lineLength) {
}
if (line.substr(-1) === '\n') {
// nothing to change here
result += line;
pos += line.length;
continue;
} else if ((match = line.substr(-lineMargin).match(/\n.*?$/))) {
}
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.,!?]*$/))) {
}
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)) {
@@ -139,13 +143,14 @@ function wrap(str, lineLength) {
*/
function checkRanges(nr, ranges) {
for (let i = ranges.length - 1; i >= 0; i--) {
if (!ranges[i].length) {
const range = ranges[i];
if (!range.length) {
continue;
}
if (ranges[i].length === 1 && nr === ranges[i][0]) {
if (range.length === 1 && nr === range[0]) {
return true;
}
if (ranges[i].length === 2 && nr >= ranges[i][0] && nr <= ranges[i][1]) {
if (range.length === 2 && nr >= range[0] && nr <= range[1]) {
return true;
}
}
@@ -163,7 +168,6 @@ class Encoder extends Transform {
constructor(options) {
super();
// init Transform
this.options = options || {};
if (this.options.lineLength !== false) {
@@ -219,7 +223,6 @@ class Encoder extends Transform {
}
}
// expose to the world
module.exports = {
encode,
wrap,

View File

@@ -1,6 +1,6 @@
'use strict';
const spawn = require('child_process').spawn;
const { spawn } = require('child_process');
const packageData = require('../../package.json');
const shared = require('../shared');
const errors = require('../errors');
@@ -24,30 +24,26 @@ class SendmailTransport {
// use a reference to spawn for mocking purposes
this._spawn = spawn;
this.options = options || {};
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());
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;
}
}
}
@@ -62,10 +58,8 @@ class SendmailTransport {
// 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;
const envelope = mail.data.envelope || mail.message.getEnvelope();
const messageId = mail.message.messageId();
let returned;
const hasInvalidAddresses = []
@@ -73,19 +67,17 @@ class SendmailTransport {
.concat(envelope.to || [])
.some(addr => /^-/.test(addr));
if (hasInvalidAddresses) {
let err = new Error('Can not send mail. Invalid envelope addresses.');
const err = new Error('Can not send mail. Invalid envelope addresses.');
err.code = errors.ESENDMAIL;
return done(err);
}
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);
}
// force -i to keep single dots
const args = this.args
? ['-i'].concat(this.args).concat(envelope.to)
: ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
let callback = err => {
const callback = err => {
if (returned) {
// ignore any additional responses, already done
return;
@@ -94,16 +86,16 @@ class SendmailTransport {
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'
});
}
return done(null, {
envelope,
messageId,
response: 'Messages queued for delivery'
});
}
};
let sendmail;
try {
sendmail = this._spawn(this.path, args);
} catch (E) {
@@ -138,12 +130,9 @@ class SendmailTransport {
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);
}
const err = new Error(
code === 127 ? 'Sendmail command not found, process exited with code ' + code : 'Sendmail exited with code ' + code
);
err.code = errors.ESENDMAIL;
this.logger.error(
@@ -174,7 +163,7 @@ class SendmailTransport {
callback(err);
});
let recipients = [].concat(envelope.to || []);
const recipients = [].concat(envelope.to || []);
if (recipients.length > 3) {
recipients.push('...and ' + recipients.splice(2).length + ' more');
}
@@ -188,7 +177,7 @@ class SendmailTransport {
recipients.join(', ')
);
let sourceStream = mail.message.createReadStream();
const sourceStream = mail.message.createReadStream();
sourceStream.once('error', err => {
this.logger.error(
{
@@ -206,7 +195,7 @@ class SendmailTransport {
sourceStream.pipe(sendmail.stdin);
} else {
let err = new Error('sendmail was not found');
const err = new Error('sendmail was not found');
err.code = errors.ESENDMAIL;
return callback(err);
}

View File

@@ -17,7 +17,7 @@ class SESTransport extends EventEmitter {
super();
options = options || {};
this.options = options || {};
this.options = options;
this.ses = this.options.SES;
this.name = 'SESTransport';
@@ -46,21 +46,16 @@ class SESTransport extends EventEmitter {
* @param {Function} callback Callback function to run when the sending is completed
*/
send(mail, callback) {
let statObject = {
ts: Date.now(),
pending: true
};
let fromHeader = mail.message._headers.find(header => /^from$/i.test(header.key));
if (fromHeader) {
let mimeNode = new MimeNode('text/plain');
const mimeNode = new MimeNode('text/plain');
fromHeader = mimeNode._convertAddresses(mimeNode._parseAddresses(fromHeader.value));
}
let envelope = mail.data.envelope || mail.message.getEnvelope();
let messageId = mail.message.messageId();
const envelope = mail.data.envelope || mail.message.getEnvelope();
const messageId = mail.message.messageId();
let recipients = [].concat(envelope.to || []);
const recipients = [].concat(envelope.to || []);
if (recipients.length > 3) {
recipients.push('...and ' + recipients.splice(2).length + ' more');
}
@@ -74,7 +69,7 @@ class SESTransport extends EventEmitter {
recipients.join(', ')
);
let getRawMessage = next => {
const getRawMessage = next => {
// do not use Message-ID and Date in DKIM signature
if (!mail.data._dkim) {
mail.data._dkim = {};
@@ -85,9 +80,9 @@ class SESTransport extends EventEmitter {
mail.data._dkim.skipFields = 'date:message-id';
}
let sourceStream = mail.message.createReadStream();
let stream = sourceStream.pipe(new LeWindows());
let chunks = [];
const sourceStream = mail.message.createReadStream();
const stream = sourceStream.pipe(new LeWindows());
const chunks = [];
let chunklen = 0;
stream.on('readable', () => {
@@ -100,9 +95,7 @@ class SESTransport extends EventEmitter {
sourceStream.once('error', err => stream.emit('error', err));
stream.once('error', err => {
next(err);
});
stream.once('error', err => next(err));
stream.once('end', () => next(null, Buffer.concat(chunks, chunklen)));
};
@@ -120,26 +113,24 @@ class SESTransport extends EventEmitter {
messageId,
err.message
);
statObject.pending = false;
return callback(err);
}
let sesMessage = {
Content: {
Raw: {
// required
Data: raw // required
const sesMessage = Object.assign(
{
Content: {
Raw: {
// required
Data: raw // required
}
},
FromEmailAddress: fromHeader || envelope.from,
Destination: {
ToAddresses: envelope.to
}
},
FromEmailAddress: fromHeader ? fromHeader : envelope.from,
Destination: {
ToAddresses: envelope.to
}
};
Object.keys(mail.data.ses || {}).forEach(key => {
sesMessage[key] = mail.data.ses[key];
});
mail.data.ses || {}
);
this.getRegion((err, region) => {
if (err || !region) {
@@ -155,7 +146,6 @@ class SESTransport extends EventEmitter {
region = 'email';
}
statObject.pending = true;
callback(null, {
envelope: {
from: envelope.from,
@@ -176,7 +166,6 @@ class SESTransport extends EventEmitter {
messageId,
err.message
);
statObject.pending = false;
callback(err);
});
});

View File

@@ -30,34 +30,28 @@ try {
module.exports.networkInterfaces = networkInterfaces;
const isFamilySupported = (family, allowInternal) => {
let networkInterfaces = module.exports.networkInterfaces;
if (!networkInterfaces) {
const ifaces = module.exports.networkInterfaces;
if (!ifaces) {
// hope for the best
return true;
}
const familySupported =
// crux that replaces Object.values(networkInterfaces) as Object.values is not supported in nodejs v6
Object.keys(networkInterfaces)
.map(key => networkInterfaces[key])
// crux that replaces .flat() as it is not supported in older Node versions (v10 and older)
.reduce((acc, val) => acc.concat(val), [])
.filter(i => !i.internal || allowInternal)
.filter(i => i.family === 'IPv' + family || i.family === family).length > 0;
return familySupported;
return Object.keys(ifaces)
.map(key => ifaces[key])
.reduce((acc, val) => acc.concat(val), [])
.filter(i => !i.internal || allowInternal)
.some(i => i.family === 'IPv' + family || i.family === family);
};
const resolver = (family, hostname, options, callback) => {
const resolve = (family, hostname, options, callback) => {
options = options || {};
const familySupported = isFamilySupported(family, options.allowInternalNetworkInterfaces);
if (!familySupported) {
if (!isFamilySupported(family, options.allowInternalNetworkInterfaces)) {
return callback(null, []);
}
const resolver = dns.Resolver ? new dns.Resolver(options) : dns;
resolver['resolve' + family](hostname, (err, addresses) => {
const dnsResolver = dns.Resolver ? new dns.Resolver(options) : dns;
dnsResolver['resolve' + family](hostname, (err, addresses) => {
if (err) {
switch (err.code) {
case dns.NODATA:
@@ -82,15 +76,10 @@ const formatDNSValue = (value, extra) => {
return Object.assign({}, extra || {});
}
let addresses = value.addresses || [];
const addresses = value.addresses || [];
// Select a random address from available addresses, or null if none
let host = null;
if (addresses.length === 1) {
host = addresses[0];
} else if (addresses.length > 1) {
host = addresses[Math.floor(Math.random() * addresses.length)];
}
const host = addresses.length > 0 ? addresses[Math.floor(Math.random() * addresses.length)] : null;
return Object.assign(
{
@@ -112,7 +101,7 @@ module.exports.resolveHostname = (options, callback) => {
if (!options.host || net.isIP(options.host)) {
// nothing to do here
let value = {
const value = {
addresses: [options.host],
servername: options.servername || false
};
@@ -164,14 +153,14 @@ module.exports.resolveHostname = (options, callback) => {
let ipv4Error = null;
let ipv6Error = null;
resolver(4, options.host, options, (err, addresses) => {
resolve(4, options.host, options, (err, addresses) => {
if (err) {
ipv4Error = err;
} else {
ipv4Addresses = addresses || [];
}
resolver(6, options.host, options, (err, addresses) => {
resolve(6, options.host, options, (err, addresses) => {
if (err) {
ipv6Error = err;
} else {
@@ -179,10 +168,10 @@ module.exports.resolveHostname = (options, callback) => {
}
// Combine addresses: IPv4 first, then IPv6
let allAddresses = ipv4Addresses.concat(ipv6Addresses);
const allAddresses = ipv4Addresses.concat(ipv6Addresses);
if (allAddresses.length) {
let value = {
const value = {
addresses: allAddresses,
servername: options.servername || options.host
};
@@ -240,7 +229,7 @@ module.exports.resolveHostname = (options, callback) => {
}
// Get all supported addresses from dns.lookup
let supportedAddresses = addresses
const supportedAddresses = addresses
? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address)
: [];
@@ -259,7 +248,7 @@ module.exports.resolveHostname = (options, callback) => {
);
}
let value = {
const value = {
addresses: supportedAddresses.length ? supportedAddresses : [options.host],
servername: options.servername || options.host
};
@@ -304,95 +293,78 @@ module.exports.resolveHostname = (options, callback) => {
*/
module.exports.parseConnectionUrl = str => {
str = str || '';
let options = {};
const options = {};
const url = urllib.parse(str, true);
[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;
}
switch (url.protocol) {
case 'smtp:':
options.secure = false;
if (!isNaN(url.port) && Number(url.port)) {
options.port = Number(url.port);
}
if (url.hostname) {
options.host = url.hostname;
}
if (url.auth) {
const auth = url.auth.split(':');
options.auth = {
user: auth.shift(),
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 'smtps:':
options.secure = true;
break;
case 'direct:':
options.direct = true;
case 'false':
value = false;
break;
}
if (!isNaN(url.port) && Number(url.port)) {
options.port = Number(url.port);
// 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 (url.hostname) {
options.host = url.hostname;
if (!(lKey in obj)) {
obj[lKey] = value;
}
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];
}
});
const entry = Object.assign({}, defaults || {}, data || {});
delete entry.level;
logger[level](entry, message, ...args);
};
@@ -407,8 +379,8 @@ module.exports._logFunc = (logger, level, defaults, data, message, ...args) => {
module.exports.getLogger = (options, defaults) => {
options = options || {};
let response = {};
let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
const response = {};
const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
if (!options.logger) {
// use vanity logger
@@ -418,12 +390,7 @@ module.exports.getLogger = (options, defaults) => {
return response;
}
let logger = options.logger;
if (options.logger === true) {
// create console logger
logger = createDefaultLogger(levels);
}
const logger = options.logger === true ? createDefaultLogger(levels) : options.logger;
levels.forEach(level => {
response[level] = (data, message, ...args) => {
@@ -443,8 +410,8 @@ module.exports.getLogger = (options, defaults) => {
*/
module.exports.callbackPromise = (resolve, reject) =>
function () {
let args = Array.from(arguments);
let err = args.shift();
const args = Array.from(arguments);
const err = args.shift();
if (err) {
reject(err);
} else {
@@ -546,8 +513,7 @@ module.exports.resolveContent = (data, key, callback) => {
}
let content = (data && data[key] && data[key].content) || data[key];
let contentStream;
let encoding = ((typeof data[key] === 'object' && data[key].encoding) || 'utf8')
const encoding = ((typeof data[key] === 'object' && data[key].encoding) || 'utf8')
.toString()
.toLowerCase()
.replace(/[-_\s]/g, '');
@@ -572,10 +538,9 @@ module.exports.resolveContent = (data, key, callback) => {
callback(null, value);
});
} else if (/^https?:\/\//i.test(content.path || content.href)) {
contentStream = nmfetch(content.path || content.href);
return resolveStream(contentStream, callback);
return resolveStream(nmfetch(content.path || content.href), callback);
} else if (/^data:/i.test(content.path || content.href)) {
let parsedDataUri = module.exports.parseDataURI(content.path || content.href);
const parsedDataUri = module.exports.parseDataURI(content.path || content.href);
if (!parsedDataUri || !parsedDataUri.data) {
return callback(null, Buffer.from(0));
@@ -600,21 +565,15 @@ module.exports.resolveContent = (data, key, callback) => {
* Copies properties from source objects to target objects
*/
module.exports.assign = function (/* target, ... sources */) {
let args = Array.from(arguments);
let target = args.shift() || {};
const args = Array.from(arguments);
const 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];
});
target[key] = Object.assign(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
@@ -631,10 +590,10 @@ module.exports.encodeXText = str => {
if (!/[^\x21-\x2A\x2C-\x3C\x3E-\x7E]/.test(str)) {
return str;
}
let buf = Buffer.from(str);
const buf = Buffer.from(str);
let result = '';
for (let i = 0, len = buf.length; i < len; i++) {
let c = buf[i];
const c = buf[i];
if (c < 0x21 || c > 0x7e || c === 0x2b || c === 0x3d) {
result += '+' + (c < 0x10 ? '0' : '') + c.toString(16).toUpperCase();
} else {
@@ -652,7 +611,7 @@ module.exports.encodeXText = str => {
*/
function resolveStream(stream, callback) {
let responded = false;
let chunks = [];
const chunks = [];
let chunklen = 0;
stream.on('error', err => {
@@ -695,13 +654,8 @@ function resolveStream(stream, callback) {
* @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;
}
});
const levelMaxLen = levels.reduce((max, level) => Math.max(max, level.length), 0);
const levelNames = new Map();
levels.forEach(level => {
let levelName = level.toUpperCase();
@@ -711,7 +665,7 @@ function createDefaultLogger(levels) {
levelNames.set(level, levelName);
});
let print = (level, entry, message, ...args) => {
const print = (level, entry, message, ...args) => {
let prefix = '';
if (entry) {
if (entry.tnx === 'server') {
@@ -735,7 +689,7 @@ function createDefaultLogger(levels) {
});
};
let logger = {};
const logger = {};
levels.forEach(level => {
logger[level] = print.bind(null, level);
});

View File

@@ -1,7 +1,6 @@
'use strict';
const stream = require('stream');
const Transform = stream.Transform;
const { Transform } = require('stream');
/**
* Escapes dots in the beginning of lines. Ends the stream with <CR><LF>.<CR><LF>
@@ -12,9 +11,7 @@ const Transform = stream.Transform;
class DataStream extends Transform {
constructor(options) {
super(options);
// init Transform
this.options = options || {};
this._curLine = '';
this.inByteCount = 0;
this.outByteCount = 0;
@@ -25,7 +22,7 @@ class DataStream extends Transform {
* Escapes dots
*/
_transform(chunk, encoding, done) {
let chunks = [];
const chunks = [];
let chunklen = 0;
let i,
len,
@@ -53,7 +50,7 @@ class DataStream extends Transform {
lastPos = i + 1;
}
} else if (chunk[i] === 0x0a) {
// .
// \n
if ((i && chunk[i - 1] !== 0x0d) || (!i && this.lastByte !== 0x0d)) {
if (i > lastPos) {
buf = chunk.slice(lastPos, i);

View File

@@ -22,18 +22,14 @@ const errors = require('../errors');
* @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);
const proxy = urllib.parse(proxyUrl);
// create a socket connection to the proxy server
let options;
let connect;
let socket;
options = {
const options = {
host: proxy.hostname,
port: Number(proxy.port) ? Number(proxy.port) : proxy.protocol === 'https:' ? 443 : 80
};
let connect;
if (proxy.protocol === 'https:') {
// we can use untrusted proxies as long as we verify actual SMTP certificates
options.rejectUnauthorized = false;
@@ -42,10 +38,12 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
connect = net.connect.bind(net);
}
let socket;
// 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 = err => {
const tempSocketErr = err => {
if (finished) {
return;
}
@@ -58,8 +56,8 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
callback(err);
};
let timeoutErr = () => {
let err = new Error('Proxy socket timed out');
const timeoutErr = () => {
const err = new Error('Proxy socket timed out');
err.code = 'ETIMEDOUT';
tempSocketErr(err);
};
@@ -69,7 +67,7 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
return;
}
let reqHeaders = {
const reqHeaders = {
Host: destinationHost + ':' + destinationPort,
Connection: 'close'
};
@@ -93,7 +91,7 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
);
let headers = '';
let onSocketData = chunk => {
const onSocketData = chunk => {
let match;
let remainder;
@@ -122,7 +120,7 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
} catch (_E) {
// ignore
}
let err = new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || ''));
const err = new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || ''));
err.code = errors.EPROXY;
return callback(err);
}

View File

@@ -1,13 +1,13 @@
'use strict';
const packageInfo = require('../../package.json');
const EventEmitter = require('events').EventEmitter;
const { EventEmitter } = require('events');
const net = require('net');
const tls = require('tls');
const os = require('os');
const crypto = require('crypto');
const DataStream = require('./data-stream');
const PassThrough = require('stream').PassThrough;
const { PassThrough } = require('stream');
const shared = require('../shared');
// default timeout values in ms
@@ -17,6 +17,28 @@ const GREETING_TIMEOUT = 30 * 1000; // how much to wait after connection is esta
const DNS_TIMEOUT = 30 * 1000; // how much to wait for resolveHostname
const TEARDOWN_NOOP = () => {}; // reusable no-op handler for absorbing errors during socket teardown
/**
* Re-interpret a server response stored in fake 8-bit byte-container form
* (the result of chunk.toString('binary') in _onData) as UTF-8.
*
* Server reply text has no formally defined charset (RFC 5321 §4.2.1), but
* modern MTAs commonly use UTF-8. The byte-container plumbing in _onData is
* required to reassemble multi-byte sequences split across socket chunks;
* this helper performs the actual decode at the line boundary, falling back
* to the byte-container form when the bytes are not valid UTF-8 so that
* legacy 8-bit replies are still recoverable byte-for-byte.
*/
function decodeServerResponse(str) {
if (!str) {
return str;
}
const utf8 = Buffer.from(str, 'binary').toString('utf8');
// The input is a byte container (each char is in U+0000..U+00FF) so it can never
// already contain U+FFFD; any \uFFFD in the result was inserted by Node's UTF-8
// decoder for invalid bytes, which means we should return the original bytes intact.
return utf8.includes('\uFFFD') ? str : utf8;
}
/**
* Generates a SMTP connection object
*
@@ -68,7 +90,7 @@ class SMTPConnection extends EventEmitter {
this.secureConnection = true;
}
this.name = this.options.name || this._getHostname();
this.name = (this.options.name || this._getHostname()).toString().replace(/[\r\n]+/g, '');
this.logger = shared.getLogger(this.options, {
component: this.options.component || 'smtp-connection',
@@ -76,13 +98,12 @@ class SMTPConnection extends EventEmitter {
});
this.customAuth = new Map();
Object.keys(this.options.customAuth || {}).forEach(key => {
let mapKey = (key || '').toString().trim().toUpperCase();
if (!mapKey) {
return;
for (const key of Object.keys(this.options.customAuth || {})) {
const mapKey = (key || '').toString().trim().toUpperCase();
if (mapKey) {
this.customAuth.set(mapKey, this.options.customAuth[key]);
}
this.customAuth.set(mapKey, this.options.customAuth[key]);
});
}
/**
* Expose version nr, just for the reference
@@ -266,27 +287,7 @@ class SMTPConnection extends EventEmitter {
} else if (this.options.socket) {
// socket object is set up but not yet connected
this._socket = this.options.socket;
return shared.resolveHostname(opts, (err, resolved) => {
if (err) {
return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN'));
}
this.logger.debug(
{
tnx: 'dns',
source: opts.host,
resolved: resolved.host,
cached: !!resolved.cached
},
'Resolved %s as %s [cache %s]',
opts.host,
resolved.host,
resolved.cached ? 'hit' : 'miss'
);
Object.keys(resolved).forEach(key => {
if (key.charAt(0) !== '_' && resolved[key]) {
opts[key] = resolved[key];
}
});
return this._resolveAndConnect(opts, _resolved => {
try {
this._socket.connect(this.port, this.host, () => {
this._socket.setKeepAlive(true);
@@ -297,80 +298,59 @@ class SMTPConnection extends EventEmitter {
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
}
});
} else if (this.secureConnection) {
// connect using tls
if (this.options.tls) {
Object.keys(this.options.tls).forEach(key => {
opts[key] = this.options.tls[key];
});
}
// ensure servername for SNI
if (this.servername && !opts.servername) {
opts.servername = this.servername;
}
return shared.resolveHostname(opts, (err, resolved) => {
if (err) {
return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN'));
}
this.logger.debug(
{
tnx: 'dns',
source: opts.host,
resolved: resolved.host,
cached: !!resolved.cached
},
'Resolved %s as %s [cache %s]',
opts.host,
resolved.host,
resolved.cached ? 'hit' : 'miss'
);
Object.keys(resolved).forEach(key => {
if (key.charAt(0) !== '_' && resolved[key]) {
opts[key] = resolved[key];
}
});
// Store fallback addresses for retry on connection failure
this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host);
this._connectOpts = Object.assign({}, opts);
this._connectToHost(opts, true);
});
} else {
// connect using plaintext
return shared.resolveHostname(opts, (err, resolved) => {
if (err) {
return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN'));
}
this.logger.debug(
{
tnx: 'dns',
source: opts.host,
resolved: resolved.host,
cached: !!resolved.cached
},
'Resolved %s as %s [cache %s]',
opts.host,
resolved.host,
resolved.cached ? 'hit' : 'miss'
);
Object.keys(resolved).forEach(key => {
if (key.charAt(0) !== '_' && resolved[key]) {
opts[key] = resolved[key];
}
});
if (this.secureConnection) {
Object.assign(opts, this.options.tls || {});
// ensure servername for SNI
if (this.servername && !opts.servername) {
opts.servername = this.servername;
}
}
return this._resolveAndConnect(opts, resolved => {
// Store fallback addresses for retry on connection failure
this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host);
this._connectOpts = Object.assign({}, opts);
this._connectToHost(opts, false);
this._connectToHost(opts, this.secureConnection);
});
}
}
/**
* Resolves the hostname and applies resolved values to opts,
* then calls the provided callback with the resolved data
*
* @param {Object} opts Connection options (modified in place)
* @param {Function} callback Called with resolved data on success
*/
_resolveAndConnect(opts, callback) {
return shared.resolveHostname(opts, (err, resolved) => {
if (err) {
return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN'));
}
this.logger.debug(
{
tnx: 'dns',
source: opts.host,
resolved: resolved.host,
cached: !!resolved.cached
},
'Resolved %s as %s [cache %s]',
opts.host,
resolved.host,
resolved.cached ? 'hit' : 'miss'
);
for (const key of Object.keys(resolved)) {
if (key.charAt(0) !== '_' && resolved[key]) {
opts[key] = resolved[key];
}
}
callback(resolved);
});
}
/**
* Attempts to connect to the specified host address
*
@@ -381,7 +361,7 @@ class SMTPConnection extends EventEmitter {
this._connectionAttemptId++;
const currentAttemptId = this._connectionAttemptId;
let connectFn = secure ? tls.connect : net.connect;
const connectFn = secure ? tls.connect : net.connect;
try {
this._socket = connectFn(opts, () => {
// Ignore callback if this is a stale connection attempt
@@ -418,7 +398,7 @@ class SMTPConnection extends EventEmitter {
clearTimeout(this._connectionTimeout);
// Check if we have fallback addresses to try
let canFallback = this._fallbackAddresses && this._fallbackAddresses.length && this.stage === 'init' && !this._destroyed;
const canFallback = this._fallbackAddresses && this._fallbackAddresses.length && this.stage === 'init' && !this._destroyed;
if (!canFallback) {
// No more fallback addresses, report the error
@@ -426,7 +406,7 @@ class SMTPConnection extends EventEmitter {
return;
}
let nextHost = this._fallbackAddresses.shift();
const nextHost = this._fallbackAddresses.shift();
this.logger.info(
{
@@ -478,12 +458,7 @@ class SMTPConnection extends EventEmitter {
}
this._closing = true;
let closeMethod = 'end';
if (this.stage === 'init') {
// Close the socket immediately when connection timed out
closeMethod = 'destroy';
}
const closeMethod = this.stage === 'init' ? 'destroy' : 'end';
this.logger.debug(
{
@@ -493,7 +468,7 @@ class SMTPConnection extends EventEmitter {
closeMethod
);
let socket = (this._socket && this._socket.socket) || this._socket;
const socket = (this._socket && this._socket.socket) || this._socket;
if (socket && !socket.destroyed) {
try {
@@ -551,11 +526,11 @@ class SMTPConnection extends EventEmitter {
}
if (this.customAuth.has(this._authMethod)) {
let handler = this.customAuth.get(this._authMethod);
const handler = this.customAuth.get(this._authMethod);
let lastResponse;
let returned = false;
let resolve = () => {
const resolve = () => {
if (returned) {
return;
}
@@ -574,7 +549,7 @@ class SMTPConnection extends EventEmitter {
callback(null, true);
};
let reject = err => {
const reject = err => {
if (returned) {
return;
}
@@ -582,7 +557,7 @@ class SMTPConnection extends EventEmitter {
callback(this._formatError(err, 'EAUTH', lastResponse, 'AUTH ' + this._authMethod));
};
let handlerResponse = handler({
const handlerResponse = handler({
auth: this._auth,
method: this._authMethod,
@@ -709,7 +684,7 @@ class SMTPConnection extends EventEmitter {
// ensure that callback is only called once
let returned = false;
let callback = function () {
const callback = function () {
if (returned) {
return;
}
@@ -722,11 +697,11 @@ class SMTPConnection extends EventEmitter {
message.on('error', err => callback(this._formatError(err, 'ESTREAM', false, 'API')));
}
let startTime = Date.now();
const startTime = Date.now();
this._setEnvelope(envelope, (err, info) => {
if (err) {
// create passthrough stream to consume to prevent OOM
let stream = new PassThrough();
const stream = new PassThrough();
if (typeof message.pipe === 'function') {
message.pipe(stream);
} else {
@@ -736,8 +711,8 @@ class SMTPConnection extends EventEmitter {
return callback(err);
}
let envelopeTime = Date.now();
let stream = this._createSendStream((err, str) => {
const envelopeTime = Date.now();
const stream = this._createSendStream((err, str) => {
if (err) {
return callback(err);
}
@@ -921,7 +896,7 @@ class SMTPConnection extends EventEmitter {
err.message += ': ' + response;
}
let responseCode = (typeof response === 'string' && Number((response.match(/^\d+/) || [])[0])) || false;
const responseCode = (typeof response === 'string' && Number((response.match(/^\d+/) || [])[0])) || false;
if (responseCode) {
err.responseCode = responseCode;
}
@@ -942,15 +917,15 @@ class SMTPConnection extends EventEmitter {
let serverResponse = false;
if (this._remainder && this._remainder.trim()) {
this.lastServerResponse = serverResponse = decodeServerResponse(this._remainder.trim());
if (this.options.debug || this.options.transactionLog) {
this.logger.debug(
{
tnx: 'server'
},
this._remainder.replace(/\r?\n$/, '')
serverResponse
);
}
this.lastServerResponse = serverResponse = this._remainder.trim();
}
this.logger.info(
@@ -1016,15 +991,14 @@ class SMTPConnection extends EventEmitter {
this._socket.removeListener('data', this._onSocketData); // incoming data is going to be gibberish from this point onwards
this._socket.removeListener('timeout', this._onSocketTimeout); // timeout will be re-set for the new socket object
let socketPlain = this._socket;
let opts = {
socket: this._socket,
host: this.host
};
Object.keys(this.options.tls || {}).forEach(key => {
opts[key] = this.options.tls[key];
});
const socketPlain = this._socket;
const opts = Object.assign(
{
socket: this._socket,
host: this.host
},
this.options.tls || {}
);
// ensure servername for SNI
if (this.servername && !opts.servername) {
@@ -1071,7 +1045,7 @@ class SMTPConnection extends EventEmitter {
return false;
}
let str = (this.lastServerResponse = (this._responseQueue.shift() || '').toString());
let str = (this.lastServerResponse = decodeServerResponse((this._responseQueue.shift() || '').toString()));
if (/^\d+-/.test(str.split('\n').pop())) {
// keep waiting for the final part of multiline response
@@ -1092,7 +1066,7 @@ class SMTPConnection extends EventEmitter {
setImmediate(() => this._processResponse());
}
let action = this._responseActions.shift();
const action = this._responseActions.shift();
if (typeof action === 'function') {
action.call(this, str);
@@ -1140,7 +1114,7 @@ class SMTPConnection extends EventEmitter {
* {from:{address:'...',name:'...'}, to:[address:'...',name:'...']}
*/
_setEnvelope(envelope, callback) {
let args = [];
const args = [];
let useSmtpUtf8 = false;
this._envelope = envelope || {};
@@ -1175,7 +1149,7 @@ class SMTPConnection extends EventEmitter {
}
// clone the recipients array for latter manipulation
this._envelope.rcptQueue = JSON.parse(JSON.stringify(this._envelope.to || []));
this._envelope.rcptQueue = [].concat(this._envelope.to || []);
this._envelope.rejected = [];
this._envelope.rejectedErrors = [];
this._envelope.accepted = [];
@@ -1207,7 +1181,10 @@ class SMTPConnection extends EventEmitter {
}
if (this._envelope.size && this._supportedExtensions.includes('SIZE')) {
args.push('SIZE=' + this._envelope.size);
const sizeValue = Number(this._envelope.size) || 0;
if (sizeValue > 0) {
args.push('SIZE=' + sizeValue);
}
}
// If the server supports DSN and the envelope includes an DSN prop
@@ -1260,7 +1237,7 @@ class SMTPConnection extends EventEmitter {
throw new Error('ret: ' + JSON.stringify(ret));
}
let envid = (params.envid || params.id || '').toString() || null;
const envid = (params.envid || params.id || '').toString() || null;
let notify = params.notify || null;
if (notify) {
@@ -1268,8 +1245,8 @@ class SMTPConnection extends EventEmitter {
notify = notify.split(',');
}
notify = notify.map(n => n.trim().toUpperCase());
let validNotify = ['NEVER', 'SUCCESS', 'FAILURE', 'DELAY'];
let invalidNotify = notify.filter(n => !validNotify.includes(n));
const validNotify = ['NEVER', 'SUCCESS', 'FAILURE', 'DELAY'];
const invalidNotify = notify.filter(n => !validNotify.includes(n));
if (invalidNotify.length || (notify.length > 1 && notify.includes('NEVER'))) {
throw new Error('notify: ' + JSON.stringify(notify.join(',')));
}
@@ -1290,7 +1267,7 @@ class SMTPConnection extends EventEmitter {
}
_getDsnRcptToArgs() {
let args = [];
const args = [];
// If the server supports DSN and the envelope includes an DSN prop
// then append DSN params to the RCPT TO command
if (this._envelope.dsn && this._supportedExtensions.includes('DSN')) {
@@ -1305,12 +1282,11 @@ class SMTPConnection extends EventEmitter {
}
_createSendStream(callback) {
let dataStream = new DataStream();
let logStream;
const dataStream = new DataStream();
if (this.options.lmtp) {
this._envelope.accepted.forEach((recipient, i) => {
let final = i === this._envelope.accepted.length - 1;
const final = i === this._envelope.accepted.length - 1;
this._responseActions.push(str => {
this._actionLMTPStream(recipient, final, str, callback);
});
@@ -1326,7 +1302,7 @@ class SMTPConnection extends EventEmitter {
});
if (this.options.debug) {
logStream = new PassThrough();
const logStream = new PassThrough();
logStream.on('readable', () => {
let chunk;
while ((chunk = logStream.read())) {
@@ -1604,24 +1580,21 @@ class SMTPConnection extends EventEmitter {
* @param {String} str Message from the server
*/
_actionAUTH_CRAM_MD5(str, callback) {
let challengeMatch = str.match(/^334\s+(.+)$/);
let challengeString = '';
const challengeMatch = str.match(/^334\s+(.+)$/);
if (!challengeMatch) {
return callback(
this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5')
);
} else {
challengeString = challengeMatch[1];
}
// Decode from base64
let base64decoded = Buffer.from(challengeString, 'base64').toString('ascii'),
hmacMD5 = crypto.createHmac('md5', this._auth.credentials.pass);
const base64decoded = Buffer.from(challengeMatch[1], 'base64').toString('ascii');
const hmacMD5 = crypto.createHmac('md5', this._auth.credentials.pass);
hmacMD5.update(base64decoded);
let prepended = this._auth.credentials.user + ' ' + hmacMD5.digest('hex');
const prepended = this._auth.credentials.user + ' ' + hmacMD5.digest('hex');
this._responseActions.push(str => {
this._actionAUTH_CRAM_MD5_PASS(str, callback);
@@ -1742,39 +1715,29 @@ class SMTPConnection extends EventEmitter {
* @param {String} str Message from the server
*/
_actionMAIL(str, callback) {
let message, curRecipient;
if (Number(str.charAt(0)) !== 2) {
if (this._usingSmtpUtf8 && /^550 /.test(str) && /[\x80-\uFFFF]/.test(this._envelope.from)) {
message = 'Internationalized mailbox name not allowed';
} else {
message = 'Mail command failed';
}
const message =
this._usingSmtpUtf8 && /^550 /.test(str) && /[\x80-\uFFFF]/.test(this._envelope.from)
? 'Internationalized mailbox name not allowed'
: 'Mail command failed';
return callback(this._formatError(message, 'EENVELOPE', str, 'MAIL FROM'));
}
if (!this._envelope.rcptQueue.length) {
return callback(this._formatError("Can't send mail - no recipients defined", 'EENVELOPE', false, 'API'));
} else {
this._recipientQueue = [];
if (this._supportedExtensions.includes('PIPELINING')) {
while (this._envelope.rcptQueue.length) {
curRecipient = this._envelope.rcptQueue.shift();
this._recipientQueue.push(curRecipient);
this._responseActions.push(str => {
this._actionRCPT(str, callback);
});
this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
}
} else {
curRecipient = this._envelope.rcptQueue.shift();
this._recipientQueue.push(curRecipient);
this._responseActions.push(str => {
this._actionRCPT(str, callback);
});
this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
}
}
this._recipientQueue = [];
const usePipelining = this._supportedExtensions.includes('PIPELINING');
do {
const curRecipient = this._envelope.rcptQueue.shift();
this._recipientQueue.push(curRecipient);
this._responseActions.push(str => {
this._actionRCPT(str, callback);
});
this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
} while (usePipelining && this._envelope.rcptQueue.length);
}
/**
@@ -1783,16 +1746,14 @@ class SMTPConnection extends EventEmitter {
* @param {String} str Message from the server
*/
_actionRCPT(str, callback) {
let message,
err,
curRecipient = this._recipientQueue.shift();
let err;
const curRecipient = this._recipientQueue.shift();
if (Number(str.charAt(0)) !== 2) {
// this is a soft error
if (this._usingSmtpUtf8 && /^553 /.test(str) && /[\x80-\uFFFF]/.test(curRecipient)) {
message = 'Internationalized mailbox name not allowed';
} else {
message = 'Recipient command failed';
}
const message =
this._usingSmtpUtf8 && /^553 /.test(str) && /[\x80-\uFFFF]/.test(curRecipient)
? 'Internationalized mailbox name not allowed'
: 'Recipient command failed';
this._envelope.rejected.push(curRecipient);
// store error for the failed recipient
err = this._formatError(message, 'EENVELOPE', str, 'RCPT TO');
@@ -1815,12 +1776,12 @@ class SMTPConnection extends EventEmitter {
return callback(err);
}
} else if (this._envelope.rcptQueue.length) {
curRecipient = this._envelope.rcptQueue.shift();
this._recipientQueue.push(curRecipient);
const nextRecipient = this._envelope.rcptQueue.shift();
this._recipientQueue.push(nextRecipient);
this._responseActions.push(str => {
this._actionRCPT(str, callback);
});
this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
this._sendCommand('RCPT TO:<' + nextRecipient + '>' + this._getDsnRcptToArgs());
}
}
@@ -1836,7 +1797,7 @@ class SMTPConnection extends EventEmitter {
return callback(this._formatError('Data command failed', 'EENVELOPE', str, 'DATA'));
}
let response = {
const response = {
accepted: this._envelope.accepted,
rejected: this._envelope.rejected
};
@@ -1860,12 +1821,9 @@ class SMTPConnection extends EventEmitter {
*/
_actionSMTPStream(str, callback) {
if (Number(str.charAt(0)) !== 2) {
// Message failed
return callback(this._formatError('Message failed', 'EMESSAGE', str, 'DATA'));
} else {
// Message sent succesfully
return callback(null, str);
}
return callback(null, str);
}
/**

View File

@@ -51,11 +51,8 @@ class SMTPPool extends EventEmitter {
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.version = packageData.version + '[client:' + packageData.version + ']';
this._rateLimit = {
counter: 0,
@@ -123,7 +120,7 @@ class SMTPPool extends EventEmitter {
*/
close() {
let connection;
let len = this._connections.length;
const len = this._connections.length;
this._closed = true;
// clear rate limit timer if it exists
@@ -164,7 +161,7 @@ class SMTPPool extends EventEmitter {
}
// make sure that entire queue would be cleaned
let invokeCallbacks = () => {
const invokeCallbacks = () => {
if (!this._queue.length) {
this.logger.debug(
{
@@ -174,7 +171,7 @@ class SMTPPool extends EventEmitter {
);
return;
}
let entry = this._queue.shift();
const entry = this._queue.shift();
if (entry && typeof entry.callback === 'function') {
try {
entry.callback(new Error('Connection pool was closed'));
@@ -201,9 +198,6 @@ class SMTPPool extends EventEmitter {
* 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;
@@ -220,12 +214,7 @@ class SMTPPool extends EventEmitter {
}
// find first available connection
for (i = 0, len = this._connections.length; i < len; i++) {
if (this._connections[i].available) {
connection = this._connections[i];
break;
}
}
let connection = this._connections.find(c => c.available);
if (!connection && this._connections.length < this.options.maxConnections) {
connection = this._createConnection();
@@ -243,7 +232,7 @@ class SMTPPool extends EventEmitter {
this.emit('idle');
}
let entry = (connection.queueEntry = this._queue.shift());
const entry = (connection.queueEntry = this._queue.shift());
entry.messageId = (connection.queueEntry.mail.message.getHeader('message-id') || '').replace(/[<>\s]/g, '');
connection.available = false;
@@ -294,7 +283,7 @@ class SMTPPool extends EventEmitter {
* Creates a new pool resource
*/
_createConnection() {
let connection = new PoolResource(this);
const connection = new PoolResource(this);
connection.id = ++this._connectionCounter;
@@ -331,7 +320,7 @@ class SMTPPool extends EventEmitter {
// resource is terminated with an error
connection.once('error', err => {
if (err.code !== 'EMAXLIMIT') {
if (err.code !== errors.EMAXLIMIT) {
this.logger.warn(
{
err,
@@ -450,7 +439,7 @@ class SMTPPool extends EventEmitter {
}
_requeueEntryOnConnectionClose(connection) {
connection.queueEntry.requeueAttempts = connection.queueEntry.requeueAttempts + 1;
connection.queueEntry.requeueAttempts += 1;
this.logger.debug(
{
tnx: 'pool',
@@ -484,7 +473,7 @@ class SMTPPool extends EventEmitter {
* @param {Object} connection The PoolResource to remove
*/
_removeConnection(connection) {
let index = this._connections.indexOf(connection);
const index = this._connections.indexOf(connection);
if (index !== -1) {
this._connections.splice(index, 1);
@@ -501,7 +490,7 @@ class SMTPPool extends EventEmitter {
return callback();
}
let now = Date.now();
const now = Date.now();
if (this._rateLimit.counter < this._rateLimit.limit) {
return callback();
@@ -511,7 +500,9 @@ class SMTPPool extends EventEmitter {
if (this._rateLimit.checkpoint <= now - this._rateLimit.delta) {
return this._clearRateLimit();
} else if (!this._rateLimit.timeout) {
}
if (!this._rateLimit.timeout) {
this._rateLimit.timeout = setTimeout(() => this._clearRateLimit(), this._rateLimit.delta - (now - this._rateLimit.checkpoint));
this._rateLimit.checkpoint = now;
}
@@ -528,7 +519,7 @@ class SMTPPool extends EventEmitter {
// resume all paused connections
while (this._rateLimit.waiting.length) {
let cb = this._rateLimit.waiting.shift();
const cb = this._rateLimit.waiting.shift();
setImmediate(cb);
}
}
@@ -554,7 +545,7 @@ class SMTPPool extends EventEmitter {
});
}
let auth = new PoolResource(this).auth;
const auth = new PoolResource(this).auth;
this.getSocket(this.options, (err, socketOptions) => {
if (err) {
@@ -578,13 +569,10 @@ class SMTPPool extends EventEmitter {
options.host || '',
options.port || ''
);
options = shared.assign(false, options);
Object.keys(socketOptions).forEach(key => {
options[key] = socketOptions[key];
});
options = Object.assign(shared.assign(false, options), socketOptions);
}
let connection = new SMTPConnection(options);
const connection = new SMTPConnection(options);
let returned = false;
connection.once('error', err => {
@@ -604,7 +592,7 @@ class SMTPPool extends EventEmitter {
return callback(new Error('Connection closed'));
});
let finalize = () => {
const finalize = () => {
if (returned) {
return;
}
@@ -633,7 +621,7 @@ class SMTPPool extends EventEmitter {
finalize();
});
} else if (!auth && connection.allowsAuth && options.forceAuth) {
let err = new Error('Authentication info was not provided');
const err = new Error('Authentication info was not provided');
err.code = errors.ENOAUTH;
returned = true;

View File

@@ -23,7 +23,7 @@ class PoolResource extends EventEmitter {
if (this.options.auth) {
switch ((this.options.auth.type || '').toString().toUpperCase()) {
case 'OAUTH2': {
let oauth2 = new XOAuth2(this.options.auth, this.logger);
const oauth2 = new XOAuth2(this.options.auth, this.logger);
oauth2.provisionCallback =
(this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
this.auth = {
@@ -90,10 +90,7 @@ class PoolResource extends EventEmitter {
options.port || ''
);
options = assign(false, options);
Object.keys(socketOptions).forEach(key => {
options[key] = socketOptions[key];
});
options = Object.assign(assign(false, options), socketOptions);
}
this.connection = new SMTPConnection(options);
@@ -114,12 +111,12 @@ class PoolResource extends EventEmitter {
}
returned = true;
let timer = setTimeout(() => {
const timer = setTimeout(() => {
if (returned) {
return;
}
// still have not returned, this means we have an unexpected connection close
let err = new Error('Unexpected socket close');
const err = new Error('Unexpected socket close');
if (this.connection && this.connection._socket && this.connection._socket.upgrading) {
// starttls connection errors
err.code = errors.ETLS;
@@ -180,10 +177,10 @@ class PoolResource extends EventEmitter {
});
}
let envelope = mail.message.getEnvelope();
let messageId = mail.message.messageId();
const envelope = mail.message.getEnvelope();
const messageId = mail.message.messageId();
let recipients = [].concat(envelope.to || []);
const recipients = [].concat(envelope.to || []);
if (recipients.length > 3) {
recipients.push('...and ' + recipients.splice(2).length + ' more');
}
@@ -224,9 +221,8 @@ class PoolResource extends EventEmitter {
info.messageId = messageId;
setImmediate(() => {
let err;
if (this.messages >= this.options.maxMessages) {
err = new Error('Resource exhausted');
const err = new Error('Resource exhausted');
err.code = errors.EMAXLIMIT;
this.connection.close();
this.emit('error', err);

View File

@@ -49,11 +49,8 @@ class SMTPTransport extends EventEmitter {
component: this.options.component || 'smtp-transport'
});
// temporary object
let connection = new SMTPConnection(this.options);
this.name = 'SMTP';
this.version = packageData.version + '[client:' + connection.version + ']';
this.version = packageData.version + '[client:' + packageData.version + ']';
if (this.options.auth) {
this.auth = this.getAuth({});
@@ -77,24 +74,13 @@ class SMTPTransport extends EventEmitter {
return this.auth;
}
let hasAuth = false;
let authData = {};
const authData = Object.assign(
{},
this.options.auth && typeof this.options.auth === 'object' ? this.options.auth : {},
authOpts && typeof authOpts === 'object' ? authOpts : {}
);
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) {
if (Object.keys(authData).length === 0) {
return false;
}
@@ -103,7 +89,7 @@ class SMTPTransport extends EventEmitter {
if (!authData.service && !authData.user) {
return false;
}
let oauth2 = new XOAuth2(authData, this.logger);
const 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));
@@ -160,13 +146,10 @@ class SMTPTransport extends EventEmitter {
);
// only copy options if we need to modify it
options = shared.assign(false, options);
Object.keys(socketOptions).forEach(key => {
options[key] = socketOptions[key];
});
options = Object.assign(shared.assign(false, options), socketOptions);
}
let connection = new SMTPConnection(options);
const connection = new SMTPConnection(options);
connection.once('error', err => {
if (returned) {
@@ -182,13 +165,13 @@ class SMTPTransport extends EventEmitter {
return;
}
let timer = setTimeout(() => {
const 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');
const err = new Error('Unexpected socket close');
if (connection && connection._socket && connection._socket.upgrading) {
// starttls connection errors
err.code = errors.ETLS;
@@ -203,11 +186,11 @@ class SMTPTransport extends EventEmitter {
}
});
let sendMessage = () => {
let envelope = mail.message.getEnvelope();
let messageId = mail.message.messageId();
const sendMessage = () => {
const envelope = mail.message.getEnvelope();
const messageId = mail.message.messageId();
let recipients = [].concat(envelope.to || []);
const recipients = [].concat(envelope.to || []);
if (recipients.length > 3) {
recipients.push('...and ' + recipients.splice(2).length + ' more');
}
@@ -272,7 +255,7 @@ class SMTPTransport extends EventEmitter {
return;
}
let auth = this.getAuth(mail.data.auth);
const auth = this.getAuth(mail.data.auth);
if (auth && (connection.allowsAuth || options.forceAuth)) {
connection.login(auth, err => {
@@ -335,13 +318,10 @@ class SMTPTransport extends EventEmitter {
options.port || ''
);
options = shared.assign(false, options);
Object.keys(socketOptions).forEach(key => {
options[key] = socketOptions[key];
});
options = Object.assign(shared.assign(false, options), socketOptions);
}
let connection = new SMTPConnection(options);
const connection = new SMTPConnection(options);
let returned = false;
connection.once('error', err => {
@@ -361,7 +341,7 @@ class SMTPTransport extends EventEmitter {
return callback(new Error('Connection closed'));
});
let finalize = () => {
const finalize = () => {
if (returned) {
return;
}
@@ -375,7 +355,7 @@ class SMTPTransport extends EventEmitter {
return;
}
let authData = this.getAuth({});
const authData = this.getAuth({});
if (authData && (connection.allowsAuth || options.forceAuth)) {
connection.login(authData, err => {
@@ -392,7 +372,7 @@ class SMTPTransport extends EventEmitter {
finalize();
});
} else if (!authData && connection.allowsAuth && options.forceAuth) {
let err = new Error('Authentication info was not provided');
const err = new Error('Authentication info was not provided');
err.code = errors.ENOAUTH;
returned = true;

View File

@@ -18,7 +18,7 @@ class StreamTransport {
constructor(options) {
options = options || {};
this.options = options || {};
this.options = options;
this.name = 'StreamTransport';
this.version = packageData.version;
@@ -40,10 +40,10 @@ class StreamTransport {
// We probably need this in the output
mail.message.keepBcc = true;
let envelope = mail.data.envelope || mail.message.getEnvelope();
let messageId = mail.message.messageId();
const envelope = mail.data.envelope || mail.message.getEnvelope();
const messageId = mail.message.messageId();
let recipients = [].concat(envelope.to || []);
const recipients = [].concat(envelope.to || []);
if (recipients.length > 3) {
recipients.push('...and ' + recipients.splice(2).length + ' more');
}
@@ -91,13 +91,13 @@ class StreamTransport {
);
});
return done(null, {
envelope: mail.data.envelope || mail.message.getEnvelope(),
envelope,
messageId,
message: stream
});
}
let chunks = [];
const chunks = [];
let chunklen = 0;
stream.on('readable', () => {
let chunk;
@@ -123,7 +123,7 @@ class StreamTransport {
stream.on('end', () =>
done(null, {
envelope: mail.data.envelope || mail.message.getEnvelope(),
envelope,
messageId,
message: Buffer.concat(chunks, chunklen)
})

View File

@@ -4,16 +4,17 @@ const services = require('./services.json');
const normalized = {};
Object.keys(services).forEach(key => {
let service = services[key];
const service = services[key];
const normalizedService = normalizeService(service);
normalized[normalizeKey(key)] = normalizeService(service);
normalized[normalizeKey(key)] = normalizedService;
[].concat(service.aliases || []).forEach(alias => {
normalized[normalizeKey(alias)] = normalizeService(service);
normalized[normalizeKey(alias)] = normalizedService;
});
[].concat(service.domains || []).forEach(domain => {
normalized[normalizeKey(domain)] = normalizeService(service);
normalized[normalizeKey(domain)] = normalizedService;
});
});
@@ -22,11 +23,10 @@ function normalizeKey(key) {
}
function normalizeService(service) {
let filter = ['domains', 'aliases'];
let response = {};
const response = {};
Object.keys(service).forEach(key => {
if (filter.indexOf(key) < 0) {
if (!['domains', 'aliases'].includes(key)) {
response[key] = service[key];
}
});

View File

@@ -1,6 +1,6 @@
'use strict';
const Stream = require('stream').Stream;
const { Stream } = require('stream');
const nmfetch = require('../fetch');
const crypto = require('crypto');
const shared = require('../shared');
@@ -42,13 +42,13 @@ class XOAuth2 extends Stream {
if (options && options.serviceClient) {
if (!options.privateKey || !options.user) {
let err = new Error('Options "privateKey" and "user" are required for service account!');
const err = new Error('Options "privateKey" and "user" are required for service account!');
err.code = errors.EOAUTH2;
setImmediate(() => this.emit('error', err));
return;
}
let serviceRequestTimeout = Math.min(Math.max(Number(this.options.serviceRequestTimeout) || 0, 0), 3600);
const serviceRequestTimeout = Math.min(Math.max(Number(this.options.serviceRequestTimeout) || 0, 0), 3600);
this.options.serviceRequestTimeout = serviceRequestTimeout || 5 * 60;
}
@@ -72,7 +72,7 @@ class XOAuth2 extends Stream {
if (this.options.expires && Number(this.options.expires)) {
this.expires = this.options.expires;
} else {
let timeout = Math.max(Number(this.options.timeout) || 0, 0);
const timeout = Math.max(Number(this.options.timeout) || 0, 0);
this.expires = (timeout && Date.now() + timeout * 1000) || 0;
}
@@ -123,7 +123,7 @@ class XOAuth2 extends Stream {
'Cannot renew access token for %s: No refresh mechanism available',
this.options.user
);
let err = new Error("Can't create new access token for user");
const err = new Error("Can't create new access token for user");
err.code = errors.EOAUTH2;
return callback(err);
}
@@ -210,8 +210,8 @@ class XOAuth2 extends Stream {
let loggedUrlOptions;
if (this.options.serviceClient) {
// service account - https://developers.google.com/identity/protocols/OAuth2ServiceAccount
let iat = Math.floor(Date.now() / 1000); // unix time
let tokenData = {
const iat = Math.floor(Date.now() / 1000); // unix time
const tokenData = {
iss: this.options.serviceClient,
scope: this.options.scope || 'https://mail.google.com/',
sub: this.options.user,
@@ -223,7 +223,7 @@ class XOAuth2 extends Stream {
try {
token = this.jwtSignRS256(tokenData);
} catch (_err) {
let err = new Error("Can't generate token. Check your auth options");
const err = new Error("Can't generate token. Check your auth options");
err.code = errors.EOAUTH2;
return callback(err);
}
@@ -239,7 +239,7 @@ class XOAuth2 extends Stream {
};
} else {
if (!this.options.refreshToken) {
let err = new Error("Can't create new access token for user");
const err = new Error("Can't create new access token for user");
err.code = errors.EOAUTH2;
return callback(err);
}
@@ -260,10 +260,8 @@ class XOAuth2 extends Stream {
};
}
Object.keys(this.options.customParams).forEach(key => {
urlOptions[key] = this.options.customParams[key];
loggedUrlOptions[key] = this.options.customParams[key];
});
Object.assign(urlOptions, this.options.customParams);
Object.assign(loggedUrlOptions, this.options.customParams);
this.logger.debug(
{
@@ -298,19 +296,15 @@ class XOAuth2 extends Stream {
'Response: %s',
(body || '').toString()
);
let err = new Error('Invalid authentication response');
const err = new Error('Invalid authentication response');
err.code = errors.EOAUTH2;
return callback(err);
}
let logData = {};
Object.keys(data).forEach(key => {
if (key !== 'access_token') {
logData[key] = data[key];
} else {
logData[key] = (data[key] || '').toString().substr(0, 6) + '...';
}
});
const logData = Object.assign({}, data);
if (logData.access_token) {
logData.access_token = (logData.access_token || '').toString().substr(0, 6) + '...';
}
this.logger.debug(
{
@@ -331,7 +325,7 @@ class XOAuth2 extends Stream {
if (data.error_uri) {
errorMessage += ' (' + data.error_uri + ')';
}
let err = new Error(errorMessage);
const err = new Error(errorMessage);
err.code = errors.EOAUTH2;
return callback(err);
}
@@ -341,7 +335,7 @@ class XOAuth2 extends Stream {
return callback(null, this.accessToken);
}
let err = new Error('No access token');
const err = new Error('No access token');
err.code = errors.EOAUTH2;
return callback(err);
});
@@ -354,7 +348,7 @@ class XOAuth2 extends Stream {
* @return {String} Base64 encoded token for IMAP or SMTP login
*/
buildXOAuth2Token(accessToken) {
let authData = ['user=' + (this.options.user || ''), 'auth=Bearer ' + (accessToken || this.accessToken), '', ''];
const authData = ['user=' + (this.options.user || ''), 'auth=Bearer ' + (accessToken || this.accessToken), '', ''];
return Buffer.from(authData.join('\x01'), 'utf-8').toString('base64');
}
@@ -373,10 +367,10 @@ class XOAuth2 extends Stream {
postRequest(url, payload, params, callback) {
let returned = false;
let chunks = [];
const chunks = [];
let chunklen = 0;
let req = nmfetch(url, {
const req = nmfetch(url, {
method: 'post',
headers: params.customHeaders,
body: payload,
@@ -434,7 +428,7 @@ class XOAuth2 extends Stream {
*/
jwtSignRS256(payload) {
payload = ['{"alg":"RS256","typ":"JWT"}', JSON.stringify(payload)].map(val => this.toBase64URL(val)).join('.');
let signature = crypto.createSign('RSA-SHA256').update(payload).sign(this.options.privateKey);
const signature = crypto.createSign('RSA-SHA256').update(payload).sign(this.options.privateKey);
return payload + '.' + this.toBase64URL(signature);
}
}

18
node_modules/nodemailer/package.json generated vendored
View File

@@ -1,6 +1,6 @@
{
"name": "nodemailer",
"version": "8.0.2",
"version": "8.0.7",
"description": "Easy as cake e-mail sending from your Node.js applications",
"main": "lib/nodemailer.js",
"scripts": {
@@ -10,7 +10,8 @@
"format:check": "prettier --check \"**/*.{js,json,md}\"",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"update": "rm -rf node_modules/ package-lock.json && ncu -u && npm install"
"update": "rm -rf node_modules/ package-lock.json && ncu -u && npm install",
"test:syntax": "docker run --rm -v \"$PWD:/app:ro\" -w /app node:6-alpine node test/syntax-compat.js"
},
"repository": {
"type": "git",
@@ -26,20 +27,19 @@
},
"homepage": "https://nodemailer.com/",
"devDependencies": {
"@aws-sdk/client-sesv2": "3.1004.0",
"@aws-sdk/client-sesv2": "3.1037.0",
"bunyan": "1.8.15",
"c8": "11.0.0",
"eslint": "10.0.3",
"eslint": "10.2.1",
"eslint-config-prettier": "10.1.8",
"globals": "17.4.0",
"globals": "17.5.0",
"libbase64": "1.3.0",
"libmime": "5.3.7",
"libmime": "5.3.8",
"libqp": "2.1.1",
"nodemailer-ntlm-auth": "1.0.4",
"prettier": "3.8.1",
"prettier": "3.8.3",
"proxy": "1.0.2",
"proxy-test-server": "1.0.0",
"smtp-server": "3.18.1"
"smtp-server": "3.18.4"
},
"engines": {
"node": ">=6.0.0"