emailjs-3.6.0-to-4.0.0
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