Comparing sensitive data, confidential files or internal emails?

Most legal and privacy policies prohibit uploading sensitive data online. Diffchecker Desktop ensures your confidential information never leaves your computer. Work offline and compare documents securely.

emailjs-3.6.0-to-4.0.0

Created Diff never expires
21 removals
703 lines
15 additions
699 lines
'use strict';
import { existsSync, open, read, closeSync, close } from 'fs';

import { hostname } from 'os';
Object.defineProperty(exports, '__esModule', { value: true });
import { Stream } from 'stream';

import { TextEncoder, TextDecoder } from 'util';
var fs = require('fs');
import { createHmac } from 'crypto';
var os = require('os');
import { EventEmitter } from 'events';
var stream = require('stream');
import { Socket } from 'net';
var util = require('util');
import { connect, TLSSocket, createSecureContext } from 'tls';
var crypto = require('crypto');
var events = require('events');
var net = require('net');
var tls = require('tls');


/*
/*
* Operator tokens and which tokens are expected to end the sequence
* Operator tokens and which tokens are expected to end the sequence
*/
*/
const OPERATORS = new Map([
const OPERATORS = new Map([
['"', '"'],
['"', '"'],
['(', ')'],
['(', ')'],
['<', '>'],
['<', '>'],
[',', ''],
[',', ''],
// Groups are ended by semicolons
// Groups are ended by semicolons
[':', ';'],
[':', ';'],
// Semicolons are not a legal delimiter per the RFC2822 grammar other
// Semicolons are not a legal delimiter per the RFC2822 grammar other
// than for terminating a group, but they are also not valid for any
// than for terminating a group, but they are also not valid for any
// other use in this context. Given that some mail clients have
// other use in this context. Given that some mail clients have
// historically allowed the semicolon as a delimiter equivalent to the
// historically allowed the semicolon as a delimiter equivalent to the
// comma in their UI, it makes sense to treat them the same as a comma
// comma in their UI, it makes sense to treat them the same as a comma
// when used outside of a group.
// when used outside of a group.
[';', ''],
[';', ''],
]);
]);
/**
/**
* Tokenizes the original input string
* Tokenizes the original input string
*
*
* @param {string | string[] | undefined} address string(s) to tokenize
* @param {string | string[] | undefined} address string(s) to tokenize
* @return {AddressToken[]} An array of operator|text tokens
* @return {AddressToken[]} An array of operator|text tokens
*/
*/
function tokenizeAddress(address = '') {
function tokenizeAddress(address = '') {
var _a, _b;
var _a, _b;
const tokens = [];
const tokens = [];
let token = undefined;
let token = undefined;
let operator = undefined;
let operator = undefined;
for (const character of address.toString()) {
for (const character of address.toString()) {
if (((_a = operator === null || operator === void 0 ? void 0 : operator.length) !== null && _a !== void 0 ? _a : 0) > 0 && character === operator) {
if (((_a = operator === null || operator === void 0 ? void 0 : operator.length) !== null && _a !== void 0 ? _a : 0) > 0 && character === operator) {
tokens.push({ type: 'operator', value: character });
tokens.push({ type: 'operator', value: character });
token = undefined;
token = undefined;
operator = undefined;
operator = undefined;
}
}
else if (((_b = operator === null || operator === void 0 ? void 0 : operator.length) !== null && _b !== void 0 ? _b : 0) === 0 && OPERATORS.has(character)) {
else if (((_b = operator === null || operator === void 0 ? void 0 : operator.length) !== null && _b !== void 0 ? _b : 0) === 0 && OPERATORS.has(character)) {
tokens.push({ type: 'operator', value: character });
tokens.push({ type: 'operator', value: character });
token = undefined;
token = undefined;
operator = OPERATORS.get(character);
operator = OPERATORS.get(character);
}
}
else {
else {
if (token == null) {
if (token == null) {
token = { type: 'text', value: character };
token = { type: 'text', value: character };
tokens.push(token);
tokens.push(token);
}
}
else {
else {
token.value += character;
token.value += character;
}
}
}
}
}
}
return tokens
return tokens
.map((x) => {
.map((x) => {
x.value = x.value.trim();
x.value = x.value.trim();
return x;
return x;
})
})
.filter((x) => x.value.length > 0);
.filter((x) => x.value.length > 0);
}
}
/**
/**
* Converts tokens for a single address into an address object
* Converts tokens for a single address into an address object
*
*
* @param {AddressToken[]} tokens Tokens object
* @param {AddressToken[]} tokens Tokens object
* @return {AddressObject[]} addresses object array
* @return {AddressObject[]} addresses object array
*/
*/
function convertAddressTokens(tokens) {
function convertAddressTokens(tokens) {
const addressObjects = [];
const addressObjects = [];
const groups = [];
const groups = [];
let addresses = [];
let addresses = [];
let comments = [];
let comments = [];
let texts = [];
let texts = [];
let state = 'text';
let state = 'text';
let isGroup = false;
let isGroup = false;
function handleToken(token) {
function handleToken(token) {
if (token.type === 'operator') {
if (token.type === 'operator') {
switch (token.value) {
switch (token.value) {
case '<':
case '<':
state = 'address';
state = 'address';
break;
break;
case '(':
case '(':
state = 'comment';
state = 'comment';
break;
break;
case ':':
case ':':
state = 'group';
state = 'group';
isGroup = true;
isGroup = true;
break;
break;
default:
default:
state = 'text';
state = 'text';
break;
break;
}
}
}
}
else if (token.value.length > 0) {
else if (token.value.length > 0) {
switch (state) {
switch (state) {
case 'address':
case 'address':
addresses.push(token.value);
addresses.push(token.value);
break;
break;
case 'comment':
case 'comment':
comments.push(token.value);
comments.push(token.value);
break;
break;
case 'group':
case 'group':
groups.push(token.value);
groups.push(token.value);
break;
break;
default:
default:
texts.push(token.value);
texts.push(token.value);
break;
break;
}
}
}
}
}
}
// Filter out <addresses>, (comments) and regular text
// Filter out <addresses>, (comments) and regular text
for (const token of tokens) {
for (const token of tokens) {
handleToken(token);
handleToken(token);
}
}
// If there is no text but a comment, replace the two
// If there is no text but a comment, replace the two
if (texts.length === 0 && comments.length > 0) {
if (texts.length === 0 && comments.length > 0) {
texts = [...comments];
texts = [...comments];
comments = [];
comments = [];
}
}
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
if (isGroup) {
if (isGroup) {
addressObjects.push({
addressObjects.push({
name: texts.length === 0 ? undefined : texts.join(' '),
name: texts.length === 0 ? undefined : texts.join(' '),
group: groups.length > 0 ? addressparser(groups.join(',')) : [],
group: groups.length > 0 ? addressparser(groups.join(',')) : [],
});
});
}
}
else {
else {
// If no address was found, try to detect one from regular text
// If no address was found, try to detect one from regular text
if (addresses.length === 0 && texts.length > 0) {
if (addresses.length === 0 && texts.length > 0) {
for (let i = texts.length - 1; i >= 0; i--) {
for (let i = texts.length - 1; i >= 0; i--) {
if (texts[i].match(/^[^@\s]+@[^@\s]+$/)) {
if (texts[i].match(/^[^@\s]+@[^@\s]+$/)) {
addresses = texts.splice(i, 1);
addresses = texts.splice(i, 1);
break;
break;
}
}
}
}
// still no address
// still no address
if (addresses.length === 0) {
if (addresses.length === 0) {
for (let i = texts.length - 1; i >= 0; i--) {
for (let i = texts.length - 1; i >= 0; i--) {
texts[i] = texts[i]
texts[i] = texts[i]
.replace(/\s*\b[^@\s]+@[^@\s]+\b\s*/, (address) => {
.replace(/\s*\b[^@\s]+@[^@\s]+\b\s*/, (address) => {
if (addresses.length === 0) {
if (addresses.length === 0) {
addresses = [address.trim()];
addresses = [address.trim()];
return ' ';
return ' ';
}
}
else {
else {
return address;
return address;
}
}
})
})
.trim();
.trim();
if (addresses.length > 0) {
if (addresses.length > 0) {
break;
break;
}
}
}
}
}
}
}
}
// If there's still is no text but a comment exixts, replace the two
// If there's still is no text but a comment exixts, replace the two
if (texts.length === 0 && comments.length > 0) {
if (texts.length === 0 && comments.length > 0) {
texts = [...comments];
texts = [...comments];
comments = [];
comments = [];
}
}
// Keep only the first address occurence, push others to regular text
// Keep only the first address occurence, push others to regular text
if (addresses.length > 1) {
if (addresses.length > 1) {
texts = [...texts, ...addresses.splice(1)];
texts = [...texts, ...addresses.splice(1)];
}
}
if (addresses.length === 0 && isGroup) {
if (addresses.length === 0 && isGroup) {
return [];
return [];
}
}
else {
else {
// Join values with spaces
// Join values with spaces
let address = addresses.join(' ');
let address = addresses.join(' ');
let name = texts.length === 0 ? address : texts.join(' ');
let name = texts.length === 0 ? address : texts.join(' ');
if (address === name) {
if (address === name) {
if (address.match(/@/)) {
if (address.match(/@/)) {
name = '';
name = '';
}
}
else {
else {
address = '';
address = '';
}
}
}
}
addressObjects.push({ address, name });
addressObjects.push({ address, name });
}
}
}
}
return addressObjects;
return addressObjects;
}
}
/**
/**
* Parses structured e-mail addresses from an address field
* Parses structured e-mail addresses from an address field
*
*
* Example:
* Example:
*
*
* "Name <address@domain>"
* "Name <address@domain>"
*
*
* will be converted to
* will be converted to
*
*
* [{name: "Name", address: "address@domain"}]
* [{name: "Name", address: "address@domain"}]
*
*
* @param {string | string[] | undefined} address Address field
* @param {string | string[] | undefined} address Address field
* @return {AddressObject[]} An array of address objects
* @return {AddressObject[]} An array of address objects
*/
*/
function addressparser(address) {
function addressparser(address) {
const addresses = [];
const addresses = [];
let tokens = [];
let tokens = [];
for (const token of tokenizeAddress(address)) {
for (const token of tokenizeAddress(address)) {
if (token.type === 'operator' &&
if (token.type === 'operator' &&
(token.value === ',' || token.value === ';')) {
(token.value === ',' || token.value === ';')) {
if (tokens.length > 0) {
if (tokens.length > 0) {
addresses.push(...convertAddressTokens(tokens));
addresses.push(...convertAddressTokens(tokens));
}
}
tokens = [];
tokens = [];
}
}
else {
else {
tokens.push(token);
tokens.push(token);
}
}
}
}
if (tokens.length > 0) {
if (tokens.length > 0) {
addresses.push(...convertAddressTokens(tokens));
addresses.push(...convertAddressTokens(tokens));
}
}
return addresses;
return addresses;
}
}


/**
/**
* @param {Date} [date] an optional date to convert to RFC2822 format
* @param {Date} [date] an optional date to convert to RFC2822 format
* @param {boolean} [useUtc] whether to parse the date as UTC (default: false)
* @param {boolean} [useUtc] whether to parse the date as UTC (default: false)
* @returns {string} the converted date
* @returns {string} the converted date
*/
*/
function getRFC2822Date(date = new Date(), useUtc = false) {
function getRFC2822Date(date = new Date(), useUtc = false) {
if (useUtc) {
if (useUtc) {
return getRFC2822DateUTC(date);
return getRFC2822DateUTC(date);
}
}
const dates = date
const dates = date
.toString()
.toString()
.replace('GMT', '')
.replace('GMT', '')
.replace(/\s\(.*\)$/, '')
.replace(/\s\(.*\)$/, '')
.split(' ');
.split(' ');
dates[0] = dates[0] + ',';
dates[0] = dates[0] + ',';
const day = dates[1];
const day = dates[1];
dates[1] = dates[2];
dates[1] = dates[2];
dates[2] = day;
dates[2] = day;
return dates.join(' ');
return dates.join(' ');
}
}
/**
/**
* @param {Date} [date] an optional date to convert to RFC2822 format (UTC)
* @param {Date} [date] an optional date to convert to RFC2822 format (UTC)
* @returns {string} the converted date
* @returns {string} the converted date
*/
*/
function getRFC2822DateUTC(date = new Date()) {
function getRFC2822DateUTC(date = new Date()) {
const dates = date.toUTCString().split(' ');
const dates = date.toUTCString().split(' ');
dates.pop(); // remove timezone
dates.pop(); // remove timezone
dates.push('+0000');
dates.push('+0000');
return dates.join(' ');
return dates.join(' ');
}
}
/**
/**
* RFC 2822 regex
* RFC 2822 regex
* @see https://tools.ietf.org/html/rfc2822#section-3.3
* @see https://tools.ietf.org/html/rfc2822#section-3.3
* @see https://github.com/moment/moment/blob/a831fc7e2694281ce31e4f090bbcf90a690f0277/src/lib/create/from-string.js#L101
* @see https://github.com/moment/moment/blob/a831fc7e2694281ce31e4f090bbcf90a690f0277/src/lib/create/from-string.js#L101
*/
*/
const rfc2822re = /^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/.compile();
const rfc2822re = /^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/;
/**
/**
* @param {string} [date] a string to check for conformance to the [rfc2822](https://tools.ietf.org/html/rfc2822#section-3.3) standard
* @param {string} [date] a string to check for conformance to the [rfc2822](https://tools.ietf.org/html/rfc2822#section-3.3) standard
* @returns {boolean} the result of the conformance check
* @returns {boolean} the result of the conformance check
*/
*/
function isRFC2822Date(date) {
function isRFC2822Date(date) {
return rfc2822re.test(date);
return rfc2822re.test(date);
}
}


// adapted from https://github.com/emailjs/emailjs-mime-codec/blob/6909c706b9f09bc0e5c3faf48f723cca53e5b352/src/mimecodec.js
// adapted from https://github.com/emailjs/emailjs-mime-codec/blob/6909c706b9f09bc0e5c3faf48f723cca53e5b352/src/mimecodec.js
const encoder = new util.TextEncoder();
const encoder = new TextEncoder();
/**
/**
* @see https://tools.ietf.org/html/rfc2045#section-6.7
* @see https://tools.ietf.org/html/rfc2045#section-6.7
*/
*/
const RANGES = [
const RANGES = [
[0x09],
[0x09],
[0x0a],
[0x0a],
[0x0d],
[0x0d],
[0x20, 0x3c],
[0x20, 0x3c],
[0x3e, 0x7e], // >?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}
[0x3e, 0x7e], // >?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}
];
];
const LOOKUP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('');
const LOOKUP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('');
const MAX_CHUNK_LENGTH = 16383; // must be multiple of 3
const MAX_CHUNK_LENGTH = 16383; // must be multiple of 3
const MAX_MIME_WORD_LENGTH = 52;
const MAX_MIME_WORD_LENGTH = 52;
const MAX_B64_MIME_WORD_BYTE_LENGTH = 39;
const MAX_B64_MIME_WORD_BYTE_LENGTH = 39;
function tripletToBase64(num) {
function tripletToBase64(num) {
return (LOOKUP[(num >> 18) & 0x3f] +
return (LOOKUP[(num >> 18) & 0x3f] +
LOOKUP[(num >> 12) & 0x3f] +
LOOKUP[(num >> 12) & 0x3f] +
LOOKUP[(num >> 6) & 0x3f] +
LOOKUP[(num >> 6) & 0x3f] +
LOOKUP[num & 0x3f]);
LOOKUP[num & 0x3f]);
}
}
function encodeChunk(uint8, start, end) {
function encodeChunk(uint8, start, end) {
let output = '';
let output = '';
for (let i = start; i < end; i += 3) {
for (let i = start; i < end; i += 3) {
output += tripletToBase64((uint8[i] << 16) + (uint8[i + 1] << 8) + uint8[i + 2]);
output += tripletToBase64((uint8[i] << 16) + (uint8[i + 1] << 8) + uint8[i + 2]);
}
}
return output;
return output;
}
}
function encodeBase64(data) {
function encodeBase64(data) {
const len = data.length;
const len = data.length;
const extraBytes = len % 3; // if we have 1 byte left, pad 2 bytes
const extraBytes = len % 3; // if we have 1 byte left, pad 2 bytes
let output = '';
let output = '';
// go through the array every three bytes, we'll deal with trailing stuff later
// go through the array every three bytes, we'll deal with trailing stuff later
for (let i = 0, len2 = len - extraBytes; i < len2; i += MAX_CHUNK_LENGTH) {
for (let i = 0, len2 = len - extraBytes; i < len2; i += MAX_CHUNK_LENGTH) {
output += encodeChunk(data, i, i + MAX_CHUNK_LENGTH > len2 ? len2 : i + MAX_CHUNK_LENGTH);
output += encodeChunk(data, i, i + MAX_CHUNK_LENGTH > len2 ? len2 : i + MAX_CHUNK_LENGTH);
}
}
// pad the end with zeros, but make sure to not forget the extra bytes
// pad the end with zeros, but make sure to not forget the extra bytes
if (extraBytes === 1) {
if (extraBytes === 1) {
const tmp = data[len - 1];
const tmp = data[len - 1];
output += LOOKUP[tmp >> 2];
output += LOOKUP[tmp >> 2];
output += LOOKUP[(tmp << 4) & 0x3f];
output += LOOKUP[(tmp << 4) & 0x3f];
output += '==';
output += '==';
}
}
else if (extraBytes === 2) {
else if (extraBytes === 2) {
const tmp = (data[len - 2] << 8) + data[len - 1];
const tmp = (data[len - 2] << 8) + data[len - 1];
output += LOOKUP[tmp >> 10];
output += LOOKUP[tmp >> 10];
output += LOOKUP[(tmp >> 4) & 0x3f];
output += LOOKUP[(tmp >> 4) & 0x3f];
output += LOOKUP[(tmp << 2) & 0x3f];
output += LOOKUP[(tmp << 2) & 0x3f];
output += '=';
output += '=';
}
}
return output;
return output;
}
}
/**
/**
* Splits a mime encoded string. Needed for dividing mime words into smaller chunks
* Splits a mime encoded string. Needed for dividing mime words into smaller chunks
*
*
* @param {string} str Mime encoded string to be split up
* @param {string} str Mime encoded string to be split up
* @param {number} maxlen Maximum length of characters for one part (minimum 12)
* @param {number} maxlen Maximum length of characters for one part (minimum 12)
* @return {string[]} lines
* @return {string[]} lines
*/
*/
function splitMimeEncodedString(str, maxlen = 12) {
function splitMimeEncodedString(str, maxlen = 12) {
const minWordLength = 12; // require at least 12 symbols to fit possible 4 octet UTF-8 sequences
const minWordLength = 12; // require at least 12 symbols to fit possible 4 octet UTF-8 sequences
const maxWordLength = Math.max(maxlen, minWordLength);
const maxWordLength = Math.max(maxlen, minWordLength);
const lines = [];
const lines = [];
while (str.length) {
while (str.length) {
let curLine = str.substr(0, maxWordLength);
let curLine = str.substr(0, maxWordLength);
const match = curLine.match(/=[0-9A-F]?$/i); // skip incomplete escaped char
const match = curLine.match(/=[0-9A-F]?$/i); // skip incomplete escaped char
if (match) {
if (match) {
curLine = curLine.substr(0, match.index);
curLine = curLine.substr(0, match.index);
}
}
let done = false;
let done = false;
while (!done) {
while (!done) {
let chr;
let chr;
done = true;
done = true;
const match = str.substr(curLine.length).match(/^=([0-9A-F]{2})/i); // check if not middle of a unicode char sequence
const match = str.substr(curLine.length).match(/^=([0-9A-F]{2})/i); // check if not middle of a unicode char sequence
if (match) {
if (match) {
chr = parseInt(match[1], 16);
chr = parseInt(match[1], 16);
// invalid sequence, move one char back anc recheck
// invalid sequence, move one char back anc recheck
if (chr < 0xc2 && chr > 0x7f) {
if (chr < 0xc2 && chr > 0x7f) {
curLine = curLine.substr(0, curLine.length - 3);
curLine = curLine.substr(0, curLine.length - 3);
done = false;
done = false;
}
}
}
}
}
}
if (curLine.length) {
if (curLine.length) {
lines.push(curLine);
lines.push(curLine);
}
}
str = str.substr(curLine.length);
str = str.substr(curLine.length);
}
}
return lines;
return lines;
}
}
/**
/**
*
*
* @param {number} nr number
* @param {number} nr number
* @returns {boolean} if number is in range
* @returns {boolean} if number is in range
*/
*/
function checkRanges(nr) {
function checkRanges(nr) {
return RANGES.reduce((val, range) => val ||
return RANGES.reduce((val, range) => val ||
(range.length === 1 && nr === range[0]) ||
(range.length === 1 && nr === range[0]) ||
(range.length === 2 && nr >= range[0] && nr <= range[1]), false);
(range.length === 2 && nr >= range[0] && nr <= range[1]), false);
}
}
/**
/**
* Encodes all non printable and non ascii bytes to =XX form, where XX is the
* Encodes all non printable and non ascii bytes to =XX form, where XX is the
* byte value in hex. This function does not convert linebreaks etc. it
* byte value in hex. This function does not convert linebreaks etc. it
* only escapes character sequences
* only escapes character sequences
*
*
* NOTE: Encoding support depends on util.TextDecoder, which is severely limited
* NOTE: Encoding support depends on util.TextDecoder, which is severely limited
* prior to Node.js 13.
* prior to Node.js 13.
*
*
* @see https://nodejs.org/api/util.html#util_whatwg_supported_encodings
* @see https://nodejs.org/api/util.html#util_whatwg_supported_encodings
* @see https://github.com/nodejs/node/issues/19214
* @see https://github.com/nodejs/node/issues/19214
*
*
* @param {string|Uint8Array} data Either a string or an Uint8Array
* @param {string|Uint8Array} data Either a string or an Uint8Array
* @param {string} encoding WHATWG supported encoding
* @param {string} encoding WHATWG supported encoding
* @return {string} Mime encoded string
* @return {string} Mime encoded string
*/
*/
function mimeEncode(data = '', encoding = 'utf-8') {
function mimeEncode(data = '', encoding = 'utf-8') {
const decoder = new util.TextDecoder(encoding);
const decoder = new TextDecoder(encoding);
const buffer = typeof data === 'string'
const buffer = typeof data === 'string'
? encoder.encode(data)
? encoder.encode(data)
: encoder.encode(decoder.decode(data));
: encoder.encode(decoder.decode(data));
return buffer.reduce((aggregate, ord, index) => checkRanges(ord) &&
return buffer.reduce((aggregate, ord, index) => checkRanges(ord) &&
!((ord === 0x20 || ord === 0x09) &&
!((ord === 0x20 || ord === 0x09) &&
(index === buffer.length - 1 ||
(index === buffer.length - 1 ||
buffer[index + 1] === 0x0a ||
buffer[index + 1] === 0x0a ||
buffer[index + 1] === 0x0d))
buffer[index + 1] === 0x0d))
? // if the char is in allowed range, then keep as is, unless it is a ws in the end of a line
? // if the char is in allowed range, then keep as is, unless it is a ws in the end of a line
aggregate + String.fromCharCode(ord)
aggregate + String.fromCharCode(ord)
: `${aggregate}=${ord < 0x10 ? '0' : ''}${ord
: `${aggregate}=${ord < 0x10 ? '0' : ''}${ord
.toString(16)
.toString(16)
.toUpperCase()}`, '');
.toUpperCase()}`, '');
}
}
/**
/**
* Encodes a string or an Uint8Array to an UTF-8 MIME Word
* Encodes a string or an Uint8Array to an UTF-8 MIME Word
*
*
* NOTE: Encoding support depends on util.TextDecoder, which is severely limited
* NOTE: Encoding support depends on util.TextDecoder, which is severely limited
* prior to Node.js 13.
* prior to Node.js 13.
*
*
* @see https://tools.ietf.org/html/rfc2047
* @see https://tools.ietf.org/html/rfc2047
* @see https://nodejs.org/api/util.html#util_whatwg_supported_encodings
* @see https://nodejs.org/api/util.html#util_whatwg_supported_encodings
* @see https://github.com/nodejs/node/issues/19214
* @see https://github.com/nodejs/node/issues/19214
*
*
* @param {string|Uint8Array} data String to be encoded
* @param {string|Uint8Array} data String to be encoded
* @param {'Q' | 'B'} mimeWordEncoding='Q' Encoding for the mime word, either Q or B
* @param {'Q' | 'B'} mimeWordEncoding='Q' Encoding for the mime word, either Q or B
* @param {string} encoding WHATWG supported encoding
* @param {string} encoding WHATWG supported encoding
* @return {string} Single or several mime words joined together
* @return {string} Single or several mime words joined together
*/
*/
function mimeWordEncode(data, mimeWordEncoding = 'Q', encoding = 'utf-8') {
function mimeWordEncode(data, mimeWordEncoding = 'Q', encoding = 'utf-8') {
let parts = [];
let parts = [];
const decoder = new util.TextDecoder(encoding);
const decoder = new TextDecoder(encoding);
const str = typeof data === 'string' ? data : decoder.decode(data);
const str = typeof data === 'string' ? data : decoder.decode(data);
if (mimeWordEncoding === 'Q') {
if (mimeWordEncoding === 'Q') {
const encodedStr = mimeEncode(str, encoding).replace(/[^a-z0-9!*+\-/=]/gi, (chr) => chr === ' '
const encodedStr = mimeEncode(str, encoding).replace(/[^a-z0-9!*+\-/=]/gi, (chr) => chr === ' '
? '_'
? '_'
: '=' +
: '=' +
(chr.charCodeAt(0) < 0x10 ? '0' : '') +
(chr.charCodeAt(0) < 0x10 ? '0' : '') +
chr.charCodeAt(0).toString(16).toUpperCase());
chr.charCodeAt(0).toString(16).toUpperCase());
parts =
parts =
encodedStr.length < MAX_MIME_WORD_LENGTH
encodedStr.length < MAX_MIME_WORD_LENGTH
? [encodedStr]
? [encodedStr]
: splitMimeEncodedString(encodedStr, MAX_MIME_WORD_LENGTH);
: splitMimeEncodedString(encodedStr, MAX_MIME_WORD_LENGTH);
}
}
else {
else {
// Fits as much as possible into every line without breaking utf-8 multibyte characters' octets up across lines
// Fits as much as possible into every line without breaking utf-8 multibyte characters' octets up across lines
let j = 0;
let j = 0;
let i = 0;
let i = 0;
while (i < str.length) {
while (i < str.length) {
if (encoder.encode(str.substring(j, i)).length >
if (encoder.encode(str.substring(j, i)).length >
MAX_B64_MIME_WORD_BYTE_LENGTH) {
MAX_B64_MIME_WORD_BYTE_LENGTH) {
// we went one character too far, substring at the char before
// we went one character too far, substring at the char before
parts.push(str.substring(j, i - 1));
parts.push(str.substring(j, i - 1));
j = i - 1;
j = i - 1;
}
}
else {
else {
i++;
i++;
}
}
}
}
// add the remainder of the string
// add the remainder of the string
str.substring(j) && parts.push(str.substring(j));
str.substring(j) && parts.push(str.substring(j));
parts = parts.map((x) => encoder.encode(x)).map((x) => encodeBase64(x));
parts = parts.map((x) => encoder.encode(x)).map((x) => encodeBase64(x));
}
}
return parts
return parts
.map((p) => `=?UTF-8?${mimeWordEncoding}?${p}?= `)
.map((p) => `=?UTF-8?${mimeWordEncoding}?${p}?= `)
.join('')
.join('')
.trim();
.trim();
}
}


const CRLF$1 = '\r\n';
const CRLF$1 = '\r\n';
/**
/**
* MIME standard wants 76 char chunks when sending out.
* MIME standard wants 76 char chunks when sending out.
*/
*/
const MIMECHUNK = 76;
const MIMECHUNK = 76;
/**
/**
* meets both base64 and mime divisibility
* meets both base64 and mime divisibility
*/
*/
const MIME64CHUNK = (MIMECHUNK * 6);
const MIME64CHUNK = (MIMECHUNK * 6);
/**
/**
* size of the message stream buffer
* size of the message stream buffer
*/
*/
const BUFFERSIZE = (MIMECHUNK * 24 * 7);
const BUFFERSIZE = (MIMECHUNK * 24 * 7);
let counter = 0;
let counter = 0;
function generateBoundary() {
function generateBoundary() {
let text = '';
let text = '';
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'()+_,-./:=?";
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'()+_,-./:=?";
for (let i = 0; i < 69; i++) {
for (let i = 0; i < 69; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
}
return text;
return text;
}
}
function convertPersonToAddress(person) {
function convertPersonToAddress(person) {
return addressparser(person)
return addressparser(person)
.map(({ name, address }) => {
.map(({ name, address }) => {
return name
return name
? `${mimeWordEncode(name).replace(/,/g, '=2C')} <${address}>`
? `${mimeWordEncode(name).replace(/,/g, '=2C')} <${address}>`
: address;
: address;
})
})
.join(', ');
.join(', ');
}
}
function convertDashDelimitedTextToSnakeCase(text) {
function convertDashDelimitedTextToSnakeCase(text) {
return text
return text
.toLowerCase()
.toLowerCase()
.replace(/^(.)|-(.)/g, (match) => match.toUpperCase());
.replace(/^(.)|-(.)/g, (match) => match.toUpperCase());
}
}
class Message {
class Message {
/**
/**
* Construct an rfc2822-compliant message object.
* Construct an rfc2822-compliant message object.
*
*
* Special notes:
* Special notes:
* - The `from` field is required.
* - The `from` field is required.
* - At least one `to`, `cc`, or `bcc` header is also required.
* - At least one `to`, `cc`, or `bcc` header is also required.
* - You can also add whatever other headers you want.
* - You can also add whatever other headers you want.
*
*
* @see https://tools.ietf.org/html/rfc2822
* @see https://tools.ietf.org/html/rfc2822
* @param {Partial<MessageHeaders>} headers Message headers
* @param {Partial<MessageHeaders>} headers Message headers
*/
*/
constructor(headers) {
constructor(headers) {
this.attachments = [];
this.attachments = [];
this.header = {
this.header = {
'message-id': `<${new Date().getTime()}.${counter++}.${process.pid}@${os.hostname()}>`,
'message-id': `<${new Date().getTime()}.${counter++}.${process.pid}@${hostname()}>`,
date: getRFC2822Date(),
date: getRFC2822Date(),
};
};
this.content = 'text/plain; charset=utf-8';
this.content = 'text/plain; charset=utf-8';
this.alternative = null;
this.alternative = null;
for (const header in headers) {
for (const header in headers) {
// allow user to override default content-type to override charset or send a single non-text message
// allow user to override default content-type to override charset or send a single non-text message
if (/^content-type$/i.test(header)) {
if (/^content-type$/i.test(header)) {
this.content = headers[header];
this.content = headers[header];
}
}
else if (header === 'text') {
else if (header === 'text') {
this.text = headers[header];
this.text = headers[header];
}
}
else if (header === 'attachment' &&
else if (header === 'attachment' &&
typeof headers[header] === 'object') {
typeof headers[header] === 'object') {
const attachment = headers[header];
const attachment = headers[header];
if (Array.isArray(attachment)) {
if (Array.isArray(attachment)) {
for (let i = 0; i < attachment.length; i++) {
for (let i = 0; i < attachment.length; i++) {
this.attach(attachment[i]);
this.attach(attachment[i]);
}
}
}
}
else if (attachment != null) {
else if (attachment != null) {
this.attach(attachment);
this.attach(attachment);
}
}
}
}
else if (header === 'subject') {
else if (header === 'subject') {
this.header.subject = mimeWordEncode(headers.subject);
this.header.subject = mimeWordEncode(headers.subject);
}
}
else if (/^(cc|bcc|to|from)/i.test(header)) {
else if (/^(cc|bcc|to|from)/i.test(header)) {
this.header[header.toLowerCase()] = convertPersonToAddress(headers[header]);
this.header[header.toLowerCase()] = convertPersonToAddress(headers[header]);
}
}
else {
else {
// allow any headers the user wants to set??
// allow any headers the user wants to set??
this.header[header.toLowerCase()] = headers[header];
this.header[header.toLowerCase()] = headers[header];
}
}
}
}
}
}
/**
/**
* Attach a file to the message.
* Attach a file to the message.
*
*
* Can be called multiple times, each adding a new attachment.
* Can be called multiple times, each adding a new attachment.
*
*
* @public
* @public
* @param {MessageAttachment} options attachment options
* @param {MessageAttachment} options attachment options
* @returns {Message} the current instance for chaining
* @returns {Message} the current instance for chaining
*/
*/
attach(options) {
attach(options) {
// sender can specify an attachment as an alternative
// sender can specify an attachment as an alternative
if (options.alternative) {
if (options.alternative) {
this.alternative = options;
this.alternative = options;
this.alternative.charset = options.charset || 'utf-8';
this.alternative.charset = options.charset || 'utf-8';
this.alternative.type = options.type || 'text/html';
this.alternative.type = options.type || 'text/html';
this.alternative.inline = true;
this.alternative.inline = true;
}
}
else {
else {
this.attachments.push(options);
this.attachments.push(options);
}
}
return this;
return this;
}
}
/**
/**
* @public
* @public
* @returns {{ isValid: boolean, validationError: (string | undefined) }} an object specifying whether this message is validly formatted, and the first validation error if it is not.
* @returns {{ isValid: boolean, validationError: (string | undefined) }} an object specifying whether this message is validly formatted, and the first validation error if it is not.
*/
*/
checkValidity() {
checkValidity() {
if (typeof this.header.from !== 'string' &&
if (typeof this.header.from !== 'string' &&
Array.isArray(this.header.from) === false) {
Array.isArray(this.header.from) === false) {
return {
return {
isValid: false,
isValid: false,
validationError: 'Message must have a `from` header',
validationError: 'Message must have a `from` header',
};
};
}
}
if (typeof this.header.to !== 'string' &&
if (typeof this.header.to !== 'string' &&
Array.isArray(this.header.to) === false &&
Array.isArray(this.header.to) === false &&
typeof this.header.cc !== 'string' &&
typeof this.header.cc !== 'string' &&
Array.isArray(this.header.cc) === false &&
Array.isArray(this.header.cc) === false &&
typeof this.header.bcc !== 'string' &&
typeof this.header.bcc !== 'string' &&
Array.isArray(this.header.bcc) === false) {
Array.isArray(this.header.bcc) === false) {
return {
return {
isValid: false,
isValid: false,
validationError: 'Message must have at least one `to`, `cc`, or `bcc` header',
validationError: 'Message must have at least one `to`, `cc`, or `bcc` header',
};
};
}
}
if (this.attachments.length > 0) {
if (this.attachments.length > 0) {
const failed = [];
const failed = [];
this.attachments.forEach((attachment) => {
this.attachments.forEach((attachment) => {
if (attachment.path) {
if (attachment.path) {
if (fs.existsSync(attachment.path) === false) {
if (existsSync(attachment.path) === false) {
failed.push(`${attachment.path} does not exist`);
failed.push(`${attachment.path} does not exist`);
}
}
}
}
else if (attachment.stream) {
else if (attachment.stream) {
if (!attachment.stream.readable) {
if (!attachment.stream.readable) {
failed.push('attachment stream is not readable');
failed.push('attachment stream is not readable');
}
}
}
}
else if (!attachment.data) {
else if (!attachment.data) {
failed.push('attachment has no data associated with it');
failed.push('attachment has no data associated with it');
}
}
});
});
return {
return {
isValid: failed.length === 0,
isValid: failed.length === 0,
validationError: failed.join(', '),
validationError: failed.join(', '),
};
};
}
}
return { isValid: true, validationError: undefined };
return { isValid: true, validationError: undefined };
}
}
/**
/**
* @public
* @public
* @deprecated does not conform to the `errback` style followed by the rest of the library, and will be removed in the next major version. use `checkValidity` instead.
* @deprecated does not conform to the `errback` style followed by the rest of the library, and will be removed in the next major version. use `checkValidity` instead.
* @param {function(isValid: boolean, invalidReason: (string | undefined)): void} callback .
* @param {function(isValid: boolean, invalidReason: (string | undefined)): void} callback .
* @returns {void}
* @returns {void}
*/
*/
valid(callback) {
valid(callback) {
const { isValid, validationError } = this.checkValidity();
const { isValid, validationError } = this.checkValidity();
callback(isValid, validationError);
callback(isValid, validationError);
}
}
/**
/**
* @public
* @public
* @returns {MessageStream} a stream of the current message
* @returns {MessageStream} a stream of the current message
*/
*/
stream() {
stream() {
return new MessageStream(this);
return new MessageStream(this);
}
}
/**
/**
* @public
* @public
* @param {function(Error, string): void} callback the function to call with the error and buffer
* @param {function(Error, string): void} callback the function to call with the error and buffer
* @returns {void}
* @returns {void}
*/
*/
read(callback) {
read(callback) {
let buffer = '';
let buffer = '';
const str = this.stream();
const str = this.stream();
str.on('data', (data) => (buffer += data));
str.on('data', (data) => (buffer += data));
str.on('end', (err) => callback(err, buffer));
str.on('end', (err) => callback(err, buffer));
str.on('error', (err) => callback(err, buffer));
str.on('error', (err) => callback(err, buffer));
}
}
readAsync() {
readAsync() {
return new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
this.read((err, buffer) => {
this.read((err, buffer) => {
if (err != null) {
if (err != null) {
reject(err);
reject(err);
}
}
else {
else {
resolve(buffer);
resolve(buffer);
}
}
});
});
});
});
}
}
}
}
class MessageStream extends stream.Stream {
class MessageStream extends Stream {
/**
/**
* @param {Message} message the message to stream
* @param {Message} message the message to stream
*/
*/
constructor(message) {
constructor(message) {
super();
super();
this.message = message;
this.message = message;
this.readable = true;
this.readable = true;
this.paused = false;
this.paused = false;
this.buffer = Buffer.alloc(MIMECHUNK * 24 * 7);
this.buffer = Buffer.alloc(MIMECHUNK * 24 * 7);
this.bufferIndex = 0;
this.bufferIndex = 0;
/**
/**
* @param {string} [data] the data to output
* @param {string} [data] the data to output
* @param {Function} [callback] the function
* @param {Function} [callback] the function
* @param {any[]} [args] array of arguments to pass to the callback
* @param {any[]} [args] array of arguments to pass to the callback
* @returns {void}
* @returns {void}
*/
*/
const output = (data) => {
const output = (data) => {
// can we buffer the data?
// can we buffer the data?
if (this.buffer != null) {
if (this.buffer != null) {
const bytes = Buffer.byteLength(data);
const bytes = Buffer.byteLength(data);
if (bytes + this.bufferIndex < this.buffer.length) {
if (bytes + this.bufferIndex < this.buffer.length) {
this.buffer.write(data, this.bufferIndex);
this.buffer.write(data, this.bufferIndex);
this.bufferIndex += bytes;
this.bufferIndex += bytes;
}
}
// we can't buffer the data, so ship it out!
// we can't buffer the data, so ship it out!
else if (bytes > this.buffer.length) {
else if (bytes > this.buffer.length) {
if (this.bufferIndex) {
if (this.bufferIndex) {
this.emit('data', this.buffer.toString('utf-8', 0, this.bufferIndex));
this.emit('data', this.buffer.toString('utf-8', 0, this.bufferIndex));
this.bufferIndex = 0;
this.bufferIndex = 0;
}
}
const loops = Math.ceil(data.length / this.buffer.length);
const loops = Math.ceil(data.length / this.buffer.length);
let loop = 0;
let loop = 0;
while (loop < loops) {
while (loop < loops) {
this.emit('data', data.substring(this.buffer.length * loop, this.buffer.length * (loop + 1)));
this.emit('data', data.substring(this.buffer.length * loop, this.buffer.length * (loop + 1)));
loop++;
loop++;
}
}
} // we need to clean out the buffer, it is getting full
} // we need to clean out the buffer, it is getting full
else {
else {
if (!this.paused) {
if (!this.paused) {
this.emit('data', this.buffer.toString('utf-8', 0, this.bufferIndex));
this.emit('data', this.buffer.toString('utf-8', 0, this.bufferIndex));
this.buffer.write(data, 0);
this.buffer.write(data, 0);
this.bufferIndex = bytes;
this.bufferIndex = bytes;
}
}
el