mirror of
https://github.com/xfarrow/blink
synced 2025-06-27 09:03:02 +02:00
Change endpoint from persons to people
This commit is contained in:
327
backend/apis/nodejs/node_modules/nodemailer/lib/addressparser/index.js
generated
vendored
Normal file
327
backend/apis/nodejs/node_modules/nodemailer/lib/addressparser/index.js
generated
vendored
Normal file
@ -0,0 +1,327 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Converts tokens for a single address into an address object
|
||||
*
|
||||
* @param {Array} tokens Tokens object
|
||||
* @return {Object} Address object
|
||||
*/
|
||||
function _handleAddress(tokens) {
|
||||
let isGroup = false;
|
||||
let state = 'text';
|
||||
let address;
|
||||
let addresses = [];
|
||||
let data = {
|
||||
address: [],
|
||||
comment: [],
|
||||
group: [],
|
||||
text: []
|
||||
};
|
||||
let i;
|
||||
let len;
|
||||
|
||||
// Filter out <addresses>, (comments) and regular text
|
||||
for (i = 0, len = tokens.length; i < len; i++) {
|
||||
let token = tokens[i];
|
||||
let prevToken = i ? tokens[i - 1] : null;
|
||||
if (token.type === 'operator') {
|
||||
switch (token.value) {
|
||||
case '<':
|
||||
state = 'address';
|
||||
break;
|
||||
case '(':
|
||||
state = 'comment';
|
||||
break;
|
||||
case ':':
|
||||
state = 'group';
|
||||
isGroup = true;
|
||||
break;
|
||||
default:
|
||||
state = 'text';
|
||||
break;
|
||||
}
|
||||
} else if (token.value) {
|
||||
if (state === 'address') {
|
||||
// handle use case where unquoted name includes a "<"
|
||||
// Apple Mail truncates everything between an unexpected < and an address
|
||||
// and so will we
|
||||
token.value = token.value.replace(/^[^<]*<\s*/, '');
|
||||
}
|
||||
|
||||
if (prevToken && prevToken.noBreak && data[state].length) {
|
||||
// join values
|
||||
data[state][data[state].length - 1] += token.value;
|
||||
} else {
|
||||
data[state].push(token.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no text but a comment, replace the two
|
||||
if (!data.text.length && data.comment.length) {
|
||||
data.text = data.comment;
|
||||
data.comment = [];
|
||||
}
|
||||
|
||||
if (isGroup) {
|
||||
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
|
||||
data.text = data.text.join(' ');
|
||||
addresses.push({
|
||||
name: data.text || (address && address.name),
|
||||
group: data.group.length ? addressparser(data.group.join(',')) : []
|
||||
});
|
||||
} else {
|
||||
// If no address was found, try to detect one from regular text
|
||||
if (!data.address.length && data.text.length) {
|
||||
for (i = data.text.length - 1; i >= 0; i--) {
|
||||
if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
|
||||
data.address = data.text.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let _regexHandler = function (address) {
|
||||
if (!data.address.length) {
|
||||
data.address = [address.trim()];
|
||||
return ' ';
|
||||
} else {
|
||||
return address;
|
||||
}
|
||||
};
|
||||
|
||||
// still no address
|
||||
if (!data.address.length) {
|
||||
for (i = data.text.length - 1; i >= 0; i--) {
|
||||
// fixed the regex to parse email address correctly when email address has more than one @
|
||||
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, _regexHandler).trim();
|
||||
if (data.address.length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there's still is no text but a comment exixts, replace the two
|
||||
if (!data.text.length && data.comment.length) {
|
||||
data.text = data.comment;
|
||||
data.comment = [];
|
||||
}
|
||||
|
||||
// Keep only the first address occurence, push others to regular text
|
||||
if (data.address.length > 1) {
|
||||
data.text = data.text.concat(data.address.splice(1));
|
||||
}
|
||||
|
||||
// Join values with spaces
|
||||
data.text = data.text.join(' ');
|
||||
data.address = data.address.join(' ');
|
||||
|
||||
if (!data.address && isGroup) {
|
||||
return [];
|
||||
} else {
|
||||
address = {
|
||||
address: data.address || data.text || '',
|
||||
name: data.text || data.address || ''
|
||||
};
|
||||
|
||||
if (address.address === address.name) {
|
||||
if ((address.address || '').match(/@/)) {
|
||||
address.name = '';
|
||||
} else {
|
||||
address.address = '';
|
||||
}
|
||||
}
|
||||
|
||||
addresses.push(address);
|
||||
}
|
||||
}
|
||||
|
||||
return addresses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Tokenizer object for tokenizing address field strings
|
||||
*
|
||||
* @constructor
|
||||
* @param {String} str Address field string
|
||||
*/
|
||||
class Tokenizer {
|
||||
constructor(str) {
|
||||
this.str = (str || '').toString();
|
||||
this.operatorCurrent = '';
|
||||
this.operatorExpecting = '';
|
||||
this.node = null;
|
||||
this.escaped = false;
|
||||
|
||||
this.list = [];
|
||||
/**
|
||||
* Operator tokens and which tokens are expected to end the sequence
|
||||
*/
|
||||
this.operators = {
|
||||
'"': '"',
|
||||
'(': ')',
|
||||
'<': '>',
|
||||
',': '',
|
||||
':': ';',
|
||||
// Semicolons are not a legal delimiter per the RFC2822 grammar other
|
||||
// than for terminating a group, but they are also not valid for any
|
||||
// other use in this context. Given that some mail clients have
|
||||
// historically allowed the semicolon as a delimiter equivalent to the
|
||||
// comma in their UI, it makes sense to treat them the same as a comma
|
||||
// when used outside of a group.
|
||||
';': ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenizes the original input string
|
||||
*
|
||||
* @return {Array} An array of operator|text tokens
|
||||
*/
|
||||
tokenize() {
|
||||
let 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;
|
||||
this.checkChar(chr, nextChr);
|
||||
}
|
||||
|
||||
this.list.forEach(node => {
|
||||
node.value = (node.value || '').toString().trim();
|
||||
if (node.value) {
|
||||
list.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a character is an operator or text and acts accordingly
|
||||
*
|
||||
* @param {String} chr Character from the address field
|
||||
*/
|
||||
checkChar(chr, nextChr) {
|
||||
if (this.escaped) {
|
||||
// ignore next condition blocks
|
||||
} else if (chr === this.operatorExpecting) {
|
||||
this.node = {
|
||||
type: 'operator',
|
||||
value: chr
|
||||
};
|
||||
|
||||
if (nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr)) {
|
||||
this.node.noBreak = true;
|
||||
}
|
||||
|
||||
this.list.push(this.node);
|
||||
this.node = null;
|
||||
this.operatorExpecting = '';
|
||||
this.escaped = false;
|
||||
|
||||
return;
|
||||
} else if (!this.operatorExpecting && chr in this.operators) {
|
||||
this.node = {
|
||||
type: 'operator',
|
||||
value: chr
|
||||
};
|
||||
this.list.push(this.node);
|
||||
this.node = null;
|
||||
this.operatorExpecting = this.operators[chr];
|
||||
this.escaped = false;
|
||||
return;
|
||||
} else if (['"', "'"].includes(this.operatorExpecting) && chr === '\\') {
|
||||
this.escaped = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.node) {
|
||||
this.node = {
|
||||
type: 'text',
|
||||
value: ''
|
||||
};
|
||||
this.list.push(this.node);
|
||||
}
|
||||
|
||||
if (chr === '\n') {
|
||||
// Convert newlines to spaces. Carriage return is ignored as \r and \n usually
|
||||
// go together anyway and there already is a WS for \n. Lone \r means something is fishy.
|
||||
chr = ' ';
|
||||
}
|
||||
|
||||
if (chr.charCodeAt(0) >= 0x21 || [' ', '\t'].includes(chr)) {
|
||||
// skip command bytes
|
||||
this.node.value += chr;
|
||||
}
|
||||
|
||||
this.escaped = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses structured e-mail addresses from an address field
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* 'Name <address@domain>'
|
||||
*
|
||||
* will be converted to
|
||||
*
|
||||
* [{name: 'Name', address: 'address@domain'}]
|
||||
*
|
||||
* @param {String} str Address field
|
||||
* @return {Array} An array of address objects
|
||||
*/
|
||||
function addressparser(str, options) {
|
||||
options = options || {};
|
||||
|
||||
let tokenizer = new Tokenizer(str);
|
||||
let tokens = tokenizer.tokenize();
|
||||
|
||||
let addresses = [];
|
||||
let address = [];
|
||||
let parsedAddresses = [];
|
||||
|
||||
tokens.forEach(token => {
|
||||
if (token.type === 'operator' && (token.value === ',' || token.value === ';')) {
|
||||
if (address.length) {
|
||||
addresses.push(address);
|
||||
}
|
||||
address = [];
|
||||
} else {
|
||||
address.push(token);
|
||||
}
|
||||
});
|
||||
|
||||
if (address.length) {
|
||||
addresses.push(address);
|
||||
}
|
||||
|
||||
addresses.forEach(address => {
|
||||
address = _handleAddress(address);
|
||||
if (address.length) {
|
||||
parsedAddresses = parsedAddresses.concat(address);
|
||||
}
|
||||
});
|
||||
|
||||
if (options.flatten) {
|
||||
let addresses = [];
|
||||
let walkAddressList = list => {
|
||||
list.forEach(address => {
|
||||
if (address.group) {
|
||||
return walkAddressList(address.group);
|
||||
} else {
|
||||
addresses.push(address);
|
||||
}
|
||||
});
|
||||
};
|
||||
walkAddressList(parsedAddresses);
|
||||
return addresses;
|
||||
}
|
||||
|
||||
return parsedAddresses;
|
||||
}
|
||||
|
||||
// expose to the world
|
||||
module.exports = addressparser;
|
142
backend/apis/nodejs/node_modules/nodemailer/lib/base64/index.js
generated
vendored
Normal file
142
backend/apis/nodejs/node_modules/nodemailer/lib/base64/index.js
generated
vendored
Normal file
@ -0,0 +1,142 @@
|
||||
'use strict';
|
||||
|
||||
const Transform = require('stream').Transform;
|
||||
|
||||
/**
|
||||
* Encodes a Buffer into a base64 encoded string
|
||||
*
|
||||
* @param {Buffer} buffer Buffer to convert
|
||||
* @returns {String} base64 encoded string
|
||||
*/
|
||||
function encode(buffer) {
|
||||
if (typeof buffer === 'string') {
|
||||
buffer = Buffer.from(buffer, 'utf-8');
|
||||
}
|
||||
|
||||
return buffer.toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds soft line breaks to a base64 string
|
||||
*
|
||||
* @param {String} str base64 encoded string that might need line wrapping
|
||||
* @param {Number} [lineLength=76] Maximum allowed length for a line
|
||||
* @returns {String} Soft-wrapped base64 encoded string
|
||||
*/
|
||||
function wrap(str, lineLength) {
|
||||
str = (str || '').toString();
|
||||
lineLength = lineLength || 76;
|
||||
|
||||
if (str.length <= lineLength) {
|
||||
return str;
|
||||
}
|
||||
|
||||
let result = [];
|
||||
let pos = 0;
|
||||
let chunkLength = lineLength * 1024;
|
||||
while (pos < str.length) {
|
||||
let wrappedLines = str
|
||||
.substr(pos, chunkLength)
|
||||
.replace(new RegExp('.{' + lineLength + '}', 'g'), '$&\r\n')
|
||||
.trim();
|
||||
result.push(wrappedLines);
|
||||
pos += chunkLength;
|
||||
}
|
||||
|
||||
return result.join('\r\n').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a transform stream for encoding data to base64 encoding
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} options Stream options
|
||||
* @param {Number} [options.lineLength=76] Maximum length for lines, set to false to disable wrapping
|
||||
*/
|
||||
class Encoder extends Transform {
|
||||
constructor(options) {
|
||||
super();
|
||||
// init Transform
|
||||
this.options = options || {};
|
||||
|
||||
if (this.options.lineLength !== false) {
|
||||
this.options.lineLength = this.options.lineLength || 76;
|
||||
}
|
||||
|
||||
this._curLine = '';
|
||||
this._remainingBytes = false;
|
||||
|
||||
this.inputBytes = 0;
|
||||
this.outputBytes = 0;
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, done) {
|
||||
if (encoding !== 'buffer') {
|
||||
chunk = Buffer.from(chunk, encoding);
|
||||
}
|
||||
|
||||
if (!chunk || !chunk.length) {
|
||||
return setImmediate(done);
|
||||
}
|
||||
|
||||
this.inputBytes += chunk.length;
|
||||
|
||||
if (this._remainingBytes && this._remainingBytes.length) {
|
||||
chunk = Buffer.concat([this._remainingBytes, chunk], this._remainingBytes.length + chunk.length);
|
||||
this._remainingBytes = false;
|
||||
}
|
||||
|
||||
if (chunk.length % 3) {
|
||||
this._remainingBytes = chunk.slice(chunk.length - (chunk.length % 3));
|
||||
chunk = chunk.slice(0, chunk.length - (chunk.length % 3));
|
||||
} else {
|
||||
this._remainingBytes = false;
|
||||
}
|
||||
|
||||
let b64 = this._curLine + encode(chunk);
|
||||
|
||||
if (this.options.lineLength) {
|
||||
b64 = wrap(b64, this.options.lineLength);
|
||||
|
||||
// remove last line as it is still most probably incomplete
|
||||
let lastLF = b64.lastIndexOf('\n');
|
||||
if (lastLF < 0) {
|
||||
this._curLine = b64;
|
||||
b64 = '';
|
||||
} else if (lastLF === b64.length - 1) {
|
||||
this._curLine = '';
|
||||
} else {
|
||||
this._curLine = b64.substr(lastLF + 1);
|
||||
b64 = b64.substr(0, lastLF + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (b64) {
|
||||
this.outputBytes += b64.length;
|
||||
this.push(Buffer.from(b64, 'ascii'));
|
||||
}
|
||||
|
||||
setImmediate(done);
|
||||
}
|
||||
|
||||
_flush(done) {
|
||||
if (this._remainingBytes && this._remainingBytes.length) {
|
||||
this._curLine += encode(this._remainingBytes);
|
||||
}
|
||||
|
||||
if (this._curLine) {
|
||||
this._curLine = wrap(this._curLine, this.options.lineLength);
|
||||
this.outputBytes += this._curLine.length;
|
||||
this.push(this._curLine, 'ascii');
|
||||
this._curLine = '';
|
||||
}
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
// expose to the world
|
||||
module.exports = {
|
||||
encode,
|
||||
wrap,
|
||||
Encoder
|
||||
};
|
251
backend/apis/nodejs/node_modules/nodemailer/lib/dkim/index.js
generated
vendored
Normal file
251
backend/apis/nodejs/node_modules/nodemailer/lib/dkim/index.js
generated
vendored
Normal file
@ -0,0 +1,251 @@
|
||||
'use strict';
|
||||
|
||||
// FIXME:
|
||||
// replace this Transform mess with a method that pipes input argument to output argument
|
||||
|
||||
const MessageParser = require('./message-parser');
|
||||
const RelaxedBody = require('./relaxed-body');
|
||||
const sign = require('./sign');
|
||||
const PassThrough = require('stream').PassThrough;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const DKIM_ALGO = 'sha256';
|
||||
const MAX_MESSAGE_SIZE = 128 * 1024; // buffer messages larger than this to disk
|
||||
|
||||
/*
|
||||
// Usage:
|
||||
|
||||
let dkim = new DKIM({
|
||||
domainName: 'example.com',
|
||||
keySelector: 'key-selector',
|
||||
privateKey,
|
||||
cacheDir: '/tmp'
|
||||
});
|
||||
dkim.sign(input).pipe(process.stdout);
|
||||
|
||||
// Where inputStream is a rfc822 message (either a stream, string or Buffer)
|
||||
// and outputStream is a DKIM signed rfc822 message
|
||||
*/
|
||||
|
||||
class DKIMSigner {
|
||||
constructor(options, keys, input, output) {
|
||||
this.options = options || {};
|
||||
this.keys = keys;
|
||||
|
||||
this.cacheTreshold = Number(this.options.cacheTreshold) || MAX_MESSAGE_SIZE;
|
||||
this.hashAlgo = this.options.hashAlgo || DKIM_ALGO;
|
||||
|
||||
this.cacheDir = this.options.cacheDir || false;
|
||||
|
||||
this.chunks = [];
|
||||
this.chunklen = 0;
|
||||
this.readPos = 0;
|
||||
this.cachePath = this.cacheDir ? path.join(this.cacheDir, 'message.' + Date.now() + '-' + crypto.randomBytes(14).toString('hex')) : false;
|
||||
this.cache = false;
|
||||
|
||||
this.headers = false;
|
||||
this.bodyHash = false;
|
||||
this.parser = false;
|
||||
this.relaxedBody = false;
|
||||
|
||||
this.input = input;
|
||||
this.output = output;
|
||||
this.output.usingCache = false;
|
||||
|
||||
this.hasErrored = false;
|
||||
|
||||
this.input.on('error', err => {
|
||||
this.hasErrored = true;
|
||||
this.cleanup();
|
||||
output.emit('error', err);
|
||||
});
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if (!this.cache || !this.cachePath) {
|
||||
return;
|
||||
}
|
||||
fs.unlink(this.cachePath, () => false);
|
||||
}
|
||||
|
||||
createReadCache() {
|
||||
// pipe remainings to cache file
|
||||
this.cache = fs.createReadStream(this.cachePath);
|
||||
this.cache.once('error', err => {
|
||||
this.cleanup();
|
||||
this.output.emit('error', err);
|
||||
});
|
||||
this.cache.once('close', () => {
|
||||
this.cleanup();
|
||||
});
|
||||
this.cache.pipe(this.output);
|
||||
}
|
||||
|
||||
sendNextChunk() {
|
||||
if (this.hasErrored) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.readPos >= this.chunks.length) {
|
||||
if (!this.cache) {
|
||||
return this.output.end();
|
||||
}
|
||||
return this.createReadCache();
|
||||
}
|
||||
let chunk = this.chunks[this.readPos++];
|
||||
if (this.output.write(chunk) === false) {
|
||||
return this.output.once('drain', () => {
|
||||
this.sendNextChunk();
|
||||
});
|
||||
}
|
||||
setImmediate(() => this.sendNextChunk());
|
||||
}
|
||||
|
||||
sendSignedOutput() {
|
||||
let keyPos = 0;
|
||||
let signNextKey = () => {
|
||||
if (keyPos >= this.keys.length) {
|
||||
this.output.write(this.parser.rawHeaders);
|
||||
return setImmediate(() => this.sendNextChunk());
|
||||
}
|
||||
let key = this.keys[keyPos++];
|
||||
let dkimField = sign(this.headers, this.hashAlgo, this.bodyHash, {
|
||||
domainName: key.domainName,
|
||||
keySelector: key.keySelector,
|
||||
privateKey: key.privateKey,
|
||||
headerFieldNames: this.options.headerFieldNames,
|
||||
skipFields: this.options.skipFields
|
||||
});
|
||||
if (dkimField) {
|
||||
this.output.write(Buffer.from(dkimField + '\r\n'));
|
||||
}
|
||||
return setImmediate(signNextKey);
|
||||
};
|
||||
|
||||
if (this.bodyHash && this.headers) {
|
||||
return signNextKey();
|
||||
}
|
||||
|
||||
this.output.write(this.parser.rawHeaders);
|
||||
this.sendNextChunk();
|
||||
}
|
||||
|
||||
createWriteCache() {
|
||||
this.output.usingCache = true;
|
||||
// pipe remainings to cache file
|
||||
this.cache = fs.createWriteStream(this.cachePath);
|
||||
this.cache.once('error', err => {
|
||||
this.cleanup();
|
||||
// drain input
|
||||
this.relaxedBody.unpipe(this.cache);
|
||||
this.relaxedBody.on('readable', () => {
|
||||
while (this.relaxedBody.read() !== null) {
|
||||
// do nothing
|
||||
}
|
||||
});
|
||||
this.hasErrored = true;
|
||||
// emit error
|
||||
this.output.emit('error', err);
|
||||
});
|
||||
this.cache.once('close', () => {
|
||||
this.sendSignedOutput();
|
||||
});
|
||||
this.relaxedBody.removeAllListeners('readable');
|
||||
this.relaxedBody.pipe(this.cache);
|
||||
}
|
||||
|
||||
signStream() {
|
||||
this.parser = new MessageParser();
|
||||
this.relaxedBody = new RelaxedBody({
|
||||
hashAlgo: this.hashAlgo
|
||||
});
|
||||
|
||||
this.parser.on('headers', value => {
|
||||
this.headers = value;
|
||||
});
|
||||
|
||||
this.relaxedBody.on('hash', value => {
|
||||
this.bodyHash = value;
|
||||
});
|
||||
|
||||
this.relaxedBody.on('readable', () => {
|
||||
let chunk;
|
||||
if (this.cache) {
|
||||
return;
|
||||
}
|
||||
while ((chunk = this.relaxedBody.read()) !== null) {
|
||||
this.chunks.push(chunk);
|
||||
this.chunklen += chunk.length;
|
||||
if (this.chunklen >= this.cacheTreshold && this.cachePath) {
|
||||
return this.createWriteCache();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.relaxedBody.on('end', () => {
|
||||
if (this.cache) {
|
||||
return;
|
||||
}
|
||||
this.sendSignedOutput();
|
||||
});
|
||||
|
||||
this.parser.pipe(this.relaxedBody);
|
||||
setImmediate(() => this.input.pipe(this.parser));
|
||||
}
|
||||
}
|
||||
|
||||
class DKIM {
|
||||
constructor(options) {
|
||||
this.options = options || {};
|
||||
this.keys = [].concat(
|
||||
this.options.keys || {
|
||||
domainName: options.domainName,
|
||||
keySelector: options.keySelector,
|
||||
privateKey: options.privateKey
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
sign(input, extraOptions) {
|
||||
let output = new PassThrough();
|
||||
let inputStream = input;
|
||||
let writeValue = false;
|
||||
|
||||
if (Buffer.isBuffer(input)) {
|
||||
writeValue = input;
|
||||
inputStream = new PassThrough();
|
||||
} else if (typeof input === 'string') {
|
||||
writeValue = Buffer.from(input);
|
||||
inputStream = new PassThrough();
|
||||
}
|
||||
|
||||
let options = this.options;
|
||||
if (extraOptions && Object.keys(extraOptions).length) {
|
||||
options = {};
|
||||
Object.keys(this.options || {}).forEach(key => {
|
||||
options[key] = this.options[key];
|
||||
});
|
||||
Object.keys(extraOptions || {}).forEach(key => {
|
||||
if (!(key in options)) {
|
||||
options[key] = extraOptions[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let signer = new DKIMSigner(options, this.keys, inputStream, output);
|
||||
setImmediate(() => {
|
||||
signer.signStream();
|
||||
if (writeValue) {
|
||||
setImmediate(() => {
|
||||
inputStream.end(writeValue);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DKIM;
|
155
backend/apis/nodejs/node_modules/nodemailer/lib/dkim/message-parser.js
generated
vendored
Normal file
155
backend/apis/nodejs/node_modules/nodemailer/lib/dkim/message-parser.js
generated
vendored
Normal file
@ -0,0 +1,155 @@
|
||||
'use strict';
|
||||
|
||||
const Transform = require('stream').Transform;
|
||||
|
||||
/**
|
||||
* MessageParser instance is a transform stream that separates message headers
|
||||
* from the rest of the body. Headers are emitted with the 'headers' event. Message
|
||||
* body is passed on as the resulting stream.
|
||||
*/
|
||||
class MessageParser extends Transform {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.lastBytes = Buffer.alloc(4);
|
||||
this.headersParsed = false;
|
||||
this.headerBytes = 0;
|
||||
this.headerChunks = [];
|
||||
this.rawHeaders = false;
|
||||
this.bodySize = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps count of the last 4 bytes in order to detect line breaks on chunk boundaries
|
||||
*
|
||||
* @param {Buffer} data Next data chunk from the stream
|
||||
*/
|
||||
updateLastBytes(data) {
|
||||
let lblen = this.lastBytes.length;
|
||||
let nblen = Math.min(data.length, lblen);
|
||||
|
||||
// shift existing bytes
|
||||
for (let i = 0, len = lblen - nblen; i < len; i++) {
|
||||
this.lastBytes[i] = this.lastBytes[i + nblen];
|
||||
}
|
||||
|
||||
// add new bytes
|
||||
for (let i = 1; i <= nblen; i++) {
|
||||
this.lastBytes[lblen - i] = data[data.length - i];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and removes message headers from the remaining body. We want to keep
|
||||
* headers separated until final delivery to be able to modify these
|
||||
*
|
||||
* @param {Buffer} data Next chunk of data
|
||||
* @return {Boolean} Returns true if headers are already found or false otherwise
|
||||
*/
|
||||
checkHeaders(data) {
|
||||
if (this.headersParsed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let lblen = this.lastBytes.length;
|
||||
let headerPos = 0;
|
||||
this.curLinePos = 0;
|
||||
for (let i = 0, len = this.lastBytes.length + data.length; i < len; i++) {
|
||||
let chr;
|
||||
if (i < lblen) {
|
||||
chr = this.lastBytes[i];
|
||||
} else {
|
||||
chr = data[i - lblen];
|
||||
}
|
||||
if (chr === 0x0a && i) {
|
||||
let pr1 = i - 1 < lblen ? this.lastBytes[i - 1] : data[i - 1 - lblen];
|
||||
let pr2 = i > 1 ? (i - 2 < lblen ? this.lastBytes[i - 2] : data[i - 2 - lblen]) : false;
|
||||
if (pr1 === 0x0a) {
|
||||
this.headersParsed = true;
|
||||
headerPos = i - lblen + 1;
|
||||
this.headerBytes += headerPos;
|
||||
break;
|
||||
} else if (pr1 === 0x0d && pr2 === 0x0a) {
|
||||
this.headersParsed = true;
|
||||
headerPos = i - lblen + 1;
|
||||
this.headerBytes += headerPos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.headersParsed) {
|
||||
this.headerChunks.push(data.slice(0, headerPos));
|
||||
this.rawHeaders = Buffer.concat(this.headerChunks, this.headerBytes);
|
||||
this.headerChunks = null;
|
||||
this.emit('headers', this.parseHeaders());
|
||||
if (data.length - 1 > headerPos) {
|
||||
let chunk = data.slice(headerPos);
|
||||
this.bodySize += chunk.length;
|
||||
// this would be the first chunk of data sent downstream
|
||||
setImmediate(() => this.push(chunk));
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
this.headerBytes += data.length;
|
||||
this.headerChunks.push(data);
|
||||
}
|
||||
|
||||
// store last 4 bytes to catch header break
|
||||
this.updateLastBytes(data);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, callback) {
|
||||
if (!chunk || !chunk.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (typeof chunk === 'string') {
|
||||
chunk = Buffer.from(chunk, encoding);
|
||||
}
|
||||
|
||||
let headersFound;
|
||||
|
||||
try {
|
||||
headersFound = this.checkHeaders(chunk);
|
||||
} catch (E) {
|
||||
return callback(E);
|
||||
}
|
||||
|
||||
if (headersFound) {
|
||||
this.bodySize += chunk.length;
|
||||
this.push(chunk);
|
||||
}
|
||||
|
||||
setImmediate(callback);
|
||||
}
|
||||
|
||||
_flush(callback) {
|
||||
if (this.headerChunks) {
|
||||
let chunk = Buffer.concat(this.headerChunks, this.headerBytes);
|
||||
this.bodySize += chunk.length;
|
||||
this.push(chunk);
|
||||
this.headerChunks = null;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
parseHeaders() {
|
||||
let lines = (this.rawHeaders || '').toString().split(/\r?\n/);
|
||||
for (let i = lines.length - 1; i > 0; i--) {
|
||||
if (/^\s/.test(lines[i])) {
|
||||
lines[i - 1] += '\n' + lines[i];
|
||||
lines.splice(i, 1);
|
||||
}
|
||||
}
|
||||
return lines
|
||||
.filter(line => line.trim())
|
||||
.map(line => ({
|
||||
key: line.substr(0, line.indexOf(':')).trim().toLowerCase(),
|
||||
line
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessageParser;
|
154
backend/apis/nodejs/node_modules/nodemailer/lib/dkim/relaxed-body.js
generated
vendored
Normal file
154
backend/apis/nodejs/node_modules/nodemailer/lib/dkim/relaxed-body.js
generated
vendored
Normal file
@ -0,0 +1,154 @@
|
||||
'use strict';
|
||||
|
||||
// streams through a message body and calculates relaxed body hash
|
||||
|
||||
const Transform = require('stream').Transform;
|
||||
const crypto = require('crypto');
|
||||
|
||||
class RelaxedBody extends Transform {
|
||||
constructor(options) {
|
||||
super();
|
||||
options = options || {};
|
||||
this.chunkBuffer = [];
|
||||
this.chunkBufferLen = 0;
|
||||
this.bodyHash = crypto.createHash(options.hashAlgo || 'sha1');
|
||||
this.remainder = '';
|
||||
this.byteLength = 0;
|
||||
|
||||
this.debug = options.debug;
|
||||
this._debugBody = options.debug ? [] : false;
|
||||
}
|
||||
|
||||
updateHash(chunk) {
|
||||
let bodyStr;
|
||||
|
||||
// find next remainder
|
||||
let nextRemainder = '';
|
||||
|
||||
// This crux finds and removes the spaces from the last line and the newline characters after the last non-empty line
|
||||
// If we get another chunk that does not match this description then we can restore the previously processed data
|
||||
let state = 'file';
|
||||
for (let i = chunk.length - 1; i >= 0; i--) {
|
||||
let c = chunk[i];
|
||||
|
||||
if (state === 'file' && (c === 0x0a || c === 0x0d)) {
|
||||
// do nothing, found \n or \r at the end of chunk, stil end of file
|
||||
} else if (state === 'file' && (c === 0x09 || c === 0x20)) {
|
||||
// switch to line ending mode, this is the last non-empty line
|
||||
state = 'line';
|
||||
} else if (state === 'line' && (c === 0x09 || c === 0x20)) {
|
||||
// do nothing, found ' ' or \t at the end of line, keep processing the last non-empty line
|
||||
} else if (state === 'file' || state === 'line') {
|
||||
// non line/file ending character found, switch to body mode
|
||||
state = 'body';
|
||||
if (i === chunk.length - 1) {
|
||||
// final char is not part of line end or file end, so do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (i === 0) {
|
||||
// reached to the beginning of the chunk, check if it is still about the ending
|
||||
// and if the remainder also matches
|
||||
if (
|
||||
(state === 'file' && (!this.remainder || /[\r\n]$/.test(this.remainder))) ||
|
||||
(state === 'line' && (!this.remainder || /[ \t]$/.test(this.remainder)))
|
||||
) {
|
||||
// keep everything
|
||||
this.remainder += chunk.toString('binary');
|
||||
return;
|
||||
} else if (state === 'line' || state === 'file') {
|
||||
// process existing remainder as normal line but store the current chunk
|
||||
nextRemainder = chunk.toString('binary');
|
||||
chunk = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (state !== 'body') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// reached first non ending byte
|
||||
nextRemainder = chunk.slice(i + 1).toString('binary');
|
||||
chunk = chunk.slice(0, i + 1);
|
||||
break;
|
||||
}
|
||||
|
||||
let needsFixing = !!this.remainder;
|
||||
if (chunk && !needsFixing) {
|
||||
// check if we even need to change anything
|
||||
for (let i = 0, len = chunk.length; i < len; i++) {
|
||||
if (i && chunk[i] === 0x0a && chunk[i - 1] !== 0x0d) {
|
||||
// missing \r before \n
|
||||
needsFixing = true;
|
||||
break;
|
||||
} else if (i && chunk[i] === 0x0d && chunk[i - 1] === 0x20) {
|
||||
// trailing WSP found
|
||||
needsFixing = true;
|
||||
break;
|
||||
} else if (i && chunk[i] === 0x20 && chunk[i - 1] === 0x20) {
|
||||
// multiple spaces found, needs to be replaced with just one
|
||||
needsFixing = true;
|
||||
break;
|
||||
} else if (chunk[i] === 0x09) {
|
||||
// TAB found, needs to be replaced with a space
|
||||
needsFixing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needsFixing) {
|
||||
bodyStr = this.remainder + (chunk ? chunk.toString('binary') : '');
|
||||
this.remainder = nextRemainder;
|
||||
bodyStr = bodyStr
|
||||
.replace(/\r?\n/g, '\n') // use js line endings
|
||||
.replace(/[ \t]*$/gm, '') // remove line endings, rtrim
|
||||
.replace(/[ \t]+/gm, ' ') // single spaces
|
||||
.replace(/\n/g, '\r\n'); // restore rfc822 line endings
|
||||
chunk = Buffer.from(bodyStr, 'binary');
|
||||
} else if (nextRemainder) {
|
||||
this.remainder = nextRemainder;
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
this._debugBody.push(chunk);
|
||||
}
|
||||
this.bodyHash.update(chunk);
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, callback) {
|
||||
if (!chunk || !chunk.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (typeof chunk === 'string') {
|
||||
chunk = Buffer.from(chunk, encoding);
|
||||
}
|
||||
|
||||
this.updateHash(chunk);
|
||||
|
||||
this.byteLength += chunk.length;
|
||||
this.push(chunk);
|
||||
callback();
|
||||
}
|
||||
|
||||
_flush(callback) {
|
||||
// generate final hash and emit it
|
||||
if (/[\r\n]$/.test(this.remainder) && this.byteLength > 2) {
|
||||
// add terminating line end
|
||||
this.bodyHash.update(Buffer.from('\r\n'));
|
||||
}
|
||||
if (!this.byteLength) {
|
||||
// emit empty line buffer to keep the stream flowing
|
||||
this.push(Buffer.from('\r\n'));
|
||||
// this.bodyHash.update(Buffer.from('\r\n'));
|
||||
}
|
||||
|
||||
this.emit('hash', this.bodyHash.digest('base64'), this.debug ? Buffer.concat(this._debugBody) : false);
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RelaxedBody;
|
117
backend/apis/nodejs/node_modules/nodemailer/lib/dkim/sign.js
generated
vendored
Normal file
117
backend/apis/nodejs/node_modules/nodemailer/lib/dkim/sign.js
generated
vendored
Normal file
@ -0,0 +1,117 @@
|
||||
'use strict';
|
||||
|
||||
const punycode = require('../punycode');
|
||||
const mimeFuncs = require('../mime-funcs');
|
||||
const crypto = require('crypto');
|
||||
|
||||
/**
|
||||
* Returns DKIM signature header line
|
||||
*
|
||||
* @param {Object} headers Parsed headers object from MessageParser
|
||||
* @param {String} bodyHash Base64 encoded hash of the message
|
||||
* @param {Object} options DKIM options
|
||||
* @param {String} options.domainName Domain name to be signed for
|
||||
* @param {String} options.keySelector DKIM key selector to use
|
||||
* @param {String} options.privateKey DKIM private key to use
|
||||
* @return {String} Complete header line
|
||||
*/
|
||||
|
||||
module.exports = (headers, hashAlgo, bodyHash, options) => {
|
||||
options = options || {};
|
||||
|
||||
// all listed fields from RFC4871 #5.5
|
||||
let defaultFieldNames =
|
||||
'From:Sender:Reply-To:Subject:Date:Message-ID:To:' +
|
||||
'Cc:MIME-Version:Content-Type:Content-Transfer-Encoding:Content-ID:' +
|
||||
'Content-Description:Resent-Date:Resent-From:Resent-Sender:' +
|
||||
'Resent-To:Resent-Cc:Resent-Message-ID:In-Reply-To:References:' +
|
||||
'List-Id:List-Help:List-Unsubscribe:List-Subscribe:List-Post:' +
|
||||
'List-Owner:List-Archive';
|
||||
|
||||
let fieldNames = options.headerFieldNames || defaultFieldNames;
|
||||
|
||||
let canonicalizedHeaderData = relaxedHeaders(headers, fieldNames, options.skipFields);
|
||||
let dkimHeader = generateDKIMHeader(options.domainName, options.keySelector, canonicalizedHeaderData.fieldNames, hashAlgo, bodyHash);
|
||||
|
||||
let signer, signature;
|
||||
|
||||
canonicalizedHeaderData.headers += 'dkim-signature:' + relaxedHeaderLine(dkimHeader);
|
||||
|
||||
signer = crypto.createSign(('rsa-' + hashAlgo).toUpperCase());
|
||||
signer.update(canonicalizedHeaderData.headers);
|
||||
try {
|
||||
signature = signer.sign(options.privateKey, 'base64');
|
||||
} catch (E) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return dkimHeader + signature.replace(/(^.{73}|.{75}(?!\r?\n|\r))/g, '$&\r\n ').trim();
|
||||
};
|
||||
|
||||
module.exports.relaxedHeaders = relaxedHeaders;
|
||||
|
||||
function generateDKIMHeader(domainName, keySelector, fieldNames, hashAlgo, bodyHash) {
|
||||
let dkim = [
|
||||
'v=1',
|
||||
'a=rsa-' + hashAlgo,
|
||||
'c=relaxed/relaxed',
|
||||
'd=' + punycode.toASCII(domainName),
|
||||
'q=dns/txt',
|
||||
's=' + keySelector,
|
||||
'bh=' + bodyHash,
|
||||
'h=' + fieldNames
|
||||
].join('; ');
|
||||
|
||||
return mimeFuncs.foldLines('DKIM-Signature: ' + dkim, 76) + ';\r\n b=';
|
||||
}
|
||||
|
||||
function relaxedHeaders(headers, fieldNames, skipFields) {
|
||||
let includedFields = new Set();
|
||||
let skip = new Set();
|
||||
let headerFields = new Map();
|
||||
|
||||
(skipFields || '')
|
||||
.toLowerCase()
|
||||
.split(':')
|
||||
.forEach(field => {
|
||||
skip.add(field.trim());
|
||||
});
|
||||
|
||||
(fieldNames || '')
|
||||
.toLowerCase()
|
||||
.split(':')
|
||||
.filter(field => !skip.has(field.trim()))
|
||||
.forEach(field => {
|
||||
includedFields.add(field.trim());
|
||||
});
|
||||
|
||||
for (let i = headers.length - 1; i >= 0; i--) {
|
||||
let line = headers[i];
|
||||
// only include the first value from bottom to top
|
||||
if (includedFields.has(line.key) && !headerFields.has(line.key)) {
|
||||
headerFields.set(line.key, relaxedHeaderLine(line.line));
|
||||
}
|
||||
}
|
||||
|
||||
let headersList = [];
|
||||
let fields = [];
|
||||
includedFields.forEach(field => {
|
||||
if (headerFields.has(field)) {
|
||||
fields.push(field);
|
||||
headersList.push(field + ':' + headerFields.get(field));
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
headers: headersList.join('\r\n') + '\r\n',
|
||||
fieldNames: fields.join(':')
|
||||
};
|
||||
}
|
||||
|
||||
function relaxedHeaderLine(line) {
|
||||
return line
|
||||
.substr(line.indexOf(':') + 1)
|
||||
.replace(/\r?\n/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
281
backend/apis/nodejs/node_modules/nodemailer/lib/fetch/cookies.js
generated
vendored
Normal file
281
backend/apis/nodejs/node_modules/nodemailer/lib/fetch/cookies.js
generated
vendored
Normal file
@ -0,0 +1,281 @@
|
||||
'use strict';
|
||||
|
||||
// module to handle cookies
|
||||
|
||||
const urllib = require('url');
|
||||
|
||||
const SESSION_TIMEOUT = 1800; // 30 min
|
||||
|
||||
/**
|
||||
* Creates a biskviit cookie jar for managing cookie values in memory
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} [options] Optional options object
|
||||
*/
|
||||
class Cookies {
|
||||
constructor(options) {
|
||||
this.options = options || {};
|
||||
this.cookies = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a cookie string to the cookie storage
|
||||
*
|
||||
* @param {String} cookieStr Value from the 'Set-Cookie:' header
|
||||
* @param {String} url Current URL
|
||||
*/
|
||||
set(cookieStr, url) {
|
||||
let urlparts = urllib.parse(url || '');
|
||||
let cookie = this.parse(cookieStr);
|
||||
let domain;
|
||||
|
||||
if (cookie.domain) {
|
||||
domain = cookie.domain.replace(/^\./, '');
|
||||
|
||||
// do not allow cross origin cookies
|
||||
if (
|
||||
// can't be valid if the requested domain is shorter than current hostname
|
||||
urlparts.hostname.length < domain.length ||
|
||||
// prefix domains with dot to be sure that partial matches are not used
|
||||
('.' + urlparts.hostname).substr(-domain.length + 1) !== '.' + domain
|
||||
) {
|
||||
cookie.domain = urlparts.hostname;
|
||||
}
|
||||
} else {
|
||||
cookie.domain = urlparts.hostname;
|
||||
}
|
||||
|
||||
if (!cookie.path) {
|
||||
cookie.path = this.getPath(urlparts.pathname);
|
||||
}
|
||||
|
||||
// if no expire date, then use sessionTimeout value
|
||||
if (!cookie.expires) {
|
||||
cookie.expires = new Date(Date.now() + (Number(this.options.sessionTimeout || SESSION_TIMEOUT) || SESSION_TIMEOUT) * 1000);
|
||||
}
|
||||
|
||||
return this.add(cookie);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns cookie string for the 'Cookie:' header.
|
||||
*
|
||||
* @param {String} url URL to check for
|
||||
* @returns {String} Cookie header or empty string if no matches were found
|
||||
*/
|
||||
get(url) {
|
||||
return this.list(url)
|
||||
.map(cookie => cookie.name + '=' + cookie.value)
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all valied cookie objects for the specified URL
|
||||
*
|
||||
* @param {String} url URL to check for
|
||||
* @returns {Array} An array of cookie objects
|
||||
*/
|
||||
list(url) {
|
||||
let result = [];
|
||||
let i;
|
||||
let cookie;
|
||||
|
||||
for (i = this.cookies.length - 1; i >= 0; i--) {
|
||||
cookie = this.cookies[i];
|
||||
|
||||
if (this.isExpired(cookie)) {
|
||||
this.cookies.splice(i, i);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.match(cookie, url)) {
|
||||
result.unshift(cookie);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses cookie string from the 'Set-Cookie:' header
|
||||
*
|
||||
* @param {String} cookieStr String from the 'Set-Cookie:' header
|
||||
* @returns {Object} Cookie object
|
||||
*/
|
||||
parse(cookieStr) {
|
||||
let cookie = {};
|
||||
|
||||
(cookieStr || '')
|
||||
.toString()
|
||||
.split(';')
|
||||
.forEach(cookiePart => {
|
||||
let valueParts = cookiePart.split('=');
|
||||
let key = valueParts.shift().trim().toLowerCase();
|
||||
let value = valueParts.join('=').trim();
|
||||
let domain;
|
||||
|
||||
if (!key) {
|
||||
// skip empty parts
|
||||
return;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case 'expires':
|
||||
value = new Date(value);
|
||||
// ignore date if can not parse it
|
||||
if (value.toString() !== 'Invalid Date') {
|
||||
cookie.expires = value;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'path':
|
||||
cookie.path = value;
|
||||
break;
|
||||
|
||||
case 'domain':
|
||||
domain = value.toLowerCase();
|
||||
if (domain.length && domain.charAt(0) !== '.') {
|
||||
domain = '.' + domain; // ensure preceeding dot for user set domains
|
||||
}
|
||||
cookie.domain = domain;
|
||||
break;
|
||||
|
||||
case 'max-age':
|
||||
cookie.expires = new Date(Date.now() + (Number(value) || 0) * 1000);
|
||||
break;
|
||||
|
||||
case 'secure':
|
||||
cookie.secure = true;
|
||||
break;
|
||||
|
||||
case 'httponly':
|
||||
cookie.httponly = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
if (!cookie.name) {
|
||||
cookie.name = key;
|
||||
cookie.value = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return cookie;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a cookie object is valid for a specified URL
|
||||
*
|
||||
* @param {Object} cookie Cookie object
|
||||
* @param {String} url URL to check for
|
||||
* @returns {Boolean} true if cookie is valid for specifiec URL
|
||||
*/
|
||||
match(cookie, url) {
|
||||
let urlparts = urllib.parse(url || '');
|
||||
|
||||
// check if hostname matches
|
||||
// .foo.com also matches subdomains, foo.com does not
|
||||
if (
|
||||
urlparts.hostname !== cookie.domain &&
|
||||
(cookie.domain.charAt(0) !== '.' || ('.' + urlparts.hostname).substr(-cookie.domain.length) !== cookie.domain)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if path matches
|
||||
let path = this.getPath(urlparts.pathname);
|
||||
if (path.substr(0, cookie.path.length) !== cookie.path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check secure argument
|
||||
if (cookie.secure && urlparts.protocol !== 'https:') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds (or updates/removes if needed) a cookie object to the cookie storage
|
||||
*
|
||||
* @param {Object} cookie Cookie value to be stored
|
||||
*/
|
||||
add(cookie) {
|
||||
let i;
|
||||
let len;
|
||||
|
||||
// nothing to do here
|
||||
if (!cookie || !cookie.name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// overwrite if has same params
|
||||
for (i = 0, len = this.cookies.length; i < len; i++) {
|
||||
if (this.compare(this.cookies[i], cookie)) {
|
||||
// check if the cookie needs to be removed instead
|
||||
if (this.isExpired(cookie)) {
|
||||
this.cookies.splice(i, 1); // remove expired/unset cookie
|
||||
return false;
|
||||
}
|
||||
|
||||
this.cookies[i] = cookie;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// add as new if not already expired
|
||||
if (!this.isExpired(cookie)) {
|
||||
this.cookies.push(cookie);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two cookie objects are the same
|
||||
*
|
||||
* @param {Object} a Cookie to check against
|
||||
* @param {Object} b Cookie to check against
|
||||
* @returns {Boolean} True, if the cookies are the same
|
||||
*/
|
||||
compare(a, b) {
|
||||
return a.name === b.name && a.path === b.path && a.domain === b.domain && a.secure === b.secure && a.httponly === a.httponly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a cookie is expired
|
||||
*
|
||||
* @param {Object} cookie Cookie object to check against
|
||||
* @returns {Boolean} True, if the cookie is expired
|
||||
*/
|
||||
isExpired(cookie) {
|
||||
return (cookie.expires && cookie.expires < new Date()) || !cookie.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns normalized cookie path for an URL path argument
|
||||
*
|
||||
* @param {String} pathname
|
||||
* @returns {String} Normalized path
|
||||
*/
|
||||
getPath(pathname) {
|
||||
let path = (pathname || '/').split('/');
|
||||
path.pop(); // remove filename part
|
||||
path = path.join('/').trim();
|
||||
|
||||
// ensure path prefix /
|
||||
if (path.charAt(0) !== '/') {
|
||||
path = '/' + path;
|
||||
}
|
||||
|
||||
// ensure path suffix /
|
||||
if (path.substr(-1) !== '/') {
|
||||
path += '/';
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Cookies;
|
274
backend/apis/nodejs/node_modules/nodemailer/lib/fetch/index.js
generated
vendored
Normal file
274
backend/apis/nodejs/node_modules/nodemailer/lib/fetch/index.js
generated
vendored
Normal file
@ -0,0 +1,274 @@
|
||||
'use strict';
|
||||
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const urllib = require('url');
|
||||
const zlib = require('zlib');
|
||||
const PassThrough = require('stream').PassThrough;
|
||||
const Cookies = require('./cookies');
|
||||
const packageData = require('../../package.json');
|
||||
const net = require('net');
|
||||
|
||||
const MAX_REDIRECTS = 5;
|
||||
|
||||
module.exports = function (url, options) {
|
||||
return nmfetch(url, options);
|
||||
};
|
||||
|
||||
module.exports.Cookies = Cookies;
|
||||
|
||||
function nmfetch(url, options) {
|
||||
options = options || {};
|
||||
|
||||
options.fetchRes = options.fetchRes || new PassThrough();
|
||||
options.cookies = options.cookies || new Cookies();
|
||||
options.redirects = options.redirects || 0;
|
||||
options.maxRedirects = isNaN(options.maxRedirects) ? MAX_REDIRECTS : options.maxRedirects;
|
||||
|
||||
if (options.cookie) {
|
||||
[].concat(options.cookie || []).forEach(cookie => {
|
||||
options.cookies.set(cookie, url);
|
||||
});
|
||||
options.cookie = false;
|
||||
}
|
||||
|
||||
let fetchRes = options.fetchRes;
|
||||
let parsed = urllib.parse(url);
|
||||
let method = (options.method || '').toString().trim().toUpperCase() || 'GET';
|
||||
let finished = false;
|
||||
let cookies;
|
||||
let body;
|
||||
|
||||
let handler = parsed.protocol === 'https:' ? https : http;
|
||||
|
||||
let headers = {
|
||||
'accept-encoding': 'gzip,deflate',
|
||||
'user-agent': 'nodemailer/' + packageData.version
|
||||
};
|
||||
|
||||
Object.keys(options.headers || {}).forEach(key => {
|
||||
headers[key.toLowerCase().trim()] = options.headers[key];
|
||||
});
|
||||
|
||||
if (options.userAgent) {
|
||||
headers['user-agent'] = options.userAgent;
|
||||
}
|
||||
|
||||
if (parsed.auth) {
|
||||
headers.Authorization = 'Basic ' + Buffer.from(parsed.auth).toString('base64');
|
||||
}
|
||||
|
||||
if ((cookies = options.cookies.get(url))) {
|
||||
headers.cookie = cookies;
|
||||
}
|
||||
|
||||
if (options.body) {
|
||||
if (options.contentType !== false) {
|
||||
headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded';
|
||||
}
|
||||
|
||||
if (typeof options.body.pipe === 'function') {
|
||||
// it's a stream
|
||||
headers['Transfer-Encoding'] = 'chunked';
|
||||
body = options.body;
|
||||
body.on('error', err => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.type = 'FETCH';
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
});
|
||||
} else {
|
||||
if (options.body instanceof Buffer) {
|
||||
body = options.body;
|
||||
} else if (typeof options.body === 'object') {
|
||||
try {
|
||||
// encodeURIComponent can fail on invalid input (partial emoji etc.)
|
||||
body = Buffer.from(
|
||||
Object.keys(options.body)
|
||||
.map(key => {
|
||||
let value = options.body[key].toString().trim();
|
||||
return encodeURIComponent(key) + '=' + encodeURIComponent(value);
|
||||
})
|
||||
.join('&')
|
||||
);
|
||||
} catch (E) {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
E.type = 'FETCH';
|
||||
E.sourceUrl = url;
|
||||
fetchRes.emit('error', E);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
body = Buffer.from(options.body.toString().trim());
|
||||
}
|
||||
|
||||
headers['Content-Type'] = options.contentType || 'application/x-www-form-urlencoded';
|
||||
headers['Content-Length'] = body.length;
|
||||
}
|
||||
// if method is not provided, use POST instead of GET
|
||||
method = (options.method || '').toString().trim().toUpperCase() || 'POST';
|
||||
}
|
||||
|
||||
let req;
|
||||
let reqOptions = {
|
||||
method,
|
||||
host: parsed.hostname,
|
||||
path: parsed.path,
|
||||
port: parsed.port ? parsed.port : parsed.protocol === 'https:' ? 443 : 80,
|
||||
headers,
|
||||
rejectUnauthorized: false,
|
||||
agent: false
|
||||
};
|
||||
|
||||
if (options.tls) {
|
||||
Object.keys(options.tls).forEach(key => {
|
||||
reqOptions[key] = options.tls[key];
|
||||
});
|
||||
}
|
||||
|
||||
if (parsed.protocol === 'https:' && parsed.hostname && parsed.hostname !== reqOptions.host && !net.isIP(parsed.hostname) && !reqOptions.servername) {
|
||||
reqOptions.servername = parsed.hostname;
|
||||
}
|
||||
|
||||
try {
|
||||
req = handler.request(reqOptions);
|
||||
} catch (E) {
|
||||
finished = true;
|
||||
setImmediate(() => {
|
||||
E.type = 'FETCH';
|
||||
E.sourceUrl = url;
|
||||
fetchRes.emit('error', E);
|
||||
});
|
||||
return fetchRes;
|
||||
}
|
||||
|
||||
if (options.timeout) {
|
||||
req.setTimeout(options.timeout, () => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
req.abort();
|
||||
let err = new Error('Request Timeout');
|
||||
err.type = 'FETCH';
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
});
|
||||
}
|
||||
|
||||
req.on('error', err => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.type = 'FETCH';
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
});
|
||||
|
||||
req.on('response', res => {
|
||||
let inflate;
|
||||
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (res.headers['content-encoding']) {
|
||||
case 'gzip':
|
||||
case 'deflate':
|
||||
inflate = zlib.createUnzip();
|
||||
break;
|
||||
}
|
||||
|
||||
if (res.headers['set-cookie']) {
|
||||
[].concat(res.headers['set-cookie'] || []).forEach(cookie => {
|
||||
options.cookies.set(cookie, url);
|
||||
});
|
||||
}
|
||||
|
||||
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
|
||||
// redirect
|
||||
options.redirects++;
|
||||
if (options.redirects > options.maxRedirects) {
|
||||
finished = true;
|
||||
let err = new Error('Maximum redirect count exceeded');
|
||||
err.type = 'FETCH';
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
return;
|
||||
}
|
||||
// redirect does not include POST body
|
||||
options.method = 'GET';
|
||||
options.body = false;
|
||||
return nmfetch(urllib.resolve(url, res.headers.location), options);
|
||||
}
|
||||
|
||||
fetchRes.statusCode = res.statusCode;
|
||||
fetchRes.headers = res.headers;
|
||||
|
||||
if (res.statusCode >= 300 && !options.allowErrorResponse) {
|
||||
finished = true;
|
||||
let err = new Error('Invalid status code ' + res.statusCode);
|
||||
err.type = 'FETCH';
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
return;
|
||||
}
|
||||
|
||||
res.on('error', err => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.type = 'FETCH';
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
});
|
||||
|
||||
if (inflate) {
|
||||
res.pipe(inflate).pipe(fetchRes);
|
||||
inflate.on('error', err => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.type = 'FETCH';
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
});
|
||||
} else {
|
||||
res.pipe(fetchRes);
|
||||
}
|
||||
});
|
||||
|
||||
setImmediate(() => {
|
||||
if (body) {
|
||||
try {
|
||||
if (typeof body.pipe === 'function') {
|
||||
return body.pipe(req);
|
||||
} else {
|
||||
req.write(body);
|
||||
}
|
||||
} catch (err) {
|
||||
finished = true;
|
||||
err.type = 'FETCH';
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
|
||||
return fetchRes;
|
||||
}
|
82
backend/apis/nodejs/node_modules/nodemailer/lib/json-transport/index.js
generated
vendored
Normal file
82
backend/apis/nodejs/node_modules/nodemailer/lib/json-transport/index.js
generated
vendored
Normal file
@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
const packageData = require('../../package.json');
|
||||
const shared = require('../shared');
|
||||
|
||||
/**
|
||||
* Generates a Transport object to generate JSON output
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} optional config parameter
|
||||
*/
|
||||
class JSONTransport {
|
||||
constructor(options) {
|
||||
options = options || {};
|
||||
|
||||
this.options = options || {};
|
||||
|
||||
this.name = 'JSONTransport';
|
||||
this.version = packageData.version;
|
||||
|
||||
this.logger = shared.getLogger(this.options, {
|
||||
component: this.options.component || 'json-transport'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Compiles a mailcomposer message and forwards it to handler that sends it.</p>
|
||||
*
|
||||
* @param {Object} emailMessage MailComposer object
|
||||
* @param {Function} callback Callback function to run when the sending is completed
|
||||
*/
|
||||
send(mail, done) {
|
||||
// Sendmail strips this header line by itself
|
||||
mail.message.keepBcc = true;
|
||||
|
||||
let envelope = mail.data.envelope || mail.message.getEnvelope();
|
||||
let messageId = mail.message.messageId();
|
||||
|
||||
let recipients = [].concat(envelope.to || []);
|
||||
if (recipients.length > 3) {
|
||||
recipients.push('...and ' + recipients.splice(2).length + ' more');
|
||||
}
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'send',
|
||||
messageId
|
||||
},
|
||||
'Composing JSON structure of %s to <%s>',
|
||||
messageId,
|
||||
recipients.join(', ')
|
||||
);
|
||||
|
||||
setImmediate(() => {
|
||||
mail.normalize((err, data) => {
|
||||
if (err) {
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'send',
|
||||
messageId
|
||||
},
|
||||
'Failed building JSON structure for %s. %s',
|
||||
messageId,
|
||||
err.message
|
||||
);
|
||||
return done(err);
|
||||
}
|
||||
|
||||
delete data.envelope;
|
||||
delete data.normalizedHeaders;
|
||||
|
||||
return done(null, {
|
||||
envelope,
|
||||
messageId,
|
||||
message: this.options.skipEncoding ? data : JSON.stringify(data)
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JSONTransport;
|
565
backend/apis/nodejs/node_modules/nodemailer/lib/mail-composer/index.js
generated
vendored
Normal file
565
backend/apis/nodejs/node_modules/nodemailer/lib/mail-composer/index.js
generated
vendored
Normal file
@ -0,0 +1,565 @@
|
||||
/* eslint no-undefined: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
const MimeNode = require('../mime-node');
|
||||
const mimeFuncs = require('../mime-funcs');
|
||||
const parseDataURI = require('../shared').parseDataURI;
|
||||
|
||||
/**
|
||||
* Creates the object for composing a MimeNode instance out from the mail options
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} mail Mail options
|
||||
*/
|
||||
class MailComposer {
|
||||
constructor(mail) {
|
||||
this.mail = mail || {};
|
||||
this.message = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds MimeNode instance
|
||||
*/
|
||||
compile() {
|
||||
this._alternatives = this.getAlternatives();
|
||||
this._htmlNode = this._alternatives.filter(alternative => /^text\/html\b/i.test(alternative.contentType)).pop();
|
||||
this._attachments = this.getAttachments(!!this._htmlNode);
|
||||
|
||||
this._useRelated = !!(this._htmlNode && this._attachments.related.length);
|
||||
this._useAlternative = this._alternatives.length > 1;
|
||||
this._useMixed = this._attachments.attached.length > 1 || (this._alternatives.length && this._attachments.attached.length === 1);
|
||||
|
||||
// Compose MIME tree
|
||||
if (this.mail.raw) {
|
||||
this.message = new MimeNode('message/rfc822', { newline: this.mail.newline }).setRaw(this.mail.raw);
|
||||
} else if (this._useMixed) {
|
||||
this.message = this._createMixed();
|
||||
} else if (this._useAlternative) {
|
||||
this.message = this._createAlternative();
|
||||
} else if (this._useRelated) {
|
||||
this.message = this._createRelated();
|
||||
} else {
|
||||
this.message = this._createContentNode(
|
||||
false,
|
||||
[]
|
||||
.concat(this._alternatives || [])
|
||||
.concat(this._attachments.attached || [])
|
||||
.shift() || {
|
||||
contentType: 'text/plain',
|
||||
content: ''
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Add custom headers
|
||||
if (this.mail.headers) {
|
||||
this.message.addHeader(this.mail.headers);
|
||||
}
|
||||
|
||||
// Add headers to the root node, always overrides custom headers
|
||||
['from', 'sender', 'to', 'cc', 'bcc', 'reply-to', 'in-reply-to', 'references', 'subject', 'message-id', 'date'].forEach(header => {
|
||||
let key = header.replace(/-(\w)/g, (o, c) => c.toUpperCase());
|
||||
if (this.mail[key]) {
|
||||
this.message.setHeader(header, this.mail[key]);
|
||||
}
|
||||
});
|
||||
|
||||
// Sets custom envelope
|
||||
if (this.mail.envelope) {
|
||||
this.message.setEnvelope(this.mail.envelope);
|
||||
}
|
||||
|
||||
// ensure Message-Id value
|
||||
this.message.messageId();
|
||||
|
||||
return this.message;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all attachments. Resulting attachment objects can be used as input for MimeNode nodes
|
||||
*
|
||||
* @param {Boolean} findRelated If true separate related attachments from attached ones
|
||||
* @returns {Object} An object of arrays (`related` and `attached`)
|
||||
*/
|
||||
getAttachments(findRelated) {
|
||||
let icalEvent, eventObject;
|
||||
let attachments = [].concat(this.mail.attachments || []).map((attachment, i) => {
|
||||
let data;
|
||||
let isMessageNode = /^message\//i.test(attachment.contentType);
|
||||
|
||||
if (/^data:/i.test(attachment.path || attachment.href)) {
|
||||
attachment = this._processDataUrl(attachment);
|
||||
}
|
||||
|
||||
let contentType = attachment.contentType || mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin');
|
||||
let isImage = /^image\//i.test(contentType);
|
||||
let contentDisposition = attachment.contentDisposition || (isMessageNode || (isImage && attachment.cid) ? 'inline' : 'attachment');
|
||||
|
||||
data = {
|
||||
contentType,
|
||||
contentDisposition,
|
||||
contentTransferEncoding: 'contentTransferEncoding' in attachment ? attachment.contentTransferEncoding : 'base64'
|
||||
};
|
||||
|
||||
if (attachment.filename) {
|
||||
data.filename = attachment.filename;
|
||||
} else if (!isMessageNode && attachment.filename !== false) {
|
||||
data.filename = (attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1);
|
||||
if (data.filename.indexOf('.') < 0) {
|
||||
data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
|
||||
}
|
||||
}
|
||||
|
||||
if (/^https?:\/\//i.test(attachment.path)) {
|
||||
attachment.href = attachment.path;
|
||||
attachment.path = undefined;
|
||||
}
|
||||
|
||||
if (attachment.cid) {
|
||||
data.cid = attachment.cid;
|
||||
}
|
||||
|
||||
if (attachment.raw) {
|
||||
data.raw = attachment.raw;
|
||||
} else if (attachment.path) {
|
||||
data.content = {
|
||||
path: attachment.path
|
||||
};
|
||||
} else if (attachment.href) {
|
||||
data.content = {
|
||||
href: attachment.href,
|
||||
httpHeaders: attachment.httpHeaders
|
||||
};
|
||||
} else {
|
||||
data.content = attachment.content || '';
|
||||
}
|
||||
|
||||
if (attachment.encoding) {
|
||||
data.encoding = attachment.encoding;
|
||||
}
|
||||
|
||||
if (attachment.headers) {
|
||||
data.headers = attachment.headers;
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
if (this.mail.icalEvent) {
|
||||
if (
|
||||
typeof this.mail.icalEvent === 'object' &&
|
||||
(this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw)
|
||||
) {
|
||||
icalEvent = this.mail.icalEvent;
|
||||
} else {
|
||||
icalEvent = {
|
||||
content: this.mail.icalEvent
|
||||
};
|
||||
}
|
||||
|
||||
eventObject = {};
|
||||
Object.keys(icalEvent).forEach(key => {
|
||||
eventObject[key] = icalEvent[key];
|
||||
});
|
||||
|
||||
eventObject.contentType = 'application/ics';
|
||||
if (!eventObject.headers) {
|
||||
eventObject.headers = {};
|
||||
}
|
||||
eventObject.filename = eventObject.filename || 'invite.ics';
|
||||
eventObject.headers['Content-Disposition'] = 'attachment';
|
||||
eventObject.headers['Content-Transfer-Encoding'] = 'base64';
|
||||
}
|
||||
|
||||
if (!findRelated) {
|
||||
return {
|
||||
attached: attachments.concat(eventObject || []),
|
||||
related: []
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
attached: attachments.filter(attachment => !attachment.cid).concat(eventObject || []),
|
||||
related: attachments.filter(attachment => !!attachment.cid)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List alternatives. Resulting objects can be used as input for MimeNode nodes
|
||||
*
|
||||
* @returns {Array} An array of alternative elements. Includes the `text` and `html` values as well
|
||||
*/
|
||||
getAlternatives() {
|
||||
let alternatives = [],
|
||||
text,
|
||||
html,
|
||||
watchHtml,
|
||||
amp,
|
||||
icalEvent,
|
||||
eventObject;
|
||||
|
||||
if (this.mail.text) {
|
||||
if (typeof this.mail.text === 'object' && (this.mail.text.content || this.mail.text.path || this.mail.text.href || this.mail.text.raw)) {
|
||||
text = this.mail.text;
|
||||
} else {
|
||||
text = {
|
||||
content: this.mail.text
|
||||
};
|
||||
}
|
||||
text.contentType = 'text/plain; charset=utf-8';
|
||||
}
|
||||
|
||||
if (this.mail.watchHtml) {
|
||||
if (
|
||||
typeof this.mail.watchHtml === 'object' &&
|
||||
(this.mail.watchHtml.content || this.mail.watchHtml.path || this.mail.watchHtml.href || this.mail.watchHtml.raw)
|
||||
) {
|
||||
watchHtml = this.mail.watchHtml;
|
||||
} else {
|
||||
watchHtml = {
|
||||
content: this.mail.watchHtml
|
||||
};
|
||||
}
|
||||
watchHtml.contentType = 'text/watch-html; charset=utf-8';
|
||||
}
|
||||
|
||||
if (this.mail.amp) {
|
||||
if (typeof this.mail.amp === 'object' && (this.mail.amp.content || this.mail.amp.path || this.mail.amp.href || this.mail.amp.raw)) {
|
||||
amp = this.mail.amp;
|
||||
} else {
|
||||
amp = {
|
||||
content: this.mail.amp
|
||||
};
|
||||
}
|
||||
amp.contentType = 'text/x-amp-html; charset=utf-8';
|
||||
}
|
||||
|
||||
// NB! when including attachments with a calendar alternative you might end up in a blank screen on some clients
|
||||
if (this.mail.icalEvent) {
|
||||
if (
|
||||
typeof this.mail.icalEvent === 'object' &&
|
||||
(this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw)
|
||||
) {
|
||||
icalEvent = this.mail.icalEvent;
|
||||
} else {
|
||||
icalEvent = {
|
||||
content: this.mail.icalEvent
|
||||
};
|
||||
}
|
||||
|
||||
eventObject = {};
|
||||
Object.keys(icalEvent).forEach(key => {
|
||||
eventObject[key] = icalEvent[key];
|
||||
});
|
||||
|
||||
if (eventObject.content && typeof eventObject.content === 'object') {
|
||||
// we are going to have the same attachment twice, so mark this to be
|
||||
// resolved just once
|
||||
eventObject.content._resolve = true;
|
||||
}
|
||||
|
||||
eventObject.filename = false;
|
||||
eventObject.contentType = 'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
|
||||
if (!eventObject.headers) {
|
||||
eventObject.headers = {};
|
||||
}
|
||||
}
|
||||
|
||||
if (this.mail.html) {
|
||||
if (typeof this.mail.html === 'object' && (this.mail.html.content || this.mail.html.path || this.mail.html.href || this.mail.html.raw)) {
|
||||
html = this.mail.html;
|
||||
} else {
|
||||
html = {
|
||||
content: this.mail.html
|
||||
};
|
||||
}
|
||||
html.contentType = 'text/html; charset=utf-8';
|
||||
}
|
||||
|
||||
[]
|
||||
.concat(text || [])
|
||||
.concat(watchHtml || [])
|
||||
.concat(amp || [])
|
||||
.concat(html || [])
|
||||
.concat(eventObject || [])
|
||||
.concat(this.mail.alternatives || [])
|
||||
.forEach(alternative => {
|
||||
let data;
|
||||
|
||||
if (/^data:/i.test(alternative.path || alternative.href)) {
|
||||
alternative = this._processDataUrl(alternative);
|
||||
}
|
||||
|
||||
data = {
|
||||
contentType: alternative.contentType || mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
|
||||
contentTransferEncoding: alternative.contentTransferEncoding
|
||||
};
|
||||
|
||||
if (alternative.filename) {
|
||||
data.filename = alternative.filename;
|
||||
}
|
||||
|
||||
if (/^https?:\/\//i.test(alternative.path)) {
|
||||
alternative.href = alternative.path;
|
||||
alternative.path = undefined;
|
||||
}
|
||||
|
||||
if (alternative.raw) {
|
||||
data.raw = alternative.raw;
|
||||
} else if (alternative.path) {
|
||||
data.content = {
|
||||
path: alternative.path
|
||||
};
|
||||
} else if (alternative.href) {
|
||||
data.content = {
|
||||
href: alternative.href
|
||||
};
|
||||
} else {
|
||||
data.content = alternative.content || '';
|
||||
}
|
||||
|
||||
if (alternative.encoding) {
|
||||
data.encoding = alternative.encoding;
|
||||
}
|
||||
|
||||
if (alternative.headers) {
|
||||
data.headers = alternative.headers;
|
||||
}
|
||||
|
||||
alternatives.push(data);
|
||||
});
|
||||
|
||||
return alternatives;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds multipart/mixed node. It should always contain different type of elements on the same level
|
||||
* eg. text + attachments
|
||||
*
|
||||
* @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
|
||||
* @returns {Object} MimeNode node element
|
||||
*/
|
||||
_createMixed(parentNode) {
|
||||
let node;
|
||||
|
||||
if (!parentNode) {
|
||||
node = new MimeNode('multipart/mixed', {
|
||||
baseBoundary: this.mail.baseBoundary,
|
||||
textEncoding: this.mail.textEncoding,
|
||||
boundaryPrefix: this.mail.boundaryPrefix,
|
||||
disableUrlAccess: this.mail.disableUrlAccess,
|
||||
disableFileAccess: this.mail.disableFileAccess,
|
||||
normalizeHeaderKey: this.mail.normalizeHeaderKey,
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
if (this._useAlternative) {
|
||||
this._createAlternative(node);
|
||||
} else if (this._useRelated) {
|
||||
this._createRelated(node);
|
||||
}
|
||||
|
||||
[]
|
||||
.concat((!this._useAlternative && this._alternatives) || [])
|
||||
.concat(this._attachments.attached || [])
|
||||
.forEach(element => {
|
||||
// if the element is a html node from related subpart then ignore it
|
||||
if (!this._useRelated || element !== this._htmlNode) {
|
||||
this._createContentNode(node, element);
|
||||
}
|
||||
});
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds multipart/alternative node. It should always contain same type of elements on the same level
|
||||
* eg. text + html view of the same data
|
||||
*
|
||||
* @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
|
||||
* @returns {Object} MimeNode node element
|
||||
*/
|
||||
_createAlternative(parentNode) {
|
||||
let node;
|
||||
|
||||
if (!parentNode) {
|
||||
node = new MimeNode('multipart/alternative', {
|
||||
baseBoundary: this.mail.baseBoundary,
|
||||
textEncoding: this.mail.textEncoding,
|
||||
boundaryPrefix: this.mail.boundaryPrefix,
|
||||
disableUrlAccess: this.mail.disableUrlAccess,
|
||||
disableFileAccess: this.mail.disableFileAccess,
|
||||
normalizeHeaderKey: this.mail.normalizeHeaderKey,
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
this._alternatives.forEach(alternative => {
|
||||
if (this._useRelated && this._htmlNode === alternative) {
|
||||
this._createRelated(node);
|
||||
} else {
|
||||
this._createContentNode(node, alternative);
|
||||
}
|
||||
});
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds multipart/related node. It should always contain html node with related attachments
|
||||
*
|
||||
* @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
|
||||
* @returns {Object} MimeNode node element
|
||||
*/
|
||||
_createRelated(parentNode) {
|
||||
let node;
|
||||
|
||||
if (!parentNode) {
|
||||
node = new MimeNode('multipart/related; type="text/html"', {
|
||||
baseBoundary: this.mail.baseBoundary,
|
||||
textEncoding: this.mail.textEncoding,
|
||||
boundaryPrefix: this.mail.boundaryPrefix,
|
||||
disableUrlAccess: this.mail.disableUrlAccess,
|
||||
disableFileAccess: this.mail.disableFileAccess,
|
||||
normalizeHeaderKey: this.mail.normalizeHeaderKey,
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
this._createContentNode(node, this._htmlNode);
|
||||
|
||||
this._attachments.related.forEach(alternative => this._createContentNode(node, alternative));
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a regular node with contents
|
||||
*
|
||||
* @param {Object} parentNode Parent for this note. If it does not exist, a root node is created
|
||||
* @param {Object} element Node data
|
||||
* @returns {Object} MimeNode node element
|
||||
*/
|
||||
_createContentNode(parentNode, element) {
|
||||
element = element || {};
|
||||
element.content = element.content || '';
|
||||
|
||||
let node;
|
||||
let encoding = (element.encoding || 'utf8')
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.replace(/[-_\s]/g, '');
|
||||
|
||||
if (!parentNode) {
|
||||
node = new MimeNode(element.contentType, {
|
||||
filename: element.filename,
|
||||
baseBoundary: this.mail.baseBoundary,
|
||||
textEncoding: this.mail.textEncoding,
|
||||
boundaryPrefix: this.mail.boundaryPrefix,
|
||||
disableUrlAccess: this.mail.disableUrlAccess,
|
||||
disableFileAccess: this.mail.disableFileAccess,
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
// add custom headers
|
||||
if (element.headers) {
|
||||
node.addHeader(element.headers);
|
||||
}
|
||||
|
||||
if (element.cid) {
|
||||
node.setHeader('Content-Id', '<' + element.cid.replace(/[<>]/g, '') + '>');
|
||||
}
|
||||
|
||||
if (element.contentTransferEncoding) {
|
||||
node.setHeader('Content-Transfer-Encoding', element.contentTransferEncoding);
|
||||
} else if (this.mail.encoding && /^text\//i.test(element.contentType)) {
|
||||
node.setHeader('Content-Transfer-Encoding', this.mail.encoding);
|
||||
}
|
||||
|
||||
if (!/^text\//i.test(element.contentType) || element.contentDisposition) {
|
||||
node.setHeader(
|
||||
'Content-Disposition',
|
||||
element.contentDisposition || (element.cid && /^image\//i.test(element.contentType) ? 'inline' : 'attachment')
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof element.content === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) {
|
||||
element.content = Buffer.from(element.content, encoding);
|
||||
}
|
||||
|
||||
// prefer pregenerated raw content
|
||||
if (element.raw) {
|
||||
node.setRaw(element.raw);
|
||||
} else {
|
||||
node.setContent(element.content);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses data uri and converts it to a Buffer
|
||||
*
|
||||
* @param {Object} element Content element
|
||||
* @return {Object} Parsed element
|
||||
*/
|
||||
_processDataUrl(element) {
|
||||
let parsedDataUri;
|
||||
if ((element.path || element.href).match(/^data:/)) {
|
||||
parsedDataUri = parseDataURI(element.path || element.href);
|
||||
}
|
||||
|
||||
if (!parsedDataUri) {
|
||||
return element;
|
||||
}
|
||||
|
||||
element.content = parsedDataUri.data;
|
||||
element.contentType = element.contentType || parsedDataUri.contentType;
|
||||
|
||||
if ('path' in element) {
|
||||
element.path = false;
|
||||
}
|
||||
|
||||
if ('href' in element) {
|
||||
element.href = false;
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MailComposer;
|
429
backend/apis/nodejs/node_modules/nodemailer/lib/mailer/index.js
generated
vendored
Normal file
429
backend/apis/nodejs/node_modules/nodemailer/lib/mailer/index.js
generated
vendored
Normal file
@ -0,0 +1,429 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const shared = require('../shared');
|
||||
const mimeTypes = require('../mime-funcs/mime-types');
|
||||
const MailComposer = require('../mail-composer');
|
||||
const DKIM = require('../dkim');
|
||||
const httpProxyClient = require('../smtp-connection/http-proxy-client');
|
||||
const util = require('util');
|
||||
const urllib = require('url');
|
||||
const packageData = require('../../package.json');
|
||||
const MailMessage = require('./mail-message');
|
||||
const net = require('net');
|
||||
const dns = require('dns');
|
||||
const crypto = require('crypto');
|
||||
|
||||
/**
|
||||
* Creates an object for exposing the Mail API
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} transporter Transport object instance to pass the mails to
|
||||
*/
|
||||
class Mail extends EventEmitter {
|
||||
constructor(transporter, options, defaults) {
|
||||
super();
|
||||
|
||||
this.options = options || {};
|
||||
this._defaults = defaults || {};
|
||||
|
||||
this._defaultPlugins = {
|
||||
compile: [(...args) => this._convertDataImages(...args)],
|
||||
stream: []
|
||||
};
|
||||
|
||||
this._userPlugins = {
|
||||
compile: [],
|
||||
stream: []
|
||||
};
|
||||
|
||||
this.meta = new Map();
|
||||
|
||||
this.dkim = this.options.dkim ? new DKIM(this.options.dkim) : false;
|
||||
|
||||
this.transporter = transporter;
|
||||
this.transporter.mailer = this;
|
||||
|
||||
this.logger = shared.getLogger(this.options, {
|
||||
component: this.options.component || 'mail'
|
||||
});
|
||||
|
||||
this.logger.debug(
|
||||
{
|
||||
tnx: 'create'
|
||||
},
|
||||
'Creating transport: %s',
|
||||
this.getVersionString()
|
||||
);
|
||||
|
||||
// setup emit handlers for the transporter
|
||||
if (typeof this.transporter.on === 'function') {
|
||||
// deprecated log interface
|
||||
this.transporter.on('log', log => {
|
||||
this.logger.debug(
|
||||
{
|
||||
tnx: 'transport'
|
||||
},
|
||||
'%s: %s',
|
||||
log.type,
|
||||
log.message
|
||||
);
|
||||
});
|
||||
|
||||
// transporter errors
|
||||
this.transporter.on('error', err => {
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'transport'
|
||||
},
|
||||
'Transport Error: %s',
|
||||
err.message
|
||||
);
|
||||
this.emit('error', err);
|
||||
});
|
||||
|
||||
// indicates if the sender has became idle
|
||||
this.transporter.on('idle', (...args) => {
|
||||
this.emit('idle', ...args);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional methods passed to the underlying transport object
|
||||
*/
|
||||
['close', 'isIdle', 'verify'].forEach(method => {
|
||||
this[method] = (...args) => {
|
||||
if (typeof this.transporter[method] === 'function') {
|
||||
if (method === 'verify' && typeof this.getSocket === 'function') {
|
||||
this.transporter.getSocket = this.getSocket;
|
||||
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;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// setup proxy handling
|
||||
if (this.options.proxy && typeof this.options.proxy === 'string') {
|
||||
this.setupProxy(this.options.proxy);
|
||||
}
|
||||
}
|
||||
|
||||
use(step, plugin) {
|
||||
step = (step || '').toString();
|
||||
if (!this._userPlugins.hasOwnProperty(step)) {
|
||||
this._userPlugins[step] = [plugin];
|
||||
} else {
|
||||
this._userPlugins[step].push(plugin);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email using the preselected transport object
|
||||
*
|
||||
* @param {Object} data E-data description
|
||||
* @param {Function?} callback Callback to run once the sending succeeded or failed
|
||||
*/
|
||||
sendMail(data, callback = null) {
|
||||
let promise;
|
||||
|
||||
if (!callback) {
|
||||
promise = new Promise((resolve, reject) => {
|
||||
callback = shared.callbackPromise(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof this.getSocket === 'function') {
|
||||
this.transporter.getSocket = this.getSocket;
|
||||
this.getSocket = false;
|
||||
}
|
||||
|
||||
let mail = new MailMessage(this, data);
|
||||
|
||||
this.logger.debug(
|
||||
{
|
||||
tnx: 'transport',
|
||||
name: this.transporter.name,
|
||||
version: this.transporter.version,
|
||||
action: 'send'
|
||||
},
|
||||
'Sending mail using %s/%s',
|
||||
this.transporter.name,
|
||||
this.transporter.version
|
||||
);
|
||||
|
||||
this._processPlugins('compile', mail, err => {
|
||||
if (err) {
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'plugin',
|
||||
action: 'compile'
|
||||
},
|
||||
'PluginCompile Error: %s',
|
||||
err.message
|
||||
);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
mail.message = new MailComposer(mail.data).compile();
|
||||
|
||||
mail.setMailerHeader();
|
||||
mail.setPriorityHeaders();
|
||||
mail.setListHeaders();
|
||||
|
||||
this._processPlugins('stream', mail, err => {
|
||||
if (err) {
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'plugin',
|
||||
action: 'stream'
|
||||
},
|
||||
'PluginStream Error: %s',
|
||||
err.message
|
||||
);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (mail.data.dkim || this.dkim) {
|
||||
mail.message.processFunc(input => {
|
||||
let dkim = mail.data.dkim ? new DKIM(mail.data.dkim) : this.dkim;
|
||||
this.logger.debug(
|
||||
{
|
||||
tnx: 'DKIM',
|
||||
messageId: mail.message.messageId(),
|
||||
dkimDomains: dkim.keys.map(key => key.keySelector + '.' + key.domainName).join(', ')
|
||||
},
|
||||
'Signing outgoing message with %s keys',
|
||||
dkim.keys.length
|
||||
);
|
||||
return dkim.sign(input, mail.data._dkim);
|
||||
});
|
||||
}
|
||||
|
||||
this.transporter.send(mail, (...args) => {
|
||||
if (args[0]) {
|
||||
this.logger.error(
|
||||
{
|
||||
err: args[0],
|
||||
tnx: 'transport',
|
||||
action: 'send'
|
||||
},
|
||||
'Send Error: %s',
|
||||
args[0].message
|
||||
);
|
||||
}
|
||||
callback(...args);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
getVersionString() {
|
||||
return util.format('%s (%s; +%s; %s/%s)', packageData.name, packageData.version, packageData.homepage, this.transporter.name, this.transporter.version);
|
||||
}
|
||||
|
||||
_processPlugins(step, mail, callback) {
|
||||
step = (step || '').toString();
|
||||
|
||||
if (!this._userPlugins.hasOwnProperty(step)) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
let userPlugins = this._userPlugins[step] || [];
|
||||
let defaultPlugins = this._defaultPlugins[step] || [];
|
||||
|
||||
if (userPlugins.length) {
|
||||
this.logger.debug(
|
||||
{
|
||||
tnx: 'transaction',
|
||||
pluginCount: userPlugins.length,
|
||||
step
|
||||
},
|
||||
'Using %s plugins for %s',
|
||||
userPlugins.length,
|
||||
step
|
||||
);
|
||||
}
|
||||
|
||||
if (userPlugins.length + defaultPlugins.length === 0) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
let pos = 0;
|
||||
let block = 'default';
|
||||
let processPlugins = () => {
|
||||
let curplugins = block === 'default' ? defaultPlugins : userPlugins;
|
||||
if (pos >= curplugins.length) {
|
||||
if (block === 'default' && userPlugins.length) {
|
||||
block = 'user';
|
||||
pos = 0;
|
||||
curplugins = userPlugins;
|
||||
} else {
|
||||
return callback();
|
||||
}
|
||||
}
|
||||
let plugin = curplugins[pos++];
|
||||
plugin(mail, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
processPlugins();
|
||||
});
|
||||
};
|
||||
|
||||
processPlugins();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up proxy handler for a Nodemailer object
|
||||
*
|
||||
* @param {String} proxyUrl Proxy configuration url
|
||||
*/
|
||||
setupProxy(proxyUrl) {
|
||||
let proxy = urllib.parse(proxyUrl);
|
||||
|
||||
// setup socket handler for the mailer object
|
||||
this.getSocket = (options, callback) => {
|
||||
let protocol = proxy.protocol.replace(/:$/, '').toLowerCase();
|
||||
|
||||
if (this.meta.has('proxy_handler_' + protocol)) {
|
||||
return this.meta.get('proxy_handler_' + protocol)(proxy, options, callback);
|
||||
}
|
||||
|
||||
switch (protocol) {
|
||||
// Connect using a HTTP CONNECT method
|
||||
case 'http':
|
||||
case 'https':
|
||||
httpProxyClient(proxy.href, options.port, options.host, (err, socket) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, {
|
||||
connection: socket
|
||||
});
|
||||
});
|
||||
return;
|
||||
case 'socks':
|
||||
case 'socks5':
|
||||
case 'socks4':
|
||||
case 'socks4a': {
|
||||
if (!this.meta.has('proxy_socks_module')) {
|
||||
return callback(new Error('Socks module not loaded'));
|
||||
}
|
||||
let connect = ipaddress => {
|
||||
let proxyV2 = !!this.meta.get('proxy_socks_module').SocksClient;
|
||||
let socksClient = proxyV2 ? this.meta.get('proxy_socks_module').SocksClient : this.meta.get('proxy_socks_module');
|
||||
let proxyType = Number(proxy.protocol.replace(/\D/g, '')) || 5;
|
||||
let connectionOpts = {
|
||||
proxy: {
|
||||
ipaddress,
|
||||
port: Number(proxy.port),
|
||||
type: proxyType
|
||||
},
|
||||
[proxyV2 ? 'destination' : 'target']: {
|
||||
host: options.host,
|
||||
port: options.port
|
||||
},
|
||||
command: 'connect'
|
||||
};
|
||||
|
||||
if (proxy.auth) {
|
||||
let username = decodeURIComponent(proxy.auth.split(':').shift());
|
||||
let password = decodeURIComponent(proxy.auth.split(':').pop());
|
||||
if (proxyV2) {
|
||||
connectionOpts.proxy.userId = username;
|
||||
connectionOpts.proxy.password = password;
|
||||
} else if (proxyType === 4) {
|
||||
connectionOpts.userid = username;
|
||||
} else {
|
||||
connectionOpts.authentication = {
|
||||
username,
|
||||
password
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
socksClient.createConnection(connectionOpts, (err, info) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, {
|
||||
connection: info.socket || info
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (net.isIP(proxy.hostname)) {
|
||||
return connect(proxy.hostname);
|
||||
}
|
||||
|
||||
return dns.resolve(proxy.hostname, (err, address) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connect(Array.isArray(address) ? address[0] : address);
|
||||
});
|
||||
}
|
||||
}
|
||||
callback(new Error('Unknown proxy configuration'));
|
||||
};
|
||||
}
|
||||
|
||||
_convertDataImages(mail, callback) {
|
||||
if ((!this.options.attachDataUrls && !mail.data.attachDataUrls) || !mail.data.html) {
|
||||
return callback();
|
||||
}
|
||||
mail.resolveContent(mail.data, 'html', (err, html) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let cidCounter = 0;
|
||||
html = (html || '')
|
||||
.toString()
|
||||
.replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
|
||||
let cid = crypto.randomBytes(10).toString('hex') + '@localhost';
|
||||
if (!mail.data.attachments) {
|
||||
mail.data.attachments = [];
|
||||
}
|
||||
if (!Array.isArray(mail.data.attachments)) {
|
||||
mail.data.attachments = [].concat(mail.data.attachments || []);
|
||||
}
|
||||
mail.data.attachments.push({
|
||||
path: dataUri,
|
||||
cid,
|
||||
filename: 'image-' + ++cidCounter + '.' + mimeTypes.detectExtension(mimeType)
|
||||
});
|
||||
return prefix + 'cid:' + cid;
|
||||
});
|
||||
mail.data.html = html;
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
return this.meta.set(key, value);
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return this.meta.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Mail;
|
315
backend/apis/nodejs/node_modules/nodemailer/lib/mailer/mail-message.js
generated
vendored
Normal file
315
backend/apis/nodejs/node_modules/nodemailer/lib/mailer/mail-message.js
generated
vendored
Normal file
@ -0,0 +1,315 @@
|
||||
'use strict';
|
||||
|
||||
const shared = require('../shared');
|
||||
const MimeNode = require('../mime-node');
|
||||
const mimeFuncs = require('../mime-funcs');
|
||||
|
||||
class MailMessage {
|
||||
constructor(mailer, data) {
|
||||
this.mailer = mailer;
|
||||
this.data = {};
|
||||
this.message = null;
|
||||
|
||||
data = data || {};
|
||||
let options = mailer.options || {};
|
||||
let defaults = mailer._defaults || {};
|
||||
|
||||
Object.keys(data).forEach(key => {
|
||||
this.data[key] = data[key];
|
||||
});
|
||||
|
||||
this.data.headers = this.data.headers || {};
|
||||
|
||||
// apply defaults
|
||||
Object.keys(defaults).forEach(key => {
|
||||
if (!(key in this.data)) {
|
||||
this.data[key] = defaults[key];
|
||||
} else if (key === 'headers') {
|
||||
// headers is a special case. Allow setting individual default headers
|
||||
Object.keys(defaults.headers).forEach(key => {
|
||||
if (!(key in this.data.headers)) {
|
||||
this.data.headers[key] = defaults.headers[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// force specific keys from transporter options
|
||||
['disableFileAccess', 'disableUrlAccess', 'normalizeHeaderKey'].forEach(key => {
|
||||
if (key in options) {
|
||||
this.data[key] = options[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resolveContent(...args) {
|
||||
return shared.resolveContent(...args);
|
||||
}
|
||||
|
||||
resolveAll(callback) {
|
||||
let keys = [
|
||||
[this.data, 'html'],
|
||||
[this.data, 'text'],
|
||||
[this.data, 'watchHtml'],
|
||||
[this.data, 'amp'],
|
||||
[this.data, 'icalEvent']
|
||||
];
|
||||
|
||||
if (this.data.alternatives && this.data.alternatives.length) {
|
||||
this.data.alternatives.forEach((alternative, i) => {
|
||||
keys.push([this.data.alternatives, i]);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.data.attachments && this.data.attachments.length) {
|
||||
this.data.attachments.forEach((attachment, i) => {
|
||||
if (!attachment.filename) {
|
||||
attachment.filename = (attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1);
|
||||
if (attachment.filename.indexOf('.') < 0) {
|
||||
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
|
||||
}
|
||||
}
|
||||
|
||||
if (!attachment.contentType) {
|
||||
attachment.contentType = mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin');
|
||||
}
|
||||
|
||||
keys.push([this.data.attachments, i]);
|
||||
});
|
||||
}
|
||||
|
||||
let mimeNode = new MimeNode();
|
||||
|
||||
let addressKeys = ['from', 'to', 'cc', 'bcc', 'sender', 'replyTo'];
|
||||
|
||||
addressKeys.forEach(address => {
|
||||
let value;
|
||||
if (this.message) {
|
||||
value = [].concat(mimeNode._parseAddresses(this.message.getHeader(address === 'replyTo' ? 'reply-to' : address)) || []);
|
||||
} else if (this.data[address]) {
|
||||
value = [].concat(mimeNode._parseAddresses(this.data[address]) || []);
|
||||
}
|
||||
if (value && value.length) {
|
||||
this.data[address] = value;
|
||||
} else if (address in this.data) {
|
||||
this.data[address] = null;
|
||||
}
|
||||
});
|
||||
|
||||
let singleKeys = ['from', 'sender'];
|
||||
singleKeys.forEach(address => {
|
||||
if (this.data[address]) {
|
||||
this.data[address] = this.data[address].shift();
|
||||
}
|
||||
});
|
||||
|
||||
let pos = 0;
|
||||
let resolveNext = () => {
|
||||
if (pos >= keys.length) {
|
||||
return callback(null, this.data);
|
||||
}
|
||||
let args = keys[pos++];
|
||||
if (!args[0] || !args[0][args[1]]) {
|
||||
return resolveNext();
|
||||
}
|
||||
shared.resolveContent(...args, (err, value) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let node = {
|
||||
content: value
|
||||
};
|
||||
if (args[0][args[1]] && typeof args[0][args[1]] === 'object' && !Buffer.isBuffer(args[0][args[1]])) {
|
||||
Object.keys(args[0][args[1]]).forEach(key => {
|
||||
if (!(key in node) && !['content', 'path', 'href', 'raw'].includes(key)) {
|
||||
node[key] = args[0][args[1]][key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
args[0][args[1]] = node;
|
||||
resolveNext();
|
||||
});
|
||||
};
|
||||
|
||||
setImmediate(() => resolveNext());
|
||||
}
|
||||
|
||||
normalize(callback) {
|
||||
let envelope = this.data.envelope || this.message.getEnvelope();
|
||||
let messageId = this.message.messageId();
|
||||
|
||||
this.resolveAll((err, data) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
data.envelope = envelope;
|
||||
data.messageId = messageId;
|
||||
|
||||
['html', 'text', 'watchHtml', 'amp'].forEach(key => {
|
||||
if (data[key] && data[key].content) {
|
||||
if (typeof data[key].content === 'string') {
|
||||
data[key] = data[key].content;
|
||||
} else if (Buffer.isBuffer(data[key].content)) {
|
||||
data[key] = data[key].content.toString();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (data.icalEvent && Buffer.isBuffer(data.icalEvent.content)) {
|
||||
data.icalEvent.content = data.icalEvent.content.toString('base64');
|
||||
data.icalEvent.encoding = 'base64';
|
||||
}
|
||||
|
||||
if (data.alternatives && data.alternatives.length) {
|
||||
data.alternatives.forEach(alternative => {
|
||||
if (alternative && alternative.content && Buffer.isBuffer(alternative.content)) {
|
||||
alternative.content = alternative.content.toString('base64');
|
||||
alternative.encoding = 'base64';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (data.attachments && data.attachments.length) {
|
||||
data.attachments.forEach(attachment => {
|
||||
if (attachment && attachment.content && Buffer.isBuffer(attachment.content)) {
|
||||
attachment.content = attachment.content.toString('base64');
|
||||
attachment.encoding = 'base64';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
data.normalizedHeaders = {};
|
||||
Object.keys(data.headers || {}).forEach(key => {
|
||||
let value = [].concat(data.headers[key] || []).shift();
|
||||
value = (value && value.value) || value;
|
||||
if (value) {
|
||||
if (['references', 'in-reply-to', 'message-id', 'content-id'].includes(key)) {
|
||||
value = this.message._encodeHeaderValue(key, value);
|
||||
}
|
||||
data.normalizedHeaders[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (data.list && typeof data.list === 'object') {
|
||||
let listHeaders = this._getListHeaders(data.list);
|
||||
listHeaders.forEach(entry => {
|
||||
data.normalizedHeaders[entry.key] = entry.value.map(val => (val && val.value) || val).join(', ');
|
||||
});
|
||||
}
|
||||
|
||||
if (data.references) {
|
||||
data.normalizedHeaders.references = this.message._encodeHeaderValue('references', data.references);
|
||||
}
|
||||
|
||||
if (data.inReplyTo) {
|
||||
data.normalizedHeaders['in-reply-to'] = this.message._encodeHeaderValue('in-reply-to', data.inReplyTo);
|
||||
}
|
||||
|
||||
return callback(null, data);
|
||||
});
|
||||
}
|
||||
|
||||
setMailerHeader() {
|
||||
if (!this.message || !this.data.xMailer) {
|
||||
return;
|
||||
}
|
||||
this.message.setHeader('X-Mailer', this.data.xMailer);
|
||||
}
|
||||
|
||||
setPriorityHeaders() {
|
||||
if (!this.message || !this.data.priority) {
|
||||
return;
|
||||
}
|
||||
switch ((this.data.priority || '').toString().toLowerCase()) {
|
||||
case 'high':
|
||||
this.message.setHeader('X-Priority', '1 (Highest)');
|
||||
this.message.setHeader('X-MSMail-Priority', 'High');
|
||||
this.message.setHeader('Importance', 'High');
|
||||
break;
|
||||
case 'low':
|
||||
this.message.setHeader('X-Priority', '5 (Lowest)');
|
||||
this.message.setHeader('X-MSMail-Priority', 'Low');
|
||||
this.message.setHeader('Importance', 'Low');
|
||||
break;
|
||||
default:
|
||||
// do not add anything, since all messages are 'Normal' by default
|
||||
}
|
||||
}
|
||||
|
||||
setListHeaders() {
|
||||
if (!this.message || !this.data.list || typeof this.data.list !== 'object') {
|
||||
return;
|
||||
}
|
||||
// add optional List-* headers
|
||||
if (this.data.list && typeof this.data.list === 'object') {
|
||||
this._getListHeaders(this.data.list).forEach(listHeader => {
|
||||
listHeader.value.forEach(value => {
|
||||
this.message.addHeader(listHeader.key, value);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_getListHeaders(listData) {
|
||||
// make sure an url looks like <protocol:url>
|
||||
return Object.keys(listData).map(key => ({
|
||||
key: 'list-' + key.toLowerCase().trim(),
|
||||
value: [].concat(listData[key] || []).map(value => ({
|
||||
prepared: true,
|
||||
foldLines: true,
|
||||
value: []
|
||||
.concat(value || [])
|
||||
.map(value => {
|
||||
if (typeof value === 'string') {
|
||||
value = {
|
||||
url: value
|
||||
};
|
||||
}
|
||||
|
||||
if (value && value.url) {
|
||||
if (key.toLowerCase().trim() === 'id') {
|
||||
// List-ID: "comment" <domain>
|
||||
let comment = value.comment || '';
|
||||
if (mimeFuncs.isPlainText(comment)) {
|
||||
comment = '"' + comment + '"';
|
||||
} else {
|
||||
comment = mimeFuncs.encodeWord(comment);
|
||||
}
|
||||
|
||||
return (value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+\/{,2}/, '');
|
||||
}
|
||||
|
||||
// List-*: <http://domain> (comment)
|
||||
let comment = value.comment || '';
|
||||
if (!mimeFuncs.isPlainText(comment)) {
|
||||
comment = mimeFuncs.encodeWord(comment);
|
||||
}
|
||||
|
||||
return this._formatListUrl(value.url) + (value.comment ? ' (' + comment + ')' : '');
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
.filter(value => value)
|
||||
.join(', ')
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
_formatListUrl(url) {
|
||||
url = url.replace(/[\s<]+|[\s>]+/g, '');
|
||||
if (/^(https?|mailto|ftp):/.test(url)) {
|
||||
return '<' + url + '>';
|
||||
}
|
||||
if (/^[^@]+@[^@]+$/.test(url)) {
|
||||
return '<mailto:' + url + '>';
|
||||
}
|
||||
|
||||
return '<http://' + url + '>';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MailMessage;
|
625
backend/apis/nodejs/node_modules/nodemailer/lib/mime-funcs/index.js
generated
vendored
Normal file
625
backend/apis/nodejs/node_modules/nodemailer/lib/mime-funcs/index.js
generated
vendored
Normal file
@ -0,0 +1,625 @@
|
||||
/* eslint no-control-regex:0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
const base64 = require('../base64');
|
||||
const qp = require('../qp');
|
||||
const mimeTypes = require('./mime-types');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Checks if a value is plaintext string (uses only printable 7bit chars)
|
||||
*
|
||||
* @param {String} value String to be tested
|
||||
* @returns {Boolean} true if it is a plaintext string
|
||||
*/
|
||||
isPlainText(value, 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;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if a multi line string containes lines longer than the selected value.
|
||||
*
|
||||
* Useful when detecting if a mail message needs any processing at all –
|
||||
* if only plaintext characters are used and lines are short, then there is
|
||||
* no need to encode the values in any way. If the value is plaintext but has
|
||||
* longer lines then allowed, then use format=flowed
|
||||
*
|
||||
* @param {Number} lineLength Max line length to check for
|
||||
* @returns {Boolean} Returns true if there is at least one line longer than lineLength chars
|
||||
*/
|
||||
hasLongerLines(str, lineLength) {
|
||||
if (str.length > 128 * 1024) {
|
||||
// do not test strings longer than 128kB
|
||||
return true;
|
||||
}
|
||||
return new RegExp('^.{' + (lineLength + 1) + ',}', 'm').test(str);
|
||||
},
|
||||
|
||||
/**
|
||||
* Encodes a string or an Buffer to an UTF-8 MIME Word (rfc2047)
|
||||
*
|
||||
* @param {String|Buffer} data String to be encoded
|
||||
* @param {String} mimeWordEncoding='Q' Encoding for the mime word, either Q or B
|
||||
* @param {Number} [maxLength=0] If set, split mime words into several chunks if needed
|
||||
* @return {String} Single or several mime words joined together
|
||||
*/
|
||||
encodeWord(data, mimeWordEncoding, maxLength) {
|
||||
mimeWordEncoding = (mimeWordEncoding || 'Q').toString().toUpperCase().trim().charAt(0);
|
||||
maxLength = maxLength || 0;
|
||||
|
||||
let encodedStr;
|
||||
let toCharset = 'UTF-8';
|
||||
|
||||
if (maxLength && maxLength > 7 + toCharset.length) {
|
||||
maxLength -= 7 + toCharset.length;
|
||||
}
|
||||
|
||||
if (mimeWordEncoding === 'Q') {
|
||||
// https://tools.ietf.org/html/rfc2047#section-5 rule (3)
|
||||
encodedStr = qp.encode(data).replace(/[^a-z0-9!*+\-/=]/gi, chr => {
|
||||
let ord = chr.charCodeAt(0).toString(16).toUpperCase();
|
||||
if (chr === ' ') {
|
||||
return '_';
|
||||
} else {
|
||||
return '=' + (ord.length === 1 ? '0' + ord : ord);
|
||||
}
|
||||
});
|
||||
} else if (mimeWordEncoding === 'B') {
|
||||
encodedStr = typeof data === 'string' ? data : base64.encode(data);
|
||||
maxLength = maxLength ? Math.max(3, ((maxLength - (maxLength % 4)) / 4) * 3) : 0;
|
||||
}
|
||||
|
||||
if (maxLength && (mimeWordEncoding !== 'B' ? encodedStr : base64.encode(data)).length > maxLength) {
|
||||
if (mimeWordEncoding === 'Q') {
|
||||
encodedStr = this.splitMimeEncodedString(encodedStr, maxLength).join('?= =?' + toCharset + '?' + mimeWordEncoding + '?');
|
||||
} else {
|
||||
// RFC2047 6.3 (2) states that encoded-word must include an integral number of characters, so no chopping unicode sequences
|
||||
let parts = [];
|
||||
let lpart = '';
|
||||
for (let i = 0, len = encodedStr.length; i < len; i++) {
|
||||
let chr = encodedStr.charAt(i);
|
||||
|
||||
if (/[\ud83c\ud83d\ud83e]/.test(chr) && i < len - 1) {
|
||||
// composite emoji byte, so add the next byte as well
|
||||
chr += encodedStr.charAt(++i);
|
||||
}
|
||||
|
||||
// check if we can add this character to the existing string
|
||||
// without breaking byte length limit
|
||||
if (Buffer.byteLength(lpart + chr) <= maxLength || i === 0) {
|
||||
lpart += chr;
|
||||
} else {
|
||||
// we hit the length limit, so push the existing string and start over
|
||||
parts.push(base64.encode(lpart));
|
||||
lpart = chr;
|
||||
}
|
||||
}
|
||||
if (lpart) {
|
||||
parts.push(base64.encode(lpart));
|
||||
}
|
||||
|
||||
if (parts.length > 1) {
|
||||
encodedStr = parts.join('?= =?' + toCharset + '?' + mimeWordEncoding + '?');
|
||||
} else {
|
||||
encodedStr = parts.join('');
|
||||
}
|
||||
}
|
||||
} else if (mimeWordEncoding === 'B') {
|
||||
encodedStr = base64.encode(data);
|
||||
}
|
||||
|
||||
return '=?' + toCharset + '?' + mimeWordEncoding + '?' + encodedStr + (encodedStr.substr(-2) === '?=' ? '' : '?=');
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds word sequences with non ascii text and converts these to mime words
|
||||
*
|
||||
* @param {String} value String to be encoded
|
||||
* @param {String} mimeWordEncoding='Q' Encoding for the mime word, either Q or B
|
||||
* @param {Number} [maxLength=0] If set, split mime words into several chunks if needed
|
||||
* @param {Boolean} [encodeAll=false] If true and the value needs encoding then encodes entire string, not just the smallest match
|
||||
* @return {String} String with possible mime words
|
||||
*/
|
||||
encodeWords(value, mimeWordEncoding, maxLength, encodeAll) {
|
||||
maxLength = maxLength || 0;
|
||||
|
||||
let encodedValue;
|
||||
|
||||
// find first word with a non-printable ascii or special symbol in it
|
||||
let firstMatch = value.match(/(?:^|\s)([^\s]*["\u0080-\uFFFF])/);
|
||||
if (!firstMatch) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (encodeAll) {
|
||||
// if it is requested to encode everything or the string contains something that resebles encoded word, then encode everything
|
||||
|
||||
return this.encodeWord(value, mimeWordEncoding, maxLength);
|
||||
}
|
||||
|
||||
// find the last word with a non-printable ascii in it
|
||||
let lastMatch = value.match(/(["\u0080-\uFFFF][^\s]*)[^"\u0080-\uFFFF]*$/);
|
||||
if (!lastMatch) {
|
||||
// should not happen
|
||||
return value;
|
||||
}
|
||||
|
||||
let startIndex =
|
||||
firstMatch.index +
|
||||
(
|
||||
firstMatch[0].match(/[^\s]/) || {
|
||||
index: 0
|
||||
}
|
||||
).index;
|
||||
let endIndex = lastMatch.index + (lastMatch[1] || '').length;
|
||||
|
||||
encodedValue =
|
||||
(startIndex ? value.substr(0, startIndex) : '') +
|
||||
this.encodeWord(value.substring(startIndex, endIndex), mimeWordEncoding || 'Q', maxLength) +
|
||||
(endIndex < value.length ? value.substr(endIndex) : '');
|
||||
|
||||
return encodedValue;
|
||||
},
|
||||
|
||||
/**
|
||||
* Joins parsed header value together as 'value; param1=value1; param2=value2'
|
||||
* PS: We are following RFC 822 for the list of special characters that we need to keep in quotes.
|
||||
* Refer: https://www.w3.org/Protocols/rfc1341/4_Content-Type.html
|
||||
* @param {Object} structured Parsed header value
|
||||
* @return {String} joined header value
|
||||
*/
|
||||
buildHeaderValue(structured) {
|
||||
let paramsArray = [];
|
||||
|
||||
Object.keys(structured.params || {}).forEach(param => {
|
||||
// filename might include unicode characters so it is a special case
|
||||
// other values probably do not
|
||||
let value = structured.params[param];
|
||||
if (!this.isPlainText(value, true) || value.length >= 75) {
|
||||
this.buildHeaderParam(param, value, 50).forEach(encodedParam => {
|
||||
if (!/[\s"\\;:/=(),<>@[\]?]|^[-']|'$/.test(encodedParam.value) || encodedParam.key.substr(-1) === '*') {
|
||||
paramsArray.push(encodedParam.key + '=' + encodedParam.value);
|
||||
} else {
|
||||
paramsArray.push(encodedParam.key + '=' + JSON.stringify(encodedParam.value));
|
||||
}
|
||||
});
|
||||
} else if (/[\s'"\\;:/=(),<>@[\]?]|^-/.test(value)) {
|
||||
paramsArray.push(param + '=' + JSON.stringify(value));
|
||||
} else {
|
||||
paramsArray.push(param + '=' + value);
|
||||
}
|
||||
});
|
||||
|
||||
return structured.value + (paramsArray.length ? '; ' + paramsArray.join('; ') : '');
|
||||
},
|
||||
|
||||
/**
|
||||
* Encodes a string or an Buffer to an UTF-8 Parameter Value Continuation encoding (rfc2231)
|
||||
* Useful for splitting long parameter values.
|
||||
*
|
||||
* For example
|
||||
* title="unicode string"
|
||||
* becomes
|
||||
* title*0*=utf-8''unicode
|
||||
* title*1*=%20string
|
||||
*
|
||||
* @param {String|Buffer} data String to be encoded
|
||||
* @param {Number} [maxLength=50] Max length for generated chunks
|
||||
* @param {String} [fromCharset='UTF-8'] Source sharacter set
|
||||
* @return {Array} A list of encoded keys and headers
|
||||
*/
|
||||
buildHeaderParam(key, data, maxLength) {
|
||||
let list = [];
|
||||
let encodedStr = typeof data === 'string' ? data : (data || '').toString();
|
||||
let encodedStrArr;
|
||||
let chr, ord;
|
||||
let line;
|
||||
let startPos = 0;
|
||||
let i, len;
|
||||
|
||||
maxLength = maxLength || 50;
|
||||
|
||||
// process ascii only text
|
||||
if (this.isPlainText(data, true)) {
|
||||
// check if conversion is even needed
|
||||
if (encodedStr.length <= maxLength) {
|
||||
return [
|
||||
{
|
||||
key,
|
||||
value: encodedStr
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
encodedStr = encodedStr.replace(new RegExp('.{' + maxLength + '}', 'g'), str => {
|
||||
list.push({
|
||||
line: str
|
||||
});
|
||||
return '';
|
||||
});
|
||||
|
||||
if (encodedStr) {
|
||||
list.push({
|
||||
line: encodedStr
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (/[\uD800-\uDBFF]/.test(encodedStr)) {
|
||||
// string containts surrogate pairs, so normalize it to an array of bytes
|
||||
encodedStrArr = [];
|
||||
for (i = 0, len = encodedStr.length; i < len; i++) {
|
||||
chr = encodedStr.charAt(i);
|
||||
ord = chr.charCodeAt(0);
|
||||
if (ord >= 0xd800 && ord <= 0xdbff && i < len - 1) {
|
||||
chr += encodedStr.charAt(i + 1);
|
||||
encodedStrArr.push(chr);
|
||||
i++;
|
||||
} else {
|
||||
encodedStrArr.push(chr);
|
||||
}
|
||||
}
|
||||
encodedStr = encodedStrArr;
|
||||
}
|
||||
|
||||
// first line includes the charset and language info and needs to be encoded
|
||||
// even if it does not contain any unicode characters
|
||||
line = 'utf-8\x27\x27';
|
||||
let encoded = true;
|
||||
startPos = 0;
|
||||
|
||||
// process text with unicode or special chars
|
||||
for (i = 0, len = encodedStr.length; i < len; i++) {
|
||||
chr = encodedStr[i];
|
||||
|
||||
if (encoded) {
|
||||
chr = this.safeEncodeURIComponent(chr);
|
||||
} else {
|
||||
// try to urlencode current char
|
||||
chr = chr === ' ' ? chr : this.safeEncodeURIComponent(chr);
|
||||
// By default it is not required to encode a line, the need
|
||||
// only appears when the string contains unicode or special chars
|
||||
// in this case we start processing the line over and encode all chars
|
||||
if (chr !== encodedStr[i]) {
|
||||
// Check if it is even possible to add the encoded char to the line
|
||||
// If not, there is no reason to use this line, just push it to the list
|
||||
// and start a new line with the char that needs encoding
|
||||
if ((this.safeEncodeURIComponent(line) + chr).length >= maxLength) {
|
||||
list.push({
|
||||
line,
|
||||
encoded
|
||||
});
|
||||
line = '';
|
||||
startPos = i - 1;
|
||||
} else {
|
||||
encoded = true;
|
||||
i = startPos;
|
||||
line = '';
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if the line is already too long, push it to the list and start a new one
|
||||
if ((line + chr).length >= maxLength) {
|
||||
list.push({
|
||||
line,
|
||||
encoded
|
||||
});
|
||||
line = chr = encodedStr[i] === ' ' ? ' ' : this.safeEncodeURIComponent(encodedStr[i]);
|
||||
if (chr === encodedStr[i]) {
|
||||
encoded = false;
|
||||
startPos = i - 1;
|
||||
} else {
|
||||
encoded = true;
|
||||
}
|
||||
} else {
|
||||
line += chr;
|
||||
}
|
||||
}
|
||||
|
||||
if (line) {
|
||||
list.push({
|
||||
line,
|
||||
encoded
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return list.map((item, i) => ({
|
||||
// encoded lines: {name}*{part}*
|
||||
// unencoded lines: {name}*{part}
|
||||
// if any line needs to be encoded then the first line (part==0) is always encoded
|
||||
key: key + '*' + i + (item.encoded ? '*' : ''),
|
||||
value: item.line
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Parses a header value with key=value arguments into a structured
|
||||
* object.
|
||||
*
|
||||
* parseHeaderValue('content-type: text/plain; CHARSET='UTF-8'') ->
|
||||
* {
|
||||
* 'value': 'text/plain',
|
||||
* 'params': {
|
||||
* 'charset': 'UTF-8'
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @param {String} str Header value
|
||||
* @return {Object} Header value as a parsed structure
|
||||
*/
|
||||
parseHeaderValue(str) {
|
||||
let response = {
|
||||
value: false,
|
||||
params: {}
|
||||
};
|
||||
let key = false;
|
||||
let value = '';
|
||||
let type = 'value';
|
||||
let quote = false;
|
||||
let escaped = false;
|
||||
let chr;
|
||||
|
||||
for (let i = 0, len = str.length; i < len; i++) {
|
||||
chr = str.charAt(i);
|
||||
if (type === 'key') {
|
||||
if (chr === '=') {
|
||||
key = value.trim().toLowerCase();
|
||||
type = 'value';
|
||||
value = '';
|
||||
continue;
|
||||
}
|
||||
value += chr;
|
||||
} else {
|
||||
if (escaped) {
|
||||
value += chr;
|
||||
} else if (chr === '\\') {
|
||||
escaped = true;
|
||||
continue;
|
||||
} else if (quote && chr === quote) {
|
||||
quote = false;
|
||||
} else if (!quote && chr === '"') {
|
||||
quote = chr;
|
||||
} else if (!quote && chr === ';') {
|
||||
if (key === false) {
|
||||
response.value = value.trim();
|
||||
} else {
|
||||
response.params[key] = value.trim();
|
||||
}
|
||||
type = 'key';
|
||||
value = '';
|
||||
} else {
|
||||
value += chr;
|
||||
}
|
||||
escaped = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'value') {
|
||||
if (key === false) {
|
||||
response.value = value.trim();
|
||||
} else {
|
||||
response.params[key] = value.trim();
|
||||
}
|
||||
} else if (value.trim()) {
|
||||
response.params[value.trim().toLowerCase()] = '';
|
||||
}
|
||||
|
||||
// handle parameter value continuations
|
||||
// https://tools.ietf.org/html/rfc2231#section-3
|
||||
|
||||
// preprocess values
|
||||
Object.keys(response.params).forEach(key => {
|
||||
let actualKey, nr, match, value;
|
||||
if ((match = key.match(/(\*(\d+)|\*(\d+)\*|\*)$/))) {
|
||||
actualKey = key.substr(0, match.index);
|
||||
nr = Number(match[2] || match[3]) || 0;
|
||||
|
||||
if (!response.params[actualKey] || typeof response.params[actualKey] !== 'object') {
|
||||
response.params[actualKey] = {
|
||||
charset: false,
|
||||
values: []
|
||||
};
|
||||
}
|
||||
|
||||
value = response.params[key];
|
||||
|
||||
if (nr === 0 && match[0].substr(-1) === '*' && (match = value.match(/^([^']*)'[^']*'(.*)$/))) {
|
||||
response.params[actualKey].charset = match[1] || 'iso-8859-1';
|
||||
value = match[2];
|
||||
}
|
||||
|
||||
response.params[actualKey].values[nr] = value;
|
||||
|
||||
// remove the old reference
|
||||
delete response.params[key];
|
||||
}
|
||||
});
|
||||
|
||||
// concatenate split rfc2231 strings and convert encoded strings to mime encoded words
|
||||
Object.keys(response.params).forEach(key => {
|
||||
let value;
|
||||
if (response.params[key] && Array.isArray(response.params[key].values)) {
|
||||
value = response.params[key].values.map(val => val || '').join('');
|
||||
|
||||
if (response.params[key].charset) {
|
||||
// convert "%AB" to "=?charset?Q?=AB?="
|
||||
response.params[key] =
|
||||
'=?' +
|
||||
response.params[key].charset +
|
||||
'?Q?' +
|
||||
value
|
||||
// fix invalidly encoded chars
|
||||
.replace(/[=?_\s]/g, s => {
|
||||
let c = s.charCodeAt(0).toString(16);
|
||||
if (s === ' ') {
|
||||
return '_';
|
||||
} else {
|
||||
return '%' + (c.length < 2 ? '0' : '') + c;
|
||||
}
|
||||
})
|
||||
// change from urlencoding to percent encoding
|
||||
.replace(/%/g, '=') +
|
||||
'?=';
|
||||
} else {
|
||||
response.params[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns file extension for a content type string. If no suitable extensions
|
||||
* are found, 'bin' is used as the default extension
|
||||
*
|
||||
* @param {String} mimeType Content type to be checked for
|
||||
* @return {String} File extension
|
||||
*/
|
||||
detectExtension: mimeType => mimeTypes.detectExtension(mimeType),
|
||||
|
||||
/**
|
||||
* Returns content type for a file extension. If no suitable content types
|
||||
* are found, 'application/octet-stream' is used as the default content type
|
||||
*
|
||||
* @param {String} extension Extension to be checked for
|
||||
* @return {String} File extension
|
||||
*/
|
||||
detectMimeType: extension => mimeTypes.detectMimeType(extension),
|
||||
|
||||
/**
|
||||
* Folds long lines, useful for folding header lines (afterSpace=false) and
|
||||
* flowed text (afterSpace=true)
|
||||
*
|
||||
* @param {String} str String to be folded
|
||||
* @param {Number} [lineLength=76] Maximum length of a line
|
||||
* @param {Boolean} afterSpace If true, leave a space in th end of a line
|
||||
* @return {String} String with folded lines
|
||||
*/
|
||||
foldLines(str, lineLength, afterSpace) {
|
||||
str = (str || '').toString();
|
||||
lineLength = lineLength || 76;
|
||||
|
||||
let pos = 0,
|
||||
len = str.length,
|
||||
result = '',
|
||||
line,
|
||||
match;
|
||||
|
||||
while (pos < len) {
|
||||
line = str.substr(pos, lineLength);
|
||||
if (line.length < lineLength) {
|
||||
result += line;
|
||||
break;
|
||||
}
|
||||
if ((match = line.match(/^[^\n\r]*(\r?\n|\r)/))) {
|
||||
line = match[0];
|
||||
result += line;
|
||||
pos += line.length;
|
||||
continue;
|
||||
} else if ((match = line.match(/(\s+)[^\s]*$/)) && match[0].length - (afterSpace ? (match[1] || '').length : 0) < line.length) {
|
||||
line = line.substr(0, line.length - (match[0].length - (afterSpace ? (match[1] || '').length : 0)));
|
||||
} else if ((match = str.substr(pos + line.length).match(/^[^\s]+(\s*)/))) {
|
||||
line = line + match[0].substr(0, match[0].length - (!afterSpace ? (match[1] || '').length : 0));
|
||||
}
|
||||
|
||||
result += line;
|
||||
pos += line.length;
|
||||
if (pos < len) {
|
||||
result += '\r\n';
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Splits a mime encoded string. Needed for dividing mime words into smaller chunks
|
||||
*
|
||||
* @param {String} str Mime encoded string to be split up
|
||||
* @param {Number} maxlen Maximum length of characters for one part (minimum 12)
|
||||
* @return {Array} Split string
|
||||
*/
|
||||
splitMimeEncodedString: (str, maxlen) => {
|
||||
let curLine,
|
||||
match,
|
||||
chr,
|
||||
done,
|
||||
lines = [];
|
||||
|
||||
// require at least 12 symbols to fit possible 4 octet UTF-8 sequences
|
||||
maxlen = Math.max(maxlen || 0, 12);
|
||||
|
||||
while (str.length) {
|
||||
curLine = str.substr(0, maxlen);
|
||||
|
||||
// move incomplete escaped char back to main
|
||||
if ((match = curLine.match(/[=][0-9A-F]?$/i))) {
|
||||
curLine = curLine.substr(0, match.index);
|
||||
}
|
||||
|
||||
done = false;
|
||||
while (!done) {
|
||||
done = true;
|
||||
// check if not middle of a unicode char sequence
|
||||
if ((match = str.substr(curLine.length).match(/^[=]([0-9A-F]{2})/i))) {
|
||||
chr = parseInt(match[1], 16);
|
||||
// invalid sequence, move one char back anc recheck
|
||||
if (chr < 0xc2 && chr > 0x7f) {
|
||||
curLine = curLine.substr(0, curLine.length - 3);
|
||||
done = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (curLine.length) {
|
||||
lines.push(curLine);
|
||||
}
|
||||
str = str.substr(curLine.length);
|
||||
}
|
||||
|
||||
return lines;
|
||||
},
|
||||
|
||||
encodeURICharComponent: chr => {
|
||||
let res = '';
|
||||
let ord = chr.charCodeAt(0).toString(16).toUpperCase();
|
||||
|
||||
if (ord.length % 2) {
|
||||
ord = '0' + ord;
|
||||
}
|
||||
|
||||
if (ord.length > 2) {
|
||||
for (let i = 0, len = ord.length / 2; i < len; i++) {
|
||||
res += '%' + ord.substr(i, 2);
|
||||
}
|
||||
} else {
|
||||
res += '%' + ord;
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
|
||||
safeEncodeURIComponent(str) {
|
||||
str = (str || '').toString();
|
||||
|
||||
try {
|
||||
// might throw if we try to encode invalid sequences, eg. partial emoji
|
||||
str = encodeURIComponent(str);
|
||||
} catch (E) {
|
||||
// should never run
|
||||
return str.replace(/[^\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]+/g, '');
|
||||
}
|
||||
|
||||
// ensure chars that are not handled by encodeURICompent are converted as well
|
||||
return str.replace(/[\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]/g, chr => this.encodeURICharComponent(chr));
|
||||
}
|
||||
};
|
2104
backend/apis/nodejs/node_modules/nodemailer/lib/mime-funcs/mime-types.js
generated
vendored
Normal file
2104
backend/apis/nodejs/node_modules/nodemailer/lib/mime-funcs/mime-types.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1314
backend/apis/nodejs/node_modules/nodemailer/lib/mime-node/index.js
generated
vendored
Normal file
1314
backend/apis/nodejs/node_modules/nodemailer/lib/mime-node/index.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
33
backend/apis/nodejs/node_modules/nodemailer/lib/mime-node/last-newline.js
generated
vendored
Normal file
33
backend/apis/nodejs/node_modules/nodemailer/lib/mime-node/last-newline.js
generated
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
const Transform = require('stream').Transform;
|
||||
|
||||
class LastNewline extends Transform {
|
||||
constructor() {
|
||||
super();
|
||||
this.lastByte = false;
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, done) {
|
||||
if (chunk.length) {
|
||||
this.lastByte = chunk[chunk.length - 1];
|
||||
}
|
||||
|
||||
this.push(chunk);
|
||||
done();
|
||||
}
|
||||
|
||||
_flush(done) {
|
||||
if (this.lastByte === 0x0a) {
|
||||
return done();
|
||||
}
|
||||
if (this.lastByte === 0x0d) {
|
||||
this.push(Buffer.from('\n'));
|
||||
return done();
|
||||
}
|
||||
this.push(Buffer.from('\r\n'));
|
||||
return done();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LastNewline;
|
43
backend/apis/nodejs/node_modules/nodemailer/lib/mime-node/le-unix.js
generated
vendored
Normal file
43
backend/apis/nodejs/node_modules/nodemailer/lib/mime-node/le-unix.js
generated
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
const stream = require('stream');
|
||||
const Transform = stream.Transform;
|
||||
|
||||
/**
|
||||
* Ensures that only <LF> is used for linebreaks
|
||||
*
|
||||
* @param {Object} options Stream options
|
||||
*/
|
||||
class LeWindows extends Transform {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
// init Transform
|
||||
this.options = options || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes dots
|
||||
*/
|
||||
_transform(chunk, encoding, done) {
|
||||
let buf;
|
||||
let lastPos = 0;
|
||||
|
||||
for (let i = 0, len = chunk.length; i < len; i++) {
|
||||
if (chunk[i] === 0x0d) {
|
||||
// \n
|
||||
buf = chunk.slice(lastPos, i);
|
||||
lastPos = i + 1;
|
||||
this.push(buf);
|
||||
}
|
||||
}
|
||||
if (lastPos && lastPos < chunk.length) {
|
||||
buf = chunk.slice(lastPos);
|
||||
this.push(buf);
|
||||
} else if (!lastPos) {
|
||||
this.push(chunk);
|
||||
}
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LeWindows;
|
52
backend/apis/nodejs/node_modules/nodemailer/lib/mime-node/le-windows.js
generated
vendored
Normal file
52
backend/apis/nodejs/node_modules/nodemailer/lib/mime-node/le-windows.js
generated
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
const stream = require('stream');
|
||||
const Transform = stream.Transform;
|
||||
|
||||
/**
|
||||
* Ensures that only <CR><LF> sequences are used for linebreaks
|
||||
*
|
||||
* @param {Object} options Stream options
|
||||
*/
|
||||
class LeWindows extends Transform {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
// init Transform
|
||||
this.options = options || {};
|
||||
this.lastByte = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes dots
|
||||
*/
|
||||
_transform(chunk, encoding, done) {
|
||||
let buf;
|
||||
let lastPos = 0;
|
||||
|
||||
for (let i = 0, len = chunk.length; i < len; i++) {
|
||||
if (chunk[i] === 0x0a) {
|
||||
// \n
|
||||
if ((i && chunk[i - 1] !== 0x0d) || (!i && this.lastByte !== 0x0d)) {
|
||||
if (i > lastPos) {
|
||||
buf = chunk.slice(lastPos, i);
|
||||
this.push(buf);
|
||||
}
|
||||
this.push(Buffer.from('\r\n'));
|
||||
lastPos = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastPos && lastPos < chunk.length) {
|
||||
buf = chunk.slice(lastPos);
|
||||
this.push(buf);
|
||||
} else if (!lastPos) {
|
||||
this.push(chunk);
|
||||
}
|
||||
|
||||
this.lastByte = chunk[chunk.length - 1];
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LeWindows;
|
150
backend/apis/nodejs/node_modules/nodemailer/lib/nodemailer.js
generated
vendored
Normal file
150
backend/apis/nodejs/node_modules/nodemailer/lib/nodemailer.js
generated
vendored
Normal file
@ -0,0 +1,150 @@
|
||||
'use strict';
|
||||
|
||||
const Mailer = require('./mailer');
|
||||
const shared = require('./shared');
|
||||
const SMTPPool = require('./smtp-pool');
|
||||
const SMTPTransport = require('./smtp-transport');
|
||||
const SendmailTransport = require('./sendmail-transport');
|
||||
const StreamTransport = require('./stream-transport');
|
||||
const JSONTransport = require('./json-transport');
|
||||
const SESTransport = require('./ses-transport');
|
||||
const nmfetch = require('./fetch');
|
||||
const packageData = require('../package.json');
|
||||
|
||||
const ETHEREAL_API = (process.env.ETHEREAL_API || 'https://api.nodemailer.com').replace(/\/+$/, '');
|
||||
const ETHEREAL_WEB = (process.env.ETHEREAL_WEB || 'https://ethereal.email').replace(/\/+$/, '');
|
||||
const ETHEREAL_API_KEY = (process.env.ETHEREAL_API_KEY || '').replace(/\s*/g, '') || null;
|
||||
const ETHEREAL_CACHE = ['true', 'yes', 'y', '1'].includes((process.env.ETHEREAL_CACHE || 'yes').toString().trim().toLowerCase());
|
||||
|
||||
let testAccount = false;
|
||||
|
||||
module.exports.createTransport = function (transporter, defaults) {
|
||||
let urlConfig;
|
||||
let options;
|
||||
let mailer;
|
||||
|
||||
if (
|
||||
// provided transporter is a configuration object, not transporter plugin
|
||||
(typeof transporter === 'object' && typeof transporter.send !== 'function') ||
|
||||
// provided transporter looks like a connection url
|
||||
(typeof transporter === 'string' && /^(smtps?|direct):/i.test(transporter))
|
||||
) {
|
||||
if ((urlConfig = typeof transporter === 'string' ? transporter : transporter.url)) {
|
||||
// parse a configuration URL into configuration options
|
||||
options = shared.parseConnectionUrl(urlConfig);
|
||||
} else {
|
||||
options = transporter;
|
||||
}
|
||||
|
||||
if (options.pool) {
|
||||
transporter = new SMTPPool(options);
|
||||
} else if (options.sendmail) {
|
||||
transporter = new SendmailTransport(options);
|
||||
} else if (options.streamTransport) {
|
||||
transporter = new StreamTransport(options);
|
||||
} else if (options.jsonTransport) {
|
||||
transporter = new JSONTransport(options);
|
||||
} else if (options.SES) {
|
||||
transporter = new SESTransport(options);
|
||||
} else {
|
||||
transporter = new SMTPTransport(options);
|
||||
}
|
||||
}
|
||||
|
||||
mailer = new Mailer(transporter, options, defaults);
|
||||
|
||||
return mailer;
|
||||
};
|
||||
|
||||
module.exports.createTestAccount = function (apiUrl, callback) {
|
||||
let promise;
|
||||
|
||||
if (!callback && typeof apiUrl === 'function') {
|
||||
callback = apiUrl;
|
||||
apiUrl = false;
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
promise = new Promise((resolve, reject) => {
|
||||
callback = shared.callbackPromise(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
if (ETHEREAL_CACHE && testAccount) {
|
||||
setImmediate(() => callback(null, testAccount));
|
||||
return promise;
|
||||
}
|
||||
|
||||
apiUrl = apiUrl || ETHEREAL_API;
|
||||
|
||||
let chunks = [];
|
||||
let chunklen = 0;
|
||||
|
||||
let requestHeaders = {};
|
||||
let requestBody = {
|
||||
requestor: packageData.name,
|
||||
version: packageData.version
|
||||
};
|
||||
|
||||
if (ETHEREAL_API_KEY) {
|
||||
requestHeaders.Authorization = 'Bearer ' + ETHEREAL_API_KEY;
|
||||
}
|
||||
|
||||
let req = nmfetch(apiUrl + '/user', {
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
headers: requestHeaders,
|
||||
body: Buffer.from(JSON.stringify(requestBody))
|
||||
});
|
||||
|
||||
req.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = req.read()) !== null) {
|
||||
chunks.push(chunk);
|
||||
chunklen += chunk.length;
|
||||
}
|
||||
});
|
||||
|
||||
req.once('error', err => callback(err));
|
||||
|
||||
req.once('end', () => {
|
||||
let res = Buffer.concat(chunks, chunklen);
|
||||
let data;
|
||||
let err;
|
||||
try {
|
||||
data = JSON.parse(res.toString());
|
||||
} catch (E) {
|
||||
err = E;
|
||||
}
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (data.status !== 'success' || data.error) {
|
||||
return callback(new Error(data.error || 'Request failed'));
|
||||
}
|
||||
delete data.status;
|
||||
testAccount = data;
|
||||
callback(null, testAccount);
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
module.exports.getTestMessageUrl = function (info) {
|
||||
if (!info || !info.response) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let infoProps = new Map();
|
||||
info.response.replace(/\[([^\]]+)\]$/, (m, props) => {
|
||||
props.replace(/\b([A-Z0-9]+)=([^\s]+)/g, (m, key, value) => {
|
||||
infoProps.set(key, value);
|
||||
});
|
||||
});
|
||||
|
||||
if (infoProps.has('STATUS') && infoProps.has('MSGID')) {
|
||||
return (testAccount.web || ETHEREAL_WEB) + '/message/' + infoProps.get('MSGID');
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
460
backend/apis/nodejs/node_modules/nodemailer/lib/punycode/index.js
generated
vendored
Normal file
460
backend/apis/nodejs/node_modules/nodemailer/lib/punycode/index.js
generated
vendored
Normal file
@ -0,0 +1,460 @@
|
||||
/*
|
||||
|
||||
Copied from https://github.com/mathiasbynens/punycode.js/blob/ef3505c8abb5143a00d53ce59077c9f7f4b2ac47/punycode.js
|
||||
|
||||
Copyright Mathias Bynens <https://mathiasbynens.be/>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
*/
|
||||
/* eslint callback-return: 0, no-bitwise: 0, eqeqeq: 0, prefer-arrow-callback: 0, object-shorthand: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
/** Highest positive signed 32-bit float value */
|
||||
const maxInt = 2147483647; // aka. 0x7FFFFFFF or 2^31-1
|
||||
|
||||
/** Bootstring parameters */
|
||||
const base = 36;
|
||||
const tMin = 1;
|
||||
const tMax = 26;
|
||||
const skew = 38;
|
||||
const damp = 700;
|
||||
const initialBias = 72;
|
||||
const initialN = 128; // 0x80
|
||||
const delimiter = '-'; // '\x2D'
|
||||
|
||||
/** Regular expressions */
|
||||
const regexPunycode = /^xn--/;
|
||||
const regexNonASCII = /[^\0-\x7F]/; // Note: U+007F DEL is excluded too.
|
||||
const regexSeparators = /[\x2E\u3002\uFF0E\uFF61]/g; // RFC 3490 separators
|
||||
|
||||
/** Error messages */
|
||||
const errors = {
|
||||
overflow: 'Overflow: input needs wider integers to process',
|
||||
'not-basic': 'Illegal input >= 0x80 (not a basic code point)',
|
||||
'invalid-input': 'Invalid input'
|
||||
};
|
||||
|
||||
/** Convenience shortcuts */
|
||||
const baseMinusTMin = base - tMin;
|
||||
const floor = Math.floor;
|
||||
const stringFromCharCode = String.fromCharCode;
|
||||
|
||||
/*--------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* A generic error utility function.
|
||||
* @private
|
||||
* @param {String} type The error type.
|
||||
* @returns {Error} Throws a `RangeError` with the applicable error message.
|
||||
*/
|
||||
function error(type) {
|
||||
throw new RangeError(errors[type]);
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic `Array#map` utility function.
|
||||
* @private
|
||||
* @param {Array} array The array to iterate over.
|
||||
* @param {Function} callback The function that gets called for every array
|
||||
* item.
|
||||
* @returns {Array} A new array of values returned by the callback function.
|
||||
*/
|
||||
function map(array, callback) {
|
||||
const result = [];
|
||||
let length = array.length;
|
||||
while (length--) {
|
||||
result[length] = callback(array[length]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple `Array#map`-like wrapper to work with domain name strings or email
|
||||
* addresses.
|
||||
* @private
|
||||
* @param {String} domain The domain name or email address.
|
||||
* @param {Function} callback The function that gets called for every
|
||||
* character.
|
||||
* @returns {String} A new string of characters returned by the callback
|
||||
* function.
|
||||
*/
|
||||
function mapDomain(domain, callback) {
|
||||
const parts = domain.split('@');
|
||||
let result = '';
|
||||
if (parts.length > 1) {
|
||||
// In email addresses, only the domain name should be punycoded. Leave
|
||||
// the local part (i.e. everything up to `@`) intact.
|
||||
result = parts[0] + '@';
|
||||
domain = parts[1];
|
||||
}
|
||||
// Avoid `split(regex)` for IE8 compatibility. See #17.
|
||||
domain = domain.replace(regexSeparators, '\x2E');
|
||||
const labels = domain.split('.');
|
||||
const encoded = map(labels, callback).join('.');
|
||||
return result + encoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an array containing the numeric code points of each Unicode
|
||||
* character in the string. While JavaScript uses UCS-2 internally,
|
||||
* this function will convert a pair of surrogate halves (each of which
|
||||
* UCS-2 exposes as separate characters) into a single code point,
|
||||
* matching UTF-16.
|
||||
* @see `punycode.ucs2.encode`
|
||||
* @see <https://mathiasbynens.be/notes/javascript-encoding>
|
||||
* @memberOf punycode.ucs2
|
||||
* @name decode
|
||||
* @param {String} string The Unicode input string (UCS-2).
|
||||
* @returns {Array} The new array of code points.
|
||||
*/
|
||||
function ucs2decode(string) {
|
||||
const output = [];
|
||||
let counter = 0;
|
||||
const length = string.length;
|
||||
while (counter < length) {
|
||||
const value = string.charCodeAt(counter++);
|
||||
if (value >= 0xd800 && value <= 0xdbff && counter < length) {
|
||||
// It's a high surrogate, and there is a next character.
|
||||
const extra = string.charCodeAt(counter++);
|
||||
if ((extra & 0xfc00) == 0xdc00) {
|
||||
// Low surrogate.
|
||||
output.push(((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000);
|
||||
} else {
|
||||
// It's an unmatched surrogate; only append this code unit, in case the
|
||||
// next code unit is the high surrogate of a surrogate pair.
|
||||
output.push(value);
|
||||
counter--;
|
||||
}
|
||||
} else {
|
||||
output.push(value);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a string based on an array of numeric code points.
|
||||
* @see `punycode.ucs2.decode`
|
||||
* @memberOf punycode.ucs2
|
||||
* @name encode
|
||||
* @param {Array} codePoints The array of numeric code points.
|
||||
* @returns {String} The new Unicode string (UCS-2).
|
||||
*/
|
||||
const ucs2encode = codePoints => String.fromCodePoint(...codePoints);
|
||||
|
||||
/**
|
||||
* Converts a basic code point into a digit/integer.
|
||||
* @see `digitToBasic()`
|
||||
* @private
|
||||
* @param {Number} codePoint The basic numeric code point value.
|
||||
* @returns {Number} The numeric value of a basic code point (for use in
|
||||
* representing integers) in the range `0` to `base - 1`, or `base` if
|
||||
* the code point does not represent a value.
|
||||
*/
|
||||
const basicToDigit = function (codePoint) {
|
||||
if (codePoint >= 0x30 && codePoint < 0x3a) {
|
||||
return 26 + (codePoint - 0x30);
|
||||
}
|
||||
if (codePoint >= 0x41 && codePoint < 0x5b) {
|
||||
return codePoint - 0x41;
|
||||
}
|
||||
if (codePoint >= 0x61 && codePoint < 0x7b) {
|
||||
return codePoint - 0x61;
|
||||
}
|
||||
return base;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a digit/integer into a basic code point.
|
||||
* @see `basicToDigit()`
|
||||
* @private
|
||||
* @param {Number} digit The numeric value of a basic code point.
|
||||
* @returns {Number} The basic code point whose value (when used for
|
||||
* representing integers) is `digit`, which needs to be in the range
|
||||
* `0` to `base - 1`. If `flag` is non-zero, the uppercase form is
|
||||
* used; else, the lowercase form is used. The behavior is undefined
|
||||
* if `flag` is non-zero and `digit` has no uppercase form.
|
||||
*/
|
||||
const digitToBasic = function (digit, flag) {
|
||||
// 0..25 map to ASCII a..z or A..Z
|
||||
// 26..35 map to ASCII 0..9
|
||||
return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5);
|
||||
};
|
||||
|
||||
/**
|
||||
* Bias adaptation function as per section 3.4 of RFC 3492.
|
||||
* https://tools.ietf.org/html/rfc3492#section-3.4
|
||||
* @private
|
||||
*/
|
||||
const adapt = function (delta, numPoints, firstTime) {
|
||||
let k = 0;
|
||||
delta = firstTime ? floor(delta / damp) : delta >> 1;
|
||||
delta += floor(delta / numPoints);
|
||||
for (; /* no initialization */ delta > (baseMinusTMin * tMax) >> 1; k += base) {
|
||||
delta = floor(delta / baseMinusTMin);
|
||||
}
|
||||
return floor(k + ((baseMinusTMin + 1) * delta) / (delta + skew));
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a Punycode string of ASCII-only symbols to a string of Unicode
|
||||
* symbols.
|
||||
* @memberOf punycode
|
||||
* @param {String} input The Punycode string of ASCII-only symbols.
|
||||
* @returns {String} The resulting string of Unicode symbols.
|
||||
*/
|
||||
const decode = function (input) {
|
||||
// Don't use UCS-2.
|
||||
const output = [];
|
||||
const inputLength = input.length;
|
||||
let i = 0;
|
||||
let n = initialN;
|
||||
let bias = initialBias;
|
||||
|
||||
// Handle the basic code points: let `basic` be the number of input code
|
||||
// points before the last delimiter, or `0` if there is none, then copy
|
||||
// the first basic code points to the output.
|
||||
|
||||
let basic = input.lastIndexOf(delimiter);
|
||||
if (basic < 0) {
|
||||
basic = 0;
|
||||
}
|
||||
|
||||
for (let j = 0; j < basic; ++j) {
|
||||
// if it's not a basic code point
|
||||
if (input.charCodeAt(j) >= 0x80) {
|
||||
error('not-basic');
|
||||
}
|
||||
output.push(input.charCodeAt(j));
|
||||
}
|
||||
|
||||
// Main decoding loop: start just after the last delimiter if any basic code
|
||||
// points were copied; start at the beginning otherwise.
|
||||
|
||||
for (let index = basic > 0 ? basic + 1 : 0; index < inputLength /* no final expression */; ) {
|
||||
// `index` is the index of the next character to be consumed.
|
||||
// Decode a generalized variable-length integer into `delta`,
|
||||
// which gets added to `i`. The overflow checking is easier
|
||||
// if we increase `i` as we go, then subtract off its starting
|
||||
// value at the end to obtain `delta`.
|
||||
const oldi = i;
|
||||
for (let w = 1, k = base /* no condition */; ; k += base) {
|
||||
if (index >= inputLength) {
|
||||
error('invalid-input');
|
||||
}
|
||||
|
||||
const digit = basicToDigit(input.charCodeAt(index++));
|
||||
|
||||
if (digit >= base) {
|
||||
error('invalid-input');
|
||||
}
|
||||
if (digit > floor((maxInt - i) / w)) {
|
||||
error('overflow');
|
||||
}
|
||||
|
||||
i += digit * w;
|
||||
const t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias;
|
||||
|
||||
if (digit < t) {
|
||||
break;
|
||||
}
|
||||
|
||||
const baseMinusT = base - t;
|
||||
if (w > floor(maxInt / baseMinusT)) {
|
||||
error('overflow');
|
||||
}
|
||||
|
||||
w *= baseMinusT;
|
||||
}
|
||||
|
||||
const out = output.length + 1;
|
||||
bias = adapt(i - oldi, out, oldi == 0);
|
||||
|
||||
// `i` was supposed to wrap around from `out` to `0`,
|
||||
// incrementing `n` each time, so we'll fix that now:
|
||||
if (floor(i / out) > maxInt - n) {
|
||||
error('overflow');
|
||||
}
|
||||
|
||||
n += floor(i / out);
|
||||
i %= out;
|
||||
|
||||
// Insert `n` at position `i` of the output.
|
||||
output.splice(i++, 0, n);
|
||||
}
|
||||
|
||||
return String.fromCodePoint(...output);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a string of Unicode symbols (e.g. a domain name label) to a
|
||||
* Punycode string of ASCII-only symbols.
|
||||
* @memberOf punycode
|
||||
* @param {String} input The string of Unicode symbols.
|
||||
* @returns {String} The resulting Punycode string of ASCII-only symbols.
|
||||
*/
|
||||
const encode = function (input) {
|
||||
const output = [];
|
||||
|
||||
// Convert the input in UCS-2 to an array of Unicode code points.
|
||||
input = ucs2decode(input);
|
||||
|
||||
// Cache the length.
|
||||
const inputLength = input.length;
|
||||
|
||||
// Initialize the state.
|
||||
let n = initialN;
|
||||
let delta = 0;
|
||||
let bias = initialBias;
|
||||
|
||||
// Handle the basic code points.
|
||||
for (const currentValue of input) {
|
||||
if (currentValue < 0x80) {
|
||||
output.push(stringFromCharCode(currentValue));
|
||||
}
|
||||
}
|
||||
|
||||
const basicLength = output.length;
|
||||
let handledCPCount = basicLength;
|
||||
|
||||
// `handledCPCount` is the number of code points that have been handled;
|
||||
// `basicLength` is the number of basic code points.
|
||||
|
||||
// Finish the basic string with a delimiter unless it's empty.
|
||||
if (basicLength) {
|
||||
output.push(delimiter);
|
||||
}
|
||||
|
||||
// Main encoding loop:
|
||||
while (handledCPCount < inputLength) {
|
||||
// All non-basic code points < n have been handled already. Find the next
|
||||
// larger one:
|
||||
let m = maxInt;
|
||||
for (const currentValue of input) {
|
||||
if (currentValue >= n && currentValue < m) {
|
||||
m = currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Increase `delta` enough to advance the decoder's <n,i> state to <m,0>,
|
||||
// but guard against overflow.
|
||||
const handledCPCountPlusOne = handledCPCount + 1;
|
||||
if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) {
|
||||
error('overflow');
|
||||
}
|
||||
|
||||
delta += (m - n) * handledCPCountPlusOne;
|
||||
n = m;
|
||||
|
||||
for (const currentValue of input) {
|
||||
if (currentValue < n && ++delta > maxInt) {
|
||||
error('overflow');
|
||||
}
|
||||
if (currentValue === n) {
|
||||
// Represent delta as a generalized variable-length integer.
|
||||
let q = delta;
|
||||
for (let k = base /* no condition */; ; k += base) {
|
||||
const t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias;
|
||||
if (q < t) {
|
||||
break;
|
||||
}
|
||||
const qMinusT = q - t;
|
||||
const baseMinusT = base - t;
|
||||
output.push(stringFromCharCode(digitToBasic(t + (qMinusT % baseMinusT), 0)));
|
||||
q = floor(qMinusT / baseMinusT);
|
||||
}
|
||||
|
||||
output.push(stringFromCharCode(digitToBasic(q, 0)));
|
||||
bias = adapt(delta, handledCPCountPlusOne, handledCPCount === basicLength);
|
||||
delta = 0;
|
||||
++handledCPCount;
|
||||
}
|
||||
}
|
||||
|
||||
++delta;
|
||||
++n;
|
||||
}
|
||||
return output.join('');
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a Punycode string representing a domain name or an email address
|
||||
* to Unicode. Only the Punycoded parts of the input will be converted, i.e.
|
||||
* it doesn't matter if you call it on a string that has already been
|
||||
* converted to Unicode.
|
||||
* @memberOf punycode
|
||||
* @param {String} input The Punycoded domain name or email address to
|
||||
* convert to Unicode.
|
||||
* @returns {String} The Unicode representation of the given Punycode
|
||||
* string.
|
||||
*/
|
||||
const toUnicode = function (input) {
|
||||
return mapDomain(input, function (string) {
|
||||
return regexPunycode.test(string) ? decode(string.slice(4).toLowerCase()) : string;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a Unicode string representing a domain name or an email address to
|
||||
* Punycode. Only the non-ASCII parts of the domain name will be converted,
|
||||
* i.e. it doesn't matter if you call it with a domain that's already in
|
||||
* ASCII.
|
||||
* @memberOf punycode
|
||||
* @param {String} input The domain name or email address to convert, as a
|
||||
* Unicode string.
|
||||
* @returns {String} The Punycode representation of the given domain name or
|
||||
* email address.
|
||||
*/
|
||||
const toASCII = function (input) {
|
||||
return mapDomain(input, function (string) {
|
||||
return regexNonASCII.test(string) ? 'xn--' + encode(string) : string;
|
||||
});
|
||||
};
|
||||
|
||||
/*--------------------------------------------------------------------------*/
|
||||
|
||||
/** Define the public API */
|
||||
const punycode = {
|
||||
/**
|
||||
* A string representing the current Punycode.js version number.
|
||||
* @memberOf punycode
|
||||
* @type String
|
||||
*/
|
||||
version: '2.3.1',
|
||||
/**
|
||||
* An object of methods to convert from JavaScript's internal character
|
||||
* representation (UCS-2) to Unicode code points, and back.
|
||||
* @see <https://mathiasbynens.be/notes/javascript-encoding>
|
||||
* @memberOf punycode
|
||||
* @type Object
|
||||
*/
|
||||
ucs2: {
|
||||
decode: ucs2decode,
|
||||
encode: ucs2encode
|
||||
},
|
||||
decode: decode,
|
||||
encode: encode,
|
||||
toASCII: toASCII,
|
||||
toUnicode: toUnicode
|
||||
};
|
||||
|
||||
module.exports = punycode;
|
219
backend/apis/nodejs/node_modules/nodemailer/lib/qp/index.js
generated
vendored
Normal file
219
backend/apis/nodejs/node_modules/nodemailer/lib/qp/index.js
generated
vendored
Normal file
@ -0,0 +1,219 @@
|
||||
'use strict';
|
||||
|
||||
const Transform = require('stream').Transform;
|
||||
|
||||
/**
|
||||
* Encodes a Buffer into a Quoted-Printable encoded string
|
||||
*
|
||||
* @param {Buffer} buffer Buffer to convert
|
||||
* @returns {String} Quoted-Printable encoded string
|
||||
*/
|
||||
function encode(buffer) {
|
||||
if (typeof buffer === 'string') {
|
||||
buffer = Buffer.from(buffer, 'utf-8');
|
||||
}
|
||||
|
||||
// usable characters that do not need encoding
|
||||
let ranges = [
|
||||
// https://tools.ietf.org/html/rfc2045#section-6.7
|
||||
[0x09], // <TAB>
|
||||
[0x0a], // <LF>
|
||||
[0x0d], // <CR>
|
||||
[0x20, 0x3c], // <SP>!"#$%&'()*+,-./0123456789:;
|
||||
[0x3e, 0x7e] // >?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}
|
||||
];
|
||||
let result = '';
|
||||
let ord;
|
||||
|
||||
for (let i = 0, len = buffer.length; i < len; i++) {
|
||||
ord = buffer[i];
|
||||
// if the char is in allowed range, then keep as is, unless it is a WS in the end of a line
|
||||
if (checkRanges(ord, ranges) && !((ord === 0x20 || ord === 0x09) && (i === len - 1 || buffer[i + 1] === 0x0a || buffer[i + 1] === 0x0d))) {
|
||||
result += String.fromCharCode(ord);
|
||||
continue;
|
||||
}
|
||||
result += '=' + (ord < 0x10 ? '0' : '') + ord.toString(16).toUpperCase();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds soft line breaks to a Quoted-Printable string
|
||||
*
|
||||
* @param {String} str Quoted-Printable encoded string that might need line wrapping
|
||||
* @param {Number} [lineLength=76] Maximum allowed length for a line
|
||||
* @returns {String} Soft-wrapped Quoted-Printable encoded string
|
||||
*/
|
||||
function wrap(str, lineLength) {
|
||||
str = (str || '').toString();
|
||||
lineLength = lineLength || 76;
|
||||
|
||||
if (str.length <= lineLength) {
|
||||
return str;
|
||||
}
|
||||
|
||||
let pos = 0;
|
||||
let len = str.length;
|
||||
let match, code, line;
|
||||
let lineMargin = Math.floor(lineLength / 3);
|
||||
let result = '';
|
||||
|
||||
// insert soft linebreaks where needed
|
||||
while (pos < len) {
|
||||
line = str.substr(pos, lineLength);
|
||||
if ((match = line.match(/\r\n/))) {
|
||||
line = line.substr(0, match.index + match[0].length);
|
||||
result += line;
|
||||
pos += line.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.substr(-1) === '\n') {
|
||||
// nothing to change here
|
||||
result += line;
|
||||
pos += line.length;
|
||||
continue;
|
||||
} else if ((match = line.substr(-lineMargin).match(/\n.*?$/))) {
|
||||
// truncate to nearest line break
|
||||
line = line.substr(0, line.length - (match[0].length - 1));
|
||||
result += line;
|
||||
pos += line.length;
|
||||
continue;
|
||||
} else if (line.length > lineLength - lineMargin && (match = line.substr(-lineMargin).match(/[ \t.,!?][^ \t.,!?]*$/))) {
|
||||
// truncate to nearest space
|
||||
line = line.substr(0, line.length - (match[0].length - 1));
|
||||
} else if (line.match(/[=][\da-f]{0,2}$/i)) {
|
||||
// push incomplete encoding sequences to the next line
|
||||
if ((match = line.match(/[=][\da-f]{0,1}$/i))) {
|
||||
line = line.substr(0, line.length - match[0].length);
|
||||
}
|
||||
|
||||
// ensure that utf-8 sequences are not split
|
||||
while (line.length > 3 && line.length < len - pos && !line.match(/^(?:=[\da-f]{2}){1,4}$/i) && (match = line.match(/[=][\da-f]{2}$/gi))) {
|
||||
code = parseInt(match[0].substr(1, 2), 16);
|
||||
if (code < 128) {
|
||||
break;
|
||||
}
|
||||
|
||||
line = line.substr(0, line.length - 3);
|
||||
|
||||
if (code >= 0xc0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pos + line.length < len && line.substr(-1) !== '\n') {
|
||||
if (line.length === lineLength && line.match(/[=][\da-f]{2}$/i)) {
|
||||
line = line.substr(0, line.length - 3);
|
||||
} else if (line.length === lineLength) {
|
||||
line = line.substr(0, line.length - 1);
|
||||
}
|
||||
pos += line.length;
|
||||
line += '=\r\n';
|
||||
} else {
|
||||
pos += line.length;
|
||||
}
|
||||
|
||||
result += line;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to check if a number is inside provided ranges
|
||||
*
|
||||
* @param {Number} nr Number to check for
|
||||
* @param {Array} ranges An Array of allowed values
|
||||
* @returns {Boolean} True if the value was found inside allowed ranges, false otherwise
|
||||
*/
|
||||
function checkRanges(nr, ranges) {
|
||||
for (let i = ranges.length - 1; i >= 0; i--) {
|
||||
if (!ranges[i].length) {
|
||||
continue;
|
||||
}
|
||||
if (ranges[i].length === 1 && nr === ranges[i][0]) {
|
||||
return true;
|
||||
}
|
||||
if (ranges[i].length === 2 && nr >= ranges[i][0] && nr <= ranges[i][1]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a transform stream for encoding data to Quoted-Printable encoding
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} options Stream options
|
||||
* @param {Number} [options.lineLength=76] Maximum length for lines, set to false to disable wrapping
|
||||
*/
|
||||
class Encoder extends Transform {
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
// init Transform
|
||||
this.options = options || {};
|
||||
|
||||
if (this.options.lineLength !== false) {
|
||||
this.options.lineLength = this.options.lineLength || 76;
|
||||
}
|
||||
|
||||
this._curLine = '';
|
||||
|
||||
this.inputBytes = 0;
|
||||
this.outputBytes = 0;
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, done) {
|
||||
let qp;
|
||||
|
||||
if (encoding !== 'buffer') {
|
||||
chunk = Buffer.from(chunk, encoding);
|
||||
}
|
||||
|
||||
if (!chunk || !chunk.length) {
|
||||
return done();
|
||||
}
|
||||
|
||||
this.inputBytes += chunk.length;
|
||||
|
||||
if (this.options.lineLength) {
|
||||
qp = this._curLine + encode(chunk);
|
||||
qp = wrap(qp, this.options.lineLength);
|
||||
qp = qp.replace(/(^|\n)([^\n]*)$/, (match, lineBreak, lastLine) => {
|
||||
this._curLine = lastLine;
|
||||
return lineBreak;
|
||||
});
|
||||
|
||||
if (qp) {
|
||||
this.outputBytes += qp.length;
|
||||
this.push(qp);
|
||||
}
|
||||
} else {
|
||||
qp = encode(chunk);
|
||||
this.outputBytes += qp.length;
|
||||
this.push(qp, 'ascii');
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
_flush(done) {
|
||||
if (this._curLine) {
|
||||
this.outputBytes += this._curLine.length;
|
||||
this.push(this._curLine, 'ascii');
|
||||
}
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
// expose to the world
|
||||
module.exports = {
|
||||
encode,
|
||||
wrap,
|
||||
Encoder
|
||||
};
|
210
backend/apis/nodejs/node_modules/nodemailer/lib/sendmail-transport/index.js
generated
vendored
Normal file
210
backend/apis/nodejs/node_modules/nodemailer/lib/sendmail-transport/index.js
generated
vendored
Normal file
@ -0,0 +1,210 @@
|
||||
'use strict';
|
||||
|
||||
const spawn = require('child_process').spawn;
|
||||
const packageData = require('../../package.json');
|
||||
const shared = require('../shared');
|
||||
|
||||
/**
|
||||
* Generates a Transport object for Sendmail
|
||||
*
|
||||
* Possible options can be the following:
|
||||
*
|
||||
* * **path** optional path to sendmail binary
|
||||
* * **newline** either 'windows' or 'unix'
|
||||
* * **args** an array of arguments for the sendmail binary
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} optional config parameter for Sendmail
|
||||
*/
|
||||
class SendmailTransport {
|
||||
constructor(options) {
|
||||
options = options || {};
|
||||
|
||||
// use a reference to spawn for mocking purposes
|
||||
this._spawn = spawn;
|
||||
|
||||
this.options = options || {};
|
||||
|
||||
this.name = 'Sendmail';
|
||||
this.version = packageData.version;
|
||||
|
||||
this.path = 'sendmail';
|
||||
this.args = false;
|
||||
this.winbreak = false;
|
||||
|
||||
this.logger = shared.getLogger(this.options, {
|
||||
component: this.options.component || 'sendmail'
|
||||
});
|
||||
|
||||
if (options) {
|
||||
if (typeof options === 'string') {
|
||||
this.path = options;
|
||||
} else if (typeof options === 'object') {
|
||||
if (options.path) {
|
||||
this.path = options.path;
|
||||
}
|
||||
if (Array.isArray(options.args)) {
|
||||
this.args = options.args;
|
||||
}
|
||||
this.winbreak = ['win', 'windows', 'dos', '\r\n'].includes((options.newline || '').toString().toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Compiles a mailcomposer message and forwards it to handler that sends it.</p>
|
||||
*
|
||||
* @param {Object} emailMessage MailComposer object
|
||||
* @param {Function} callback Callback function to run when the sending is completed
|
||||
*/
|
||||
send(mail, done) {
|
||||
// Sendmail strips this header line by itself
|
||||
mail.message.keepBcc = true;
|
||||
|
||||
let envelope = mail.data.envelope || mail.message.getEnvelope();
|
||||
let messageId = mail.message.messageId();
|
||||
let args;
|
||||
let sendmail;
|
||||
let returned;
|
||||
|
||||
const hasInvalidAddresses = []
|
||||
.concat(envelope.from || [])
|
||||
.concat(envelope.to || [])
|
||||
.some(addr => /^-/.test(addr));
|
||||
if (hasInvalidAddresses) {
|
||||
return done(new Error('Can not send mail. Invalid envelope addresses.'));
|
||||
}
|
||||
|
||||
if (this.args) {
|
||||
// force -i to keep single dots
|
||||
args = ['-i'].concat(this.args).concat(envelope.to);
|
||||
} else {
|
||||
args = ['-i'].concat(envelope.from ? ['-f', envelope.from] : []).concat(envelope.to);
|
||||
}
|
||||
|
||||
let callback = err => {
|
||||
if (returned) {
|
||||
// ignore any additional responses, already done
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
if (typeof done === 'function') {
|
||||
if (err) {
|
||||
return done(err);
|
||||
} else {
|
||||
return done(null, {
|
||||
envelope: mail.data.envelope || mail.message.getEnvelope(),
|
||||
messageId,
|
||||
response: 'Messages queued for delivery'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
sendmail = this._spawn(this.path, args);
|
||||
} catch (E) {
|
||||
this.logger.error(
|
||||
{
|
||||
err: E,
|
||||
tnx: 'spawn',
|
||||
messageId
|
||||
},
|
||||
'Error occurred while spawning sendmail. %s',
|
||||
E.message
|
||||
);
|
||||
return callback(E);
|
||||
}
|
||||
|
||||
if (sendmail) {
|
||||
sendmail.on('error', err => {
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'spawn',
|
||||
messageId
|
||||
},
|
||||
'Error occurred when sending message %s. %s',
|
||||
messageId,
|
||||
err.message
|
||||
);
|
||||
callback(err);
|
||||
});
|
||||
|
||||
sendmail.once('exit', code => {
|
||||
if (!code) {
|
||||
return callback();
|
||||
}
|
||||
let err;
|
||||
if (code === 127) {
|
||||
err = new Error('Sendmail command not found, process exited with code ' + code);
|
||||
} else {
|
||||
err = new Error('Sendmail exited with code ' + code);
|
||||
}
|
||||
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'stdin',
|
||||
messageId
|
||||
},
|
||||
'Error sending message %s to sendmail. %s',
|
||||
messageId,
|
||||
err.message
|
||||
);
|
||||
callback(err);
|
||||
});
|
||||
sendmail.once('close', callback);
|
||||
|
||||
sendmail.stdin.on('error', err => {
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'stdin',
|
||||
messageId
|
||||
},
|
||||
'Error occurred when piping message %s to sendmail. %s',
|
||||
messageId,
|
||||
err.message
|
||||
);
|
||||
callback(err);
|
||||
});
|
||||
|
||||
let recipients = [].concat(envelope.to || []);
|
||||
if (recipients.length > 3) {
|
||||
recipients.push('...and ' + recipients.splice(2).length + ' more');
|
||||
}
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'send',
|
||||
messageId
|
||||
},
|
||||
'Sending message %s to <%s>',
|
||||
messageId,
|
||||
recipients.join(', ')
|
||||
);
|
||||
|
||||
let sourceStream = mail.message.createReadStream();
|
||||
sourceStream.once('error', err => {
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'stdin',
|
||||
messageId
|
||||
},
|
||||
'Error occurred when generating message %s. %s',
|
||||
messageId,
|
||||
err.message
|
||||
);
|
||||
sendmail.kill('SIGINT'); // do not deliver the message
|
||||
callback(err);
|
||||
});
|
||||
|
||||
sourceStream.pipe(sendmail.stdin);
|
||||
} else {
|
||||
return callback(new Error('sendmail was not found'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SendmailTransport;
|
349
backend/apis/nodejs/node_modules/nodemailer/lib/ses-transport/index.js
generated
vendored
Normal file
349
backend/apis/nodejs/node_modules/nodemailer/lib/ses-transport/index.js
generated
vendored
Normal file
@ -0,0 +1,349 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const packageData = require('../../package.json');
|
||||
const shared = require('../shared');
|
||||
const LeWindows = require('../mime-node/le-windows');
|
||||
|
||||
/**
|
||||
* Generates a Transport object for AWS SES
|
||||
*
|
||||
* Possible options can be the following:
|
||||
*
|
||||
* * **sendingRate** optional Number specifying how many messages per second should be delivered to SES
|
||||
* * **maxConnections** optional Number specifying max number of parallel connections to SES
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} optional config parameter
|
||||
*/
|
||||
class SESTransport extends EventEmitter {
|
||||
constructor(options) {
|
||||
super();
|
||||
options = options || {};
|
||||
|
||||
this.options = options || {};
|
||||
this.ses = this.options.SES;
|
||||
|
||||
this.name = 'SESTransport';
|
||||
this.version = packageData.version;
|
||||
|
||||
this.logger = shared.getLogger(this.options, {
|
||||
component: this.options.component || 'ses-transport'
|
||||
});
|
||||
|
||||
// parallel sending connections
|
||||
this.maxConnections = Number(this.options.maxConnections) || Infinity;
|
||||
this.connections = 0;
|
||||
|
||||
// max messages per second
|
||||
this.sendingRate = Number(this.options.sendingRate) || Infinity;
|
||||
this.sendingRateTTL = null;
|
||||
this.rateInterval = 1000; // milliseconds
|
||||
this.rateMessages = [];
|
||||
|
||||
this.pending = [];
|
||||
|
||||
this.idling = true;
|
||||
|
||||
setImmediate(() => {
|
||||
if (this.idling) {
|
||||
this.emit('idle');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a sending of a message
|
||||
*
|
||||
* @param {Object} emailMessage MailComposer object
|
||||
* @param {Function} callback Callback function to run when the sending is completed
|
||||
*/
|
||||
send(mail, callback) {
|
||||
if (this.connections >= this.maxConnections) {
|
||||
this.idling = false;
|
||||
return this.pending.push({
|
||||
mail,
|
||||
callback
|
||||
});
|
||||
}
|
||||
|
||||
if (!this._checkSendingRate()) {
|
||||
this.idling = false;
|
||||
return this.pending.push({
|
||||
mail,
|
||||
callback
|
||||
});
|
||||
}
|
||||
|
||||
this._send(mail, (...args) => {
|
||||
setImmediate(() => callback(...args));
|
||||
this._sent();
|
||||
});
|
||||
}
|
||||
|
||||
_checkRatedQueue() {
|
||||
if (this.connections >= this.maxConnections || !this._checkSendingRate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.pending.length) {
|
||||
if (!this.idling) {
|
||||
this.idling = true;
|
||||
this.emit('idle');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let next = this.pending.shift();
|
||||
this._send(next.mail, (...args) => {
|
||||
setImmediate(() => next.callback(...args));
|
||||
this._sent();
|
||||
});
|
||||
}
|
||||
|
||||
_checkSendingRate() {
|
||||
clearTimeout(this.sendingRateTTL);
|
||||
|
||||
let now = Date.now();
|
||||
let oldest = false;
|
||||
// delete older messages
|
||||
for (let i = this.rateMessages.length - 1; i >= 0; i--) {
|
||||
if (this.rateMessages[i].ts >= now - this.rateInterval && (!oldest || this.rateMessages[i].ts < oldest)) {
|
||||
oldest = this.rateMessages[i].ts;
|
||||
}
|
||||
|
||||
if (this.rateMessages[i].ts < now - this.rateInterval && !this.rateMessages[i].pending) {
|
||||
this.rateMessages.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.rateMessages.length < this.sendingRate) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let delay = Math.max(oldest + 1001, now + 20);
|
||||
this.sendingRateTTL = setTimeout(() => this._checkRatedQueue(), now - delay);
|
||||
|
||||
try {
|
||||
this.sendingRateTTL.unref();
|
||||
} catch (E) {
|
||||
// Ignore. Happens on envs with non-node timer implementation
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_sent() {
|
||||
this.connections--;
|
||||
this._checkRatedQueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there are free slots in the queue
|
||||
*/
|
||||
isIdle() {
|
||||
return this.idling;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles a mailcomposer message and forwards it to SES
|
||||
*
|
||||
* @param {Object} emailMessage MailComposer object
|
||||
* @param {Function} callback Callback function to run when the sending is completed
|
||||
*/
|
||||
_send(mail, callback) {
|
||||
let statObject = {
|
||||
ts: Date.now(),
|
||||
pending: true
|
||||
};
|
||||
this.connections++;
|
||||
this.rateMessages.push(statObject);
|
||||
|
||||
let envelope = mail.data.envelope || mail.message.getEnvelope();
|
||||
let messageId = mail.message.messageId();
|
||||
|
||||
let recipients = [].concat(envelope.to || []);
|
||||
if (recipients.length > 3) {
|
||||
recipients.push('...and ' + recipients.splice(2).length + ' more');
|
||||
}
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'send',
|
||||
messageId
|
||||
},
|
||||
'Sending message %s to <%s>',
|
||||
messageId,
|
||||
recipients.join(', ')
|
||||
);
|
||||
|
||||
let getRawMessage = next => {
|
||||
// do not use Message-ID and Date in DKIM signature
|
||||
if (!mail.data._dkim) {
|
||||
mail.data._dkim = {};
|
||||
}
|
||||
if (mail.data._dkim.skipFields && typeof mail.data._dkim.skipFields === 'string') {
|
||||
mail.data._dkim.skipFields += ':date:message-id';
|
||||
} else {
|
||||
mail.data._dkim.skipFields = 'date:message-id';
|
||||
}
|
||||
|
||||
let sourceStream = mail.message.createReadStream();
|
||||
let stream = sourceStream.pipe(new LeWindows());
|
||||
let chunks = [];
|
||||
let chunklen = 0;
|
||||
|
||||
stream.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = stream.read()) !== null) {
|
||||
chunks.push(chunk);
|
||||
chunklen += chunk.length;
|
||||
}
|
||||
});
|
||||
|
||||
sourceStream.once('error', err => stream.emit('error', err));
|
||||
|
||||
stream.once('error', err => {
|
||||
next(err);
|
||||
});
|
||||
|
||||
stream.once('end', () => next(null, Buffer.concat(chunks, chunklen)));
|
||||
};
|
||||
|
||||
setImmediate(() =>
|
||||
getRawMessage((err, raw) => {
|
||||
if (err) {
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'send',
|
||||
messageId
|
||||
},
|
||||
'Failed creating message for %s. %s',
|
||||
messageId,
|
||||
err.message
|
||||
);
|
||||
statObject.pending = false;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let sesMessage = {
|
||||
RawMessage: {
|
||||
// required
|
||||
Data: raw // required
|
||||
},
|
||||
Source: envelope.from,
|
||||
Destinations: envelope.to
|
||||
};
|
||||
|
||||
Object.keys(mail.data.ses || {}).forEach(key => {
|
||||
sesMessage[key] = mail.data.ses[key];
|
||||
});
|
||||
|
||||
let ses = (this.ses.aws ? this.ses.ses : this.ses) || {};
|
||||
let aws = this.ses.aws || {};
|
||||
|
||||
let getRegion = cb => {
|
||||
if (ses.config && typeof ses.config.region === 'function') {
|
||||
// promise
|
||||
return ses.config
|
||||
.region()
|
||||
.then(region => cb(null, region))
|
||||
.catch(err => cb(err));
|
||||
}
|
||||
return cb(null, (ses.config && ses.config.region) || 'us-east-1');
|
||||
};
|
||||
|
||||
getRegion((err, region) => {
|
||||
if (err || !region) {
|
||||
region = 'us-east-1';
|
||||
}
|
||||
|
||||
let sendPromise;
|
||||
if (typeof ses.send === 'function' && aws.SendRawEmailCommand) {
|
||||
// v3 API
|
||||
sendPromise = ses.send(new aws.SendRawEmailCommand(sesMessage));
|
||||
} else {
|
||||
// v2 API
|
||||
sendPromise = ses.sendRawEmail(sesMessage).promise();
|
||||
}
|
||||
|
||||
sendPromise
|
||||
.then(data => {
|
||||
if (region === 'us-east-1') {
|
||||
region = 'email';
|
||||
}
|
||||
|
||||
statObject.pending = false;
|
||||
callback(null, {
|
||||
envelope: {
|
||||
from: envelope.from,
|
||||
to: envelope.to
|
||||
},
|
||||
messageId: '<' + data.MessageId + (!/@/.test(data.MessageId) ? '@' + region + '.amazonses.com' : '') + '>',
|
||||
response: data.MessageId,
|
||||
raw
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'send'
|
||||
},
|
||||
'Send error for %s: %s',
|
||||
messageId,
|
||||
err.message
|
||||
);
|
||||
statObject.pending = false;
|
||||
callback(err);
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies SES configuration
|
||||
*
|
||||
* @param {Function} callback Callback function
|
||||
*/
|
||||
verify(callback) {
|
||||
let promise;
|
||||
let ses = (this.ses.aws ? this.ses.ses : this.ses) || {};
|
||||
let aws = this.ses.aws || {};
|
||||
|
||||
const sesMessage = {
|
||||
RawMessage: {
|
||||
// required
|
||||
Data: 'From: invalid@invalid\r\nTo: invalid@invalid\r\n Subject: Invalid\r\n\r\nInvalid'
|
||||
},
|
||||
Source: 'invalid@invalid',
|
||||
Destinations: ['invalid@invalid']
|
||||
};
|
||||
|
||||
if (!callback) {
|
||||
promise = new Promise((resolve, reject) => {
|
||||
callback = shared.callbackPromise(resolve, reject);
|
||||
});
|
||||
}
|
||||
const cb = err => {
|
||||
if (err && (err.code || err.Code) !== 'InvalidParameterValue') {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
};
|
||||
|
||||
if (typeof ses.send === 'function' && aws.SendRawEmailCommand) {
|
||||
// v3 API
|
||||
sesMessage.RawMessage.Data = Buffer.from(sesMessage.RawMessage.Data);
|
||||
ses.send(new aws.SendRawEmailCommand(sesMessage), cb);
|
||||
} else {
|
||||
// v2 API
|
||||
ses.sendRawEmail(sesMessage, cb);
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SESTransport;
|
688
backend/apis/nodejs/node_modules/nodemailer/lib/shared/index.js
generated
vendored
Normal file
688
backend/apis/nodejs/node_modules/nodemailer/lib/shared/index.js
generated
vendored
Normal file
@ -0,0 +1,688 @@
|
||||
/* eslint no-console: 0 */
|
||||
|
||||
'use strict';
|
||||
|
||||
const urllib = require('url');
|
||||
const util = require('util');
|
||||
const fs = require('fs');
|
||||
const nmfetch = require('../fetch');
|
||||
const dns = require('dns');
|
||||
const net = require('net');
|
||||
const os = require('os');
|
||||
|
||||
const DNS_TTL = 5 * 60 * 1000;
|
||||
|
||||
let networkInterfaces;
|
||||
try {
|
||||
networkInterfaces = os.networkInterfaces();
|
||||
} catch (err) {
|
||||
// fails on some systems
|
||||
}
|
||||
|
||||
module.exports.networkInterfaces = networkInterfaces;
|
||||
|
||||
const isFamilySupported = (family, allowInternal) => {
|
||||
let networkInterfaces = module.exports.networkInterfaces;
|
||||
if (!networkInterfaces) {
|
||||
// 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;
|
||||
};
|
||||
|
||||
const resolver = (family, hostname, options, callback) => {
|
||||
options = options || {};
|
||||
const familySupported = isFamilySupported(family, options.allowInternalNetworkInterfaces);
|
||||
|
||||
if (!familySupported) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
const resolver = dns.Resolver ? new dns.Resolver(options) : dns;
|
||||
resolver['resolve' + family](hostname, (err, addresses) => {
|
||||
if (err) {
|
||||
switch (err.code) {
|
||||
case dns.NODATA:
|
||||
case dns.NOTFOUND:
|
||||
case dns.NOTIMP:
|
||||
case dns.SERVFAIL:
|
||||
case dns.CONNREFUSED:
|
||||
case dns.REFUSED:
|
||||
case 'EAI_AGAIN':
|
||||
return callback(null, []);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, Array.isArray(addresses) ? addresses : [].concat(addresses || []));
|
||||
});
|
||||
};
|
||||
|
||||
const dnsCache = (module.exports.dnsCache = new Map());
|
||||
|
||||
const formatDNSValue = (value, extra) => {
|
||||
if (!value) {
|
||||
return Object.assign({}, extra || {});
|
||||
}
|
||||
|
||||
return Object.assign(
|
||||
{
|
||||
servername: value.servername,
|
||||
host:
|
||||
!value.addresses || !value.addresses.length
|
||||
? null
|
||||
: value.addresses.length === 1
|
||||
? value.addresses[0]
|
||||
: value.addresses[Math.floor(Math.random() * value.addresses.length)]
|
||||
},
|
||||
extra || {}
|
||||
);
|
||||
};
|
||||
|
||||
module.exports.resolveHostname = (options, callback) => {
|
||||
options = options || {};
|
||||
|
||||
if (!options.host && options.servername) {
|
||||
options.host = options.servername;
|
||||
}
|
||||
|
||||
if (!options.host || net.isIP(options.host)) {
|
||||
// nothing to do here
|
||||
let value = {
|
||||
addresses: [options.host],
|
||||
servername: options.servername || false
|
||||
};
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(value, {
|
||||
cached: false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
let cached;
|
||||
if (dnsCache.has(options.host)) {
|
||||
cached = dnsCache.get(options.host);
|
||||
|
||||
if (!cached.expires || cached.expires >= Date.now()) {
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(cached.value, {
|
||||
cached: true
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
resolver(4, options.host, options, (err, addresses) => {
|
||||
if (err) {
|
||||
if (cached) {
|
||||
// ignore error, use expired value
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(cached.value, {
|
||||
cached: true,
|
||||
error: err
|
||||
})
|
||||
);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (addresses && addresses.length) {
|
||||
let value = {
|
||||
addresses,
|
||||
servername: options.servername || options.host
|
||||
};
|
||||
|
||||
dnsCache.set(options.host, {
|
||||
value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(value, {
|
||||
cached: false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
resolver(6, options.host, options, (err, addresses) => {
|
||||
if (err) {
|
||||
if (cached) {
|
||||
// ignore error, use expired value
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(cached.value, {
|
||||
cached: true,
|
||||
error: err
|
||||
})
|
||||
);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (addresses && addresses.length) {
|
||||
let value = {
|
||||
addresses,
|
||||
servername: options.servername || options.host
|
||||
};
|
||||
|
||||
dnsCache.set(options.host, {
|
||||
value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(value, {
|
||||
cached: false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
dns.lookup(options.host, { all: true }, (err, addresses) => {
|
||||
if (err) {
|
||||
if (cached) {
|
||||
// ignore error, use expired value
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(cached.value, {
|
||||
cached: true,
|
||||
error: err
|
||||
})
|
||||
);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let address = addresses
|
||||
? addresses
|
||||
.filter(addr => isFamilySupported(addr.family))
|
||||
.map(addr => addr.address)
|
||||
.shift()
|
||||
: false;
|
||||
|
||||
if (addresses && addresses.length && !address) {
|
||||
// there are addresses but none can be used
|
||||
console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`);
|
||||
}
|
||||
|
||||
if (!address && cached) {
|
||||
// nothing was found, fallback to cached value
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(cached.value, {
|
||||
cached: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
let value = {
|
||||
addresses: address ? [address] : [options.host],
|
||||
servername: options.servername || options.host
|
||||
};
|
||||
|
||||
dnsCache.set(options.host, {
|
||||
value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(value, {
|
||||
cached: false
|
||||
})
|
||||
);
|
||||
});
|
||||
} catch (err) {
|
||||
if (cached) {
|
||||
// ignore error, use expired value
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(cached.value, {
|
||||
cached: true,
|
||||
error: err
|
||||
})
|
||||
);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Parses connection url to a structured configuration object
|
||||
*
|
||||
* @param {String} str Connection url
|
||||
* @return {Object} Configuration object
|
||||
*/
|
||||
module.exports.parseConnectionUrl = str => {
|
||||
str = str || '';
|
||||
let options = {};
|
||||
|
||||
[urllib.parse(str, true)].forEach(url => {
|
||||
let auth;
|
||||
|
||||
switch (url.protocol) {
|
||||
case 'smtp:':
|
||||
options.secure = false;
|
||||
break;
|
||||
case 'smtps:':
|
||||
options.secure = true;
|
||||
break;
|
||||
case 'direct:':
|
||||
options.direct = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isNaN(url.port) && Number(url.port)) {
|
||||
options.port = Number(url.port);
|
||||
}
|
||||
|
||||
if (url.hostname) {
|
||||
options.host = url.hostname;
|
||||
}
|
||||
|
||||
if (url.auth) {
|
||||
auth = url.auth.split(':');
|
||||
|
||||
if (!options.auth) {
|
||||
options.auth = {};
|
||||
}
|
||||
|
||||
options.auth.user = auth.shift();
|
||||
options.auth.pass = auth.join(':');
|
||||
}
|
||||
|
||||
Object.keys(url.query || {}).forEach(key => {
|
||||
let obj = options;
|
||||
let lKey = key;
|
||||
let value = url.query[key];
|
||||
|
||||
if (!isNaN(value)) {
|
||||
value = Number(value);
|
||||
}
|
||||
|
||||
switch (value) {
|
||||
case 'true':
|
||||
value = true;
|
||||
break;
|
||||
case 'false':
|
||||
value = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// tls is nested object
|
||||
if (key.indexOf('tls.') === 0) {
|
||||
lKey = key.substr(4);
|
||||
if (!options.tls) {
|
||||
options.tls = {};
|
||||
}
|
||||
obj = options.tls;
|
||||
} else if (key.indexOf('.') >= 0) {
|
||||
// ignore nested properties besides tls
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(lKey in obj)) {
|
||||
obj[lKey] = value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
module.exports._logFunc = (logger, level, defaults, data, message, ...args) => {
|
||||
let entry = {};
|
||||
|
||||
Object.keys(defaults || {}).forEach(key => {
|
||||
if (key !== 'level') {
|
||||
entry[key] = defaults[key];
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(data || {}).forEach(key => {
|
||||
if (key !== 'level') {
|
||||
entry[key] = data[key];
|
||||
}
|
||||
});
|
||||
|
||||
logger[level](entry, message, ...args);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a bunyan-compatible logger interface. Uses either provided logger or
|
||||
* creates a default console logger
|
||||
*
|
||||
* @param {Object} [options] Options object that might include 'logger' value
|
||||
* @return {Object} bunyan compatible logger
|
||||
*/
|
||||
module.exports.getLogger = (options, defaults) => {
|
||||
options = options || {};
|
||||
|
||||
let response = {};
|
||||
let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
|
||||
|
||||
if (!options.logger) {
|
||||
// use vanity logger
|
||||
levels.forEach(level => {
|
||||
response[level] = () => false;
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
let logger = options.logger;
|
||||
|
||||
if (options.logger === true) {
|
||||
// create console logger
|
||||
logger = createDefaultLogger(levels);
|
||||
}
|
||||
|
||||
levels.forEach(level => {
|
||||
response[level] = (data, message, ...args) => {
|
||||
module.exports._logFunc(logger, level, defaults, data, message, ...args);
|
||||
};
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper for creating a callback that either resolves or rejects a promise
|
||||
* based on input
|
||||
*
|
||||
* @param {Function} resolve Function to run if callback is called
|
||||
* @param {Function} reject Function to run if callback ends with an error
|
||||
*/
|
||||
module.exports.callbackPromise = (resolve, reject) =>
|
||||
function () {
|
||||
let args = Array.from(arguments);
|
||||
let err = args.shift();
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(...args);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.parseDataURI = uri => {
|
||||
let input = uri;
|
||||
let commaPos = input.indexOf(',');
|
||||
if (!commaPos) {
|
||||
return uri;
|
||||
}
|
||||
|
||||
let data = input.substring(commaPos + 1);
|
||||
let metaStr = input.substring('data:'.length, commaPos);
|
||||
|
||||
let encoding;
|
||||
|
||||
let metaEntries = metaStr.split(';');
|
||||
let lastMetaEntry = metaEntries.length > 1 ? metaEntries[metaEntries.length - 1] : false;
|
||||
if (lastMetaEntry && lastMetaEntry.indexOf('=') < 0) {
|
||||
encoding = lastMetaEntry.toLowerCase();
|
||||
metaEntries.pop();
|
||||
}
|
||||
|
||||
let contentType = metaEntries.shift() || 'application/octet-stream';
|
||||
let params = {};
|
||||
for (let entry of metaEntries) {
|
||||
let sep = entry.indexOf('=');
|
||||
if (sep >= 0) {
|
||||
let key = entry.substring(0, sep);
|
||||
let value = entry.substring(sep + 1);
|
||||
params[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
switch (encoding) {
|
||||
case 'base64':
|
||||
data = Buffer.from(data, 'base64');
|
||||
break;
|
||||
case 'utf8':
|
||||
data = Buffer.from(data);
|
||||
break;
|
||||
default:
|
||||
try {
|
||||
data = Buffer.from(decodeURIComponent(data));
|
||||
} catch (err) {
|
||||
data = Buffer.from(data);
|
||||
}
|
||||
data = Buffer.from(data);
|
||||
}
|
||||
|
||||
return { data, encoding, contentType, params };
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves a String or a Buffer value for content value. Useful if the value
|
||||
* is a Stream or a file or an URL. If the value is a Stream, overwrites
|
||||
* the stream object with the resolved value (you can't stream a value twice).
|
||||
*
|
||||
* This is useful when you want to create a plugin that needs a content value,
|
||||
* for example the `html` or `text` value as a String or a Buffer but not as
|
||||
* a file path or an URL.
|
||||
*
|
||||
* @param {Object} data An object or an Array you want to resolve an element for
|
||||
* @param {String|Number} key Property name or an Array index
|
||||
* @param {Function} callback Callback function with (err, value)
|
||||
*/
|
||||
module.exports.resolveContent = (data, key, callback) => {
|
||||
let promise;
|
||||
|
||||
if (!callback) {
|
||||
promise = new Promise((resolve, reject) => {
|
||||
callback = module.exports.callbackPromise(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
let content = (data && data[key] && data[key].content) || data[key];
|
||||
let contentStream;
|
||||
let encoding = ((typeof data[key] === 'object' && data[key].encoding) || 'utf8')
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.replace(/[-_\s]/g, '');
|
||||
|
||||
if (!content) {
|
||||
return callback(null, content);
|
||||
}
|
||||
|
||||
if (typeof content === 'object') {
|
||||
if (typeof content.pipe === 'function') {
|
||||
return resolveStream(content, (err, value) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
// we can't stream twice the same content, so we need
|
||||
// to replace the stream object with the streaming result
|
||||
if (data[key].content) {
|
||||
data[key].content = value;
|
||||
} else {
|
||||
data[key] = value;
|
||||
}
|
||||
callback(null, value);
|
||||
});
|
||||
} else if (/^https?:\/\//i.test(content.path || content.href)) {
|
||||
contentStream = nmfetch(content.path || content.href);
|
||||
return resolveStream(contentStream, callback);
|
||||
} else if (/^data:/i.test(content.path || content.href)) {
|
||||
let parsedDataUri = module.exports.parseDataURI(content.path || content.href);
|
||||
|
||||
if (!parsedDataUri || !parsedDataUri.data) {
|
||||
return callback(null, Buffer.from(0));
|
||||
}
|
||||
return callback(null, parsedDataUri.data);
|
||||
} else if (content.path) {
|
||||
return resolveStream(fs.createReadStream(content.path), callback);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof data[key].content === 'string' && !['utf8', 'usascii', 'ascii'].includes(encoding)) {
|
||||
content = Buffer.from(data[key].content, encoding);
|
||||
}
|
||||
|
||||
// default action, return as is
|
||||
setImmediate(() => callback(null, content));
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
/**
|
||||
* Copies properties from source objects to target objects
|
||||
*/
|
||||
module.exports.assign = function (/* target, ... sources */) {
|
||||
let args = Array.from(arguments);
|
||||
let target = args.shift() || {};
|
||||
|
||||
args.forEach(source => {
|
||||
Object.keys(source || {}).forEach(key => {
|
||||
if (['tls', 'auth'].includes(key) && source[key] && typeof source[key] === 'object') {
|
||||
// tls and auth are special keys that need to be enumerated separately
|
||||
// other objects are passed as is
|
||||
if (!target[key]) {
|
||||
// ensure that target has this key
|
||||
target[key] = {};
|
||||
}
|
||||
Object.keys(source[key]).forEach(subKey => {
|
||||
target[key][subKey] = source[key][subKey];
|
||||
});
|
||||
} else {
|
||||
target[key] = source[key];
|
||||
}
|
||||
});
|
||||
});
|
||||
return target;
|
||||
};
|
||||
|
||||
module.exports.encodeXText = str => {
|
||||
// ! 0x21
|
||||
// + 0x2B
|
||||
// = 0x3D
|
||||
// ~ 0x7E
|
||||
if (!/[^\x21-\x2A\x2C-\x3C\x3E-\x7E]/.test(str)) {
|
||||
return str;
|
||||
}
|
||||
let buf = Buffer.from(str);
|
||||
let result = '';
|
||||
for (let i = 0, len = buf.length; i < len; i++) {
|
||||
let c = buf[i];
|
||||
if (c < 0x21 || c > 0x7e || c === 0x2b || c === 0x3d) {
|
||||
result += '+' + (c < 0x10 ? '0' : '') + c.toString(16).toUpperCase();
|
||||
} else {
|
||||
result += String.fromCharCode(c);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Streams a stream value into a Buffer
|
||||
*
|
||||
* @param {Object} stream Readable stream
|
||||
* @param {Function} callback Callback function with (err, value)
|
||||
*/
|
||||
function resolveStream(stream, callback) {
|
||||
let responded = false;
|
||||
let chunks = [];
|
||||
let chunklen = 0;
|
||||
|
||||
stream.on('error', err => {
|
||||
if (responded) {
|
||||
return;
|
||||
}
|
||||
|
||||
responded = true;
|
||||
callback(err);
|
||||
});
|
||||
|
||||
stream.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = stream.read()) !== null) {
|
||||
chunks.push(chunk);
|
||||
chunklen += chunk.length;
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
if (responded) {
|
||||
return;
|
||||
}
|
||||
responded = true;
|
||||
|
||||
let value;
|
||||
|
||||
try {
|
||||
value = Buffer.concat(chunks, chunklen);
|
||||
} catch (E) {
|
||||
return callback(E);
|
||||
}
|
||||
callback(null, value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a bunyan-like logger that prints to console
|
||||
*
|
||||
* @returns {Object} Bunyan logger instance
|
||||
*/
|
||||
function createDefaultLogger(levels) {
|
||||
let levelMaxLen = 0;
|
||||
let levelNames = new Map();
|
||||
levels.forEach(level => {
|
||||
if (level.length > levelMaxLen) {
|
||||
levelMaxLen = level.length;
|
||||
}
|
||||
});
|
||||
|
||||
levels.forEach(level => {
|
||||
let levelName = level.toUpperCase();
|
||||
if (levelName.length < levelMaxLen) {
|
||||
levelName += ' '.repeat(levelMaxLen - levelName.length);
|
||||
}
|
||||
levelNames.set(level, levelName);
|
||||
});
|
||||
|
||||
let print = (level, entry, message, ...args) => {
|
||||
let prefix = '';
|
||||
if (entry) {
|
||||
if (entry.tnx === 'server') {
|
||||
prefix = 'S: ';
|
||||
} else if (entry.tnx === 'client') {
|
||||
prefix = 'C: ';
|
||||
}
|
||||
|
||||
if (entry.sid) {
|
||||
prefix = '[' + entry.sid + '] ' + prefix;
|
||||
}
|
||||
|
||||
if (entry.cid) {
|
||||
prefix = '[#' + entry.cid + '] ' + prefix;
|
||||
}
|
||||
}
|
||||
|
||||
message = util.format(message, ...args);
|
||||
message.split(/\r?\n/).forEach(line => {
|
||||
console.log('[%s] %s %s', new Date().toISOString().substr(0, 19).replace(/T/, ' '), levelNames.get(level), prefix + line);
|
||||
});
|
||||
};
|
||||
|
||||
let logger = {};
|
||||
levels.forEach(level => {
|
||||
logger[level] = print.bind(null, level);
|
||||
});
|
||||
|
||||
return logger;
|
||||
}
|
108
backend/apis/nodejs/node_modules/nodemailer/lib/smtp-connection/data-stream.js
generated
vendored
Normal file
108
backend/apis/nodejs/node_modules/nodemailer/lib/smtp-connection/data-stream.js
generated
vendored
Normal file
@ -0,0 +1,108 @@
|
||||
'use strict';
|
||||
|
||||
const stream = require('stream');
|
||||
const Transform = stream.Transform;
|
||||
|
||||
/**
|
||||
* Escapes dots in the beginning of lines. Ends the stream with <CR><LF>.<CR><LF>
|
||||
* Also makes sure that only <CR><LF> sequences are used for linebreaks
|
||||
*
|
||||
* @param {Object} options Stream options
|
||||
*/
|
||||
class DataStream extends Transform {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
// init Transform
|
||||
this.options = options || {};
|
||||
this._curLine = '';
|
||||
|
||||
this.inByteCount = 0;
|
||||
this.outByteCount = 0;
|
||||
this.lastByte = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes dots
|
||||
*/
|
||||
_transform(chunk, encoding, done) {
|
||||
let chunks = [];
|
||||
let chunklen = 0;
|
||||
let i,
|
||||
len,
|
||||
lastPos = 0;
|
||||
let buf;
|
||||
|
||||
if (!chunk || !chunk.length) {
|
||||
return done();
|
||||
}
|
||||
|
||||
if (typeof chunk === 'string') {
|
||||
chunk = Buffer.from(chunk);
|
||||
}
|
||||
|
||||
this.inByteCount += chunk.length;
|
||||
|
||||
for (i = 0, len = chunk.length; i < len; i++) {
|
||||
if (chunk[i] === 0x2e) {
|
||||
// .
|
||||
if ((i && chunk[i - 1] === 0x0a) || (!i && (!this.lastByte || this.lastByte === 0x0a))) {
|
||||
buf = chunk.slice(lastPos, i + 1);
|
||||
chunks.push(buf);
|
||||
chunks.push(Buffer.from('.'));
|
||||
chunklen += buf.length + 1;
|
||||
lastPos = i + 1;
|
||||
}
|
||||
} else if (chunk[i] === 0x0a) {
|
||||
// .
|
||||
if ((i && chunk[i - 1] !== 0x0d) || (!i && this.lastByte !== 0x0d)) {
|
||||
if (i > lastPos) {
|
||||
buf = chunk.slice(lastPos, i);
|
||||
chunks.push(buf);
|
||||
chunklen += buf.length + 2;
|
||||
} else {
|
||||
chunklen += 2;
|
||||
}
|
||||
chunks.push(Buffer.from('\r\n'));
|
||||
lastPos = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chunklen) {
|
||||
// add last piece
|
||||
if (lastPos < chunk.length) {
|
||||
buf = chunk.slice(lastPos);
|
||||
chunks.push(buf);
|
||||
chunklen += buf.length;
|
||||
}
|
||||
|
||||
this.outByteCount += chunklen;
|
||||
this.push(Buffer.concat(chunks, chunklen));
|
||||
} else {
|
||||
this.outByteCount += chunk.length;
|
||||
this.push(chunk);
|
||||
}
|
||||
|
||||
this.lastByte = chunk[chunk.length - 1];
|
||||
done();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizes the stream with a dot on a single line
|
||||
*/
|
||||
_flush(done) {
|
||||
let buf;
|
||||
if (this.lastByte === 0x0a) {
|
||||
buf = Buffer.from('.\r\n');
|
||||
} else if (this.lastByte === 0x0d) {
|
||||
buf = Buffer.from('\n.\r\n');
|
||||
} else {
|
||||
buf = Buffer.from('\r\n.\r\n');
|
||||
}
|
||||
this.outByteCount += buf.length;
|
||||
this.push(buf);
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DataStream;
|
143
backend/apis/nodejs/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js
generated
vendored
Normal file
143
backend/apis/nodejs/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js
generated
vendored
Normal file
@ -0,0 +1,143 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Minimal HTTP/S proxy client
|
||||
*/
|
||||
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const urllib = require('url');
|
||||
|
||||
/**
|
||||
* Establishes proxied connection to destinationPort
|
||||
*
|
||||
* httpProxyClient("http://localhost:3128/", 80, "google.com", function(err, socket){
|
||||
* socket.write("GET / HTTP/1.0\r\n\r\n");
|
||||
* });
|
||||
*
|
||||
* @param {String} proxyUrl proxy configuration, etg "http://proxy.host:3128/"
|
||||
* @param {Number} destinationPort Port to open in destination host
|
||||
* @param {String} destinationHost Destination hostname
|
||||
* @param {Function} callback Callback to run with the rocket object once connection is established
|
||||
*/
|
||||
function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
|
||||
let proxy = urllib.parse(proxyUrl);
|
||||
|
||||
// create a socket connection to the proxy server
|
||||
let options;
|
||||
let connect;
|
||||
let socket;
|
||||
|
||||
options = {
|
||||
host: proxy.hostname,
|
||||
port: Number(proxy.port) ? Number(proxy.port) : proxy.protocol === 'https:' ? 443 : 80
|
||||
};
|
||||
|
||||
if (proxy.protocol === 'https:') {
|
||||
// we can use untrusted proxies as long as we verify actual SMTP certificates
|
||||
options.rejectUnauthorized = false;
|
||||
connect = tls.connect.bind(tls);
|
||||
} else {
|
||||
connect = net.connect.bind(net);
|
||||
}
|
||||
|
||||
// Error harness for initial connection. Once connection is established, the responsibility
|
||||
// to handle errors is passed to whoever uses this socket
|
||||
let finished = false;
|
||||
let tempSocketErr = err => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
try {
|
||||
socket.destroy();
|
||||
} catch (E) {
|
||||
// ignore
|
||||
}
|
||||
callback(err);
|
||||
};
|
||||
|
||||
let timeoutErr = () => {
|
||||
let err = new Error('Proxy socket timed out');
|
||||
err.code = 'ETIMEDOUT';
|
||||
tempSocketErr(err);
|
||||
};
|
||||
|
||||
socket = connect(options, () => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
|
||||
let reqHeaders = {
|
||||
Host: destinationHost + ':' + destinationPort,
|
||||
Connection: 'close'
|
||||
};
|
||||
if (proxy.auth) {
|
||||
reqHeaders['Proxy-Authorization'] = 'Basic ' + Buffer.from(proxy.auth).toString('base64');
|
||||
}
|
||||
|
||||
socket.write(
|
||||
// HTTP method
|
||||
'CONNECT ' +
|
||||
destinationHost +
|
||||
':' +
|
||||
destinationPort +
|
||||
' HTTP/1.1\r\n' +
|
||||
// HTTP request headers
|
||||
Object.keys(reqHeaders)
|
||||
.map(key => key + ': ' + reqHeaders[key])
|
||||
.join('\r\n') +
|
||||
// End request
|
||||
'\r\n\r\n'
|
||||
);
|
||||
|
||||
let headers = '';
|
||||
let onSocketData = chunk => {
|
||||
let match;
|
||||
let remainder;
|
||||
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
|
||||
headers += chunk.toString('binary');
|
||||
if ((match = headers.match(/\r\n\r\n/))) {
|
||||
socket.removeListener('data', onSocketData);
|
||||
|
||||
remainder = headers.substr(match.index + match[0].length);
|
||||
headers = headers.substr(0, match.index);
|
||||
if (remainder) {
|
||||
socket.unshift(Buffer.from(remainder, 'binary'));
|
||||
}
|
||||
|
||||
// proxy connection is now established
|
||||
finished = true;
|
||||
|
||||
// check response code
|
||||
match = headers.match(/^HTTP\/\d+\.\d+ (\d+)/i);
|
||||
if (!match || (match[1] || '').charAt(0) !== '2') {
|
||||
try {
|
||||
socket.destroy();
|
||||
} catch (E) {
|
||||
// ignore
|
||||
}
|
||||
return callback(new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || '')));
|
||||
}
|
||||
|
||||
socket.removeListener('error', tempSocketErr);
|
||||
socket.removeListener('timeout', timeoutErr);
|
||||
socket.setTimeout(0);
|
||||
|
||||
return callback(null, socket);
|
||||
}
|
||||
};
|
||||
socket.on('data', onSocketData);
|
||||
});
|
||||
|
||||
socket.setTimeout(httpProxyClient.timeout || 30 * 1000);
|
||||
socket.on('timeout', timeoutErr);
|
||||
|
||||
socket.once('error', tempSocketErr);
|
||||
}
|
||||
|
||||
module.exports = httpProxyClient;
|
1836
backend/apis/nodejs/node_modules/nodemailer/lib/smtp-connection/index.js
generated
vendored
Normal file
1836
backend/apis/nodejs/node_modules/nodemailer/lib/smtp-connection/index.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
648
backend/apis/nodejs/node_modules/nodemailer/lib/smtp-pool/index.js
generated
vendored
Normal file
648
backend/apis/nodejs/node_modules/nodemailer/lib/smtp-pool/index.js
generated
vendored
Normal file
@ -0,0 +1,648 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const PoolResource = require('./pool-resource');
|
||||
const SMTPConnection = require('../smtp-connection');
|
||||
const wellKnown = require('../well-known');
|
||||
const shared = require('../shared');
|
||||
const packageData = require('../../package.json');
|
||||
|
||||
/**
|
||||
* Creates a SMTP pool transport object for Nodemailer
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} options SMTP Connection options
|
||||
*/
|
||||
class SMTPPool extends EventEmitter {
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
options = options || {};
|
||||
if (typeof options === 'string') {
|
||||
options = {
|
||||
url: options
|
||||
};
|
||||
}
|
||||
|
||||
let urlData;
|
||||
let service = options.service;
|
||||
|
||||
if (typeof options.getSocket === 'function') {
|
||||
this.getSocket = options.getSocket;
|
||||
}
|
||||
|
||||
if (options.url) {
|
||||
urlData = shared.parseConnectionUrl(options.url);
|
||||
service = service || urlData.service;
|
||||
}
|
||||
|
||||
this.options = shared.assign(
|
||||
false, // create new object
|
||||
options, // regular options
|
||||
urlData, // url options
|
||||
service && wellKnown(service) // wellknown options
|
||||
);
|
||||
|
||||
this.options.maxConnections = this.options.maxConnections || 5;
|
||||
this.options.maxMessages = this.options.maxMessages || 100;
|
||||
|
||||
this.logger = shared.getLogger(this.options, {
|
||||
component: this.options.component || 'smtp-pool'
|
||||
});
|
||||
|
||||
// temporary object
|
||||
let connection = new SMTPConnection(this.options);
|
||||
|
||||
this.name = 'SMTP (pool)';
|
||||
this.version = packageData.version + '[client:' + connection.version + ']';
|
||||
|
||||
this._rateLimit = {
|
||||
counter: 0,
|
||||
timeout: null,
|
||||
waiting: [],
|
||||
checkpoint: false,
|
||||
delta: Number(this.options.rateDelta) || 1000,
|
||||
limit: Number(this.options.rateLimit) || 0
|
||||
};
|
||||
this._closed = false;
|
||||
this._queue = [];
|
||||
this._connections = [];
|
||||
this._connectionCounter = 0;
|
||||
|
||||
this.idling = true;
|
||||
|
||||
setImmediate(() => {
|
||||
if (this.idling) {
|
||||
this.emit('idle');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder function for creating proxy sockets. This method immediatelly returns
|
||||
* without a socket
|
||||
*
|
||||
* @param {Object} options Connection options
|
||||
* @param {Function} callback Callback function to run with the socket keys
|
||||
*/
|
||||
getSocket(options, callback) {
|
||||
// return immediatelly
|
||||
return setImmediate(() => callback(null, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues an e-mail to be sent using the selected settings
|
||||
*
|
||||
* @param {Object} mail Mail object
|
||||
* @param {Function} callback Callback function
|
||||
*/
|
||||
send(mail, callback) {
|
||||
if (this._closed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._queue.push({
|
||||
mail,
|
||||
requeueAttempts: 0,
|
||||
callback
|
||||
});
|
||||
|
||||
if (this.idling && this._queue.length >= this.options.maxConnections) {
|
||||
this.idling = false;
|
||||
}
|
||||
|
||||
setImmediate(() => this._processMessages());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all connections in the pool. If there is a message being sent, the connection
|
||||
* is closed later
|
||||
*/
|
||||
close() {
|
||||
let connection;
|
||||
let len = this._connections.length;
|
||||
this._closed = true;
|
||||
|
||||
// clear rate limit timer if it exists
|
||||
clearTimeout(this._rateLimit.timeout);
|
||||
|
||||
if (!len && !this._queue.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove all available connections
|
||||
for (let i = len - 1; i >= 0; i--) {
|
||||
if (this._connections[i] && this._connections[i].available) {
|
||||
connection = this._connections[i];
|
||||
connection.close();
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'connection',
|
||||
cid: connection.id,
|
||||
action: 'removed'
|
||||
},
|
||||
'Connection #%s removed',
|
||||
connection.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (len && !this._connections.length) {
|
||||
this.logger.debug(
|
||||
{
|
||||
tnx: 'connection'
|
||||
},
|
||||
'All connections removed'
|
||||
);
|
||||
}
|
||||
|
||||
if (!this._queue.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure that entire queue would be cleaned
|
||||
let invokeCallbacks = () => {
|
||||
if (!this._queue.length) {
|
||||
this.logger.debug(
|
||||
{
|
||||
tnx: 'connection'
|
||||
},
|
||||
'Pending queue entries cleared'
|
||||
);
|
||||
return;
|
||||
}
|
||||
let entry = this._queue.shift();
|
||||
if (entry && typeof entry.callback === 'function') {
|
||||
try {
|
||||
entry.callback(new Error('Connection pool was closed'));
|
||||
} catch (E) {
|
||||
this.logger.error(
|
||||
{
|
||||
err: E,
|
||||
tnx: 'callback',
|
||||
cid: connection.id
|
||||
},
|
||||
'Callback error for #%s: %s',
|
||||
connection.id,
|
||||
E.message
|
||||
);
|
||||
}
|
||||
}
|
||||
setImmediate(invokeCallbacks);
|
||||
};
|
||||
setImmediate(invokeCallbacks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the queue and available connections. If there is a message to be sent and there is
|
||||
* an available connection, then use this connection to send the mail
|
||||
*/
|
||||
_processMessages() {
|
||||
let connection;
|
||||
let i, len;
|
||||
|
||||
// do nothing if already closed
|
||||
if (this._closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// do nothing if queue is empty
|
||||
if (!this._queue.length) {
|
||||
if (!this.idling) {
|
||||
// no pending jobs
|
||||
this.idling = true;
|
||||
this.emit('idle');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// find first available connection
|
||||
for (i = 0, len = this._connections.length; i < len; i++) {
|
||||
if (this._connections[i].available) {
|
||||
connection = this._connections[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!connection && this._connections.length < this.options.maxConnections) {
|
||||
connection = this._createConnection();
|
||||
}
|
||||
|
||||
if (!connection) {
|
||||
// no more free connection slots available
|
||||
this.idling = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// check if there is free space in the processing queue
|
||||
if (!this.idling && this._queue.length < this.options.maxConnections) {
|
||||
this.idling = true;
|
||||
this.emit('idle');
|
||||
}
|
||||
|
||||
let entry = (connection.queueEntry = this._queue.shift());
|
||||
entry.messageId = (connection.queueEntry.mail.message.getHeader('message-id') || '').replace(/[<>\s]/g, '');
|
||||
|
||||
connection.available = false;
|
||||
|
||||
this.logger.debug(
|
||||
{
|
||||
tnx: 'pool',
|
||||
cid: connection.id,
|
||||
messageId: entry.messageId,
|
||||
action: 'assign'
|
||||
},
|
||||
'Assigned message <%s> to #%s (%s)',
|
||||
entry.messageId,
|
||||
connection.id,
|
||||
connection.messages + 1
|
||||
);
|
||||
|
||||
if (this._rateLimit.limit) {
|
||||
this._rateLimit.counter++;
|
||||
if (!this._rateLimit.checkpoint) {
|
||||
this._rateLimit.checkpoint = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
connection.send(entry.mail, (err, info) => {
|
||||
// only process callback if current handler is not changed
|
||||
if (entry === connection.queueEntry) {
|
||||
try {
|
||||
entry.callback(err, info);
|
||||
} catch (E) {
|
||||
this.logger.error(
|
||||
{
|
||||
err: E,
|
||||
tnx: 'callback',
|
||||
cid: connection.id
|
||||
},
|
||||
'Callback error for #%s: %s',
|
||||
connection.id,
|
||||
E.message
|
||||
);
|
||||
}
|
||||
connection.queueEntry = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new pool resource
|
||||
*/
|
||||
_createConnection() {
|
||||
let connection = new PoolResource(this);
|
||||
|
||||
connection.id = ++this._connectionCounter;
|
||||
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'pool',
|
||||
cid: connection.id,
|
||||
action: 'conection'
|
||||
},
|
||||
'Created new pool resource #%s',
|
||||
connection.id
|
||||
);
|
||||
|
||||
// resource comes available
|
||||
connection.on('available', () => {
|
||||
this.logger.debug(
|
||||
{
|
||||
tnx: 'connection',
|
||||
cid: connection.id,
|
||||
action: 'available'
|
||||
},
|
||||
'Connection #%s became available',
|
||||
connection.id
|
||||
);
|
||||
|
||||
if (this._closed) {
|
||||
// if already closed run close() that will remove this connections from connections list
|
||||
this.close();
|
||||
} else {
|
||||
// check if there's anything else to send
|
||||
this._processMessages();
|
||||
}
|
||||
});
|
||||
|
||||
// resource is terminated with an error
|
||||
connection.once('error', err => {
|
||||
if (err.code !== 'EMAXLIMIT') {
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'pool',
|
||||
cid: connection.id
|
||||
},
|
||||
'Pool Error for #%s: %s',
|
||||
connection.id,
|
||||
err.message
|
||||
);
|
||||
} else {
|
||||
this.logger.debug(
|
||||
{
|
||||
tnx: 'pool',
|
||||
cid: connection.id,
|
||||
action: 'maxlimit'
|
||||
},
|
||||
'Max messages limit exchausted for #%s',
|
||||
connection.id
|
||||
);
|
||||
}
|
||||
|
||||
if (connection.queueEntry) {
|
||||
try {
|
||||
connection.queueEntry.callback(err);
|
||||
} catch (E) {
|
||||
this.logger.error(
|
||||
{
|
||||
err: E,
|
||||
tnx: 'callback',
|
||||
cid: connection.id
|
||||
},
|
||||
'Callback error for #%s: %s',
|
||||
connection.id,
|
||||
E.message
|
||||
);
|
||||
}
|
||||
connection.queueEntry = false;
|
||||
}
|
||||
|
||||
// remove the erroneus connection from connections list
|
||||
this._removeConnection(connection);
|
||||
|
||||
this._continueProcessing();
|
||||
});
|
||||
|
||||
connection.once('close', () => {
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'connection',
|
||||
cid: connection.id,
|
||||
action: 'closed'
|
||||
},
|
||||
'Connection #%s was closed',
|
||||
connection.id
|
||||
);
|
||||
|
||||
this._removeConnection(connection);
|
||||
|
||||
if (connection.queueEntry) {
|
||||
// If the connection closed when sending, add the message to the queue again
|
||||
// if max number of requeues is not reached yet
|
||||
// Note that we must wait a bit.. because the callback of the 'error' handler might be called
|
||||
// in the next event loop
|
||||
setTimeout(() => {
|
||||
if (connection.queueEntry) {
|
||||
if (this._shouldRequeuOnConnectionClose(connection.queueEntry)) {
|
||||
this._requeueEntryOnConnectionClose(connection);
|
||||
} else {
|
||||
this._failDeliveryOnConnectionClose(connection);
|
||||
}
|
||||
}
|
||||
this._continueProcessing();
|
||||
}, 50);
|
||||
} else {
|
||||
this._continueProcessing();
|
||||
}
|
||||
});
|
||||
|
||||
this._connections.push(connection);
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
_shouldRequeuOnConnectionClose(queueEntry) {
|
||||
if (this.options.maxRequeues === undefined || this.options.maxRequeues < 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return queueEntry.requeueAttempts < this.options.maxRequeues;
|
||||
}
|
||||
|
||||
_failDeliveryOnConnectionClose(connection) {
|
||||
if (connection.queueEntry && connection.queueEntry.callback) {
|
||||
try {
|
||||
connection.queueEntry.callback(new Error('Reached maximum number of retries after connection was closed'));
|
||||
} catch (E) {
|
||||
this.logger.error(
|
||||
{
|
||||
err: E,
|
||||
tnx: 'callback',
|
||||
messageId: connection.queueEntry.messageId,
|
||||
cid: connection.id
|
||||
},
|
||||
'Callback error for #%s: %s',
|
||||
connection.id,
|
||||
E.message
|
||||
);
|
||||
}
|
||||
connection.queueEntry = false;
|
||||
}
|
||||
}
|
||||
|
||||
_requeueEntryOnConnectionClose(connection) {
|
||||
connection.queueEntry.requeueAttempts = connection.queueEntry.requeueAttempts + 1;
|
||||
this.logger.debug(
|
||||
{
|
||||
tnx: 'pool',
|
||||
cid: connection.id,
|
||||
messageId: connection.queueEntry.messageId,
|
||||
action: 'requeue'
|
||||
},
|
||||
'Re-queued message <%s> for #%s. Attempt: #%s',
|
||||
connection.queueEntry.messageId,
|
||||
connection.id,
|
||||
connection.queueEntry.requeueAttempts
|
||||
);
|
||||
this._queue.unshift(connection.queueEntry);
|
||||
connection.queueEntry = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue to process message if the pool hasn't closed
|
||||
*/
|
||||
_continueProcessing() {
|
||||
if (this._closed) {
|
||||
this.close();
|
||||
} else {
|
||||
setTimeout(() => this._processMessages(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove resource from pool
|
||||
*
|
||||
* @param {Object} connection The PoolResource to remove
|
||||
*/
|
||||
_removeConnection(connection) {
|
||||
let index = this._connections.indexOf(connection);
|
||||
|
||||
if (index !== -1) {
|
||||
this._connections.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if connections have hit current rate limit and if so, queues the availability callback
|
||||
*
|
||||
* @param {Function} callback Callback function to run once rate limiter has been cleared
|
||||
*/
|
||||
_checkRateLimit(callback) {
|
||||
if (!this._rateLimit.limit) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
let now = Date.now();
|
||||
|
||||
if (this._rateLimit.counter < this._rateLimit.limit) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
this._rateLimit.waiting.push(callback);
|
||||
|
||||
if (this._rateLimit.checkpoint <= now - this._rateLimit.delta) {
|
||||
return this._clearRateLimit();
|
||||
} else if (!this._rateLimit.timeout) {
|
||||
this._rateLimit.timeout = setTimeout(() => this._clearRateLimit(), this._rateLimit.delta - (now - this._rateLimit.checkpoint));
|
||||
this._rateLimit.checkpoint = now;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears current rate limit limitation and runs paused callback
|
||||
*/
|
||||
_clearRateLimit() {
|
||||
clearTimeout(this._rateLimit.timeout);
|
||||
this._rateLimit.timeout = null;
|
||||
this._rateLimit.counter = 0;
|
||||
this._rateLimit.checkpoint = false;
|
||||
|
||||
// resume all paused connections
|
||||
while (this._rateLimit.waiting.length) {
|
||||
let cb = this._rateLimit.waiting.shift();
|
||||
setImmediate(cb);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there are free slots in the queue
|
||||
*/
|
||||
isIdle() {
|
||||
return this.idling;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies SMTP configuration
|
||||
*
|
||||
* @param {Function} callback Callback function
|
||||
*/
|
||||
verify(callback) {
|
||||
let promise;
|
||||
|
||||
if (!callback) {
|
||||
promise = new Promise((resolve, reject) => {
|
||||
callback = shared.callbackPromise(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
let auth = new PoolResource(this).auth;
|
||||
|
||||
this.getSocket(this.options, (err, socketOptions) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let options = this.options;
|
||||
if (socketOptions && socketOptions.connection) {
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'proxy',
|
||||
remoteAddress: socketOptions.connection.remoteAddress,
|
||||
remotePort: socketOptions.connection.remotePort,
|
||||
destHost: options.host || '',
|
||||
destPort: options.port || '',
|
||||
action: 'connected'
|
||||
},
|
||||
'Using proxied socket from %s:%s to %s:%s',
|
||||
socketOptions.connection.remoteAddress,
|
||||
socketOptions.connection.remotePort,
|
||||
options.host || '',
|
||||
options.port || ''
|
||||
);
|
||||
options = shared.assign(false, options);
|
||||
Object.keys(socketOptions).forEach(key => {
|
||||
options[key] = socketOptions[key];
|
||||
});
|
||||
}
|
||||
|
||||
let connection = new SMTPConnection(options);
|
||||
let returned = false;
|
||||
|
||||
connection.once('error', err => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
connection.close();
|
||||
return callback(err);
|
||||
});
|
||||
|
||||
connection.once('end', () => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
return callback(new Error('Connection closed'));
|
||||
});
|
||||
|
||||
let finalize = () => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
connection.quit();
|
||||
return callback(null, true);
|
||||
};
|
||||
|
||||
connection.connect(() => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (auth && (connection.allowsAuth || options.forceAuth)) {
|
||||
connection.login(auth, err => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
returned = true;
|
||||
connection.close();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
finalize();
|
||||
});
|
||||
} else if (!auth && connection.allowsAuth && options.forceAuth) {
|
||||
let err = new Error('Authentication info was not provided');
|
||||
err.code = 'NoAuth';
|
||||
|
||||
returned = true;
|
||||
connection.close();
|
||||
return callback(err);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
// expose to the world
|
||||
module.exports = SMTPPool;
|
253
backend/apis/nodejs/node_modules/nodemailer/lib/smtp-pool/pool-resource.js
generated
vendored
Normal file
253
backend/apis/nodejs/node_modules/nodemailer/lib/smtp-pool/pool-resource.js
generated
vendored
Normal file
@ -0,0 +1,253 @@
|
||||
'use strict';
|
||||
|
||||
const SMTPConnection = require('../smtp-connection');
|
||||
const assign = require('../shared').assign;
|
||||
const XOAuth2 = require('../xoauth2');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
/**
|
||||
* Creates an element for the pool
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} options SMTPPool instance
|
||||
*/
|
||||
class PoolResource extends EventEmitter {
|
||||
constructor(pool) {
|
||||
super();
|
||||
|
||||
this.pool = pool;
|
||||
this.options = pool.options;
|
||||
this.logger = this.pool.logger;
|
||||
|
||||
if (this.options.auth) {
|
||||
switch ((this.options.auth.type || '').toString().toUpperCase()) {
|
||||
case 'OAUTH2': {
|
||||
let oauth2 = new XOAuth2(this.options.auth, this.logger);
|
||||
oauth2.provisionCallback = (this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
|
||||
this.auth = {
|
||||
type: 'OAUTH2',
|
||||
user: this.options.auth.user,
|
||||
oauth2,
|
||||
method: 'XOAUTH2'
|
||||
};
|
||||
oauth2.on('token', token => this.pool.mailer.emit('token', token));
|
||||
oauth2.on('error', err => this.emit('error', err));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (!this.options.auth.user && !this.options.auth.pass) {
|
||||
break;
|
||||
}
|
||||
this.auth = {
|
||||
type: (this.options.auth.type || '').toString().toUpperCase() || 'LOGIN',
|
||||
user: this.options.auth.user,
|
||||
credentials: {
|
||||
user: this.options.auth.user || '',
|
||||
pass: this.options.auth.pass,
|
||||
options: this.options.auth.options
|
||||
},
|
||||
method: (this.options.auth.method || '').trim().toUpperCase() || this.options.authMethod || false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this._connection = false;
|
||||
this._connected = false;
|
||||
|
||||
this.messages = 0;
|
||||
this.available = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a connection to the SMTP server
|
||||
*
|
||||
* @param {Function} callback Callback function to run once the connection is established or failed
|
||||
*/
|
||||
connect(callback) {
|
||||
this.pool.getSocket(this.options, (err, socketOptions) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let returned = false;
|
||||
let options = this.options;
|
||||
if (socketOptions && socketOptions.connection) {
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'proxy',
|
||||
remoteAddress: socketOptions.connection.remoteAddress,
|
||||
remotePort: socketOptions.connection.remotePort,
|
||||
destHost: options.host || '',
|
||||
destPort: options.port || '',
|
||||
action: 'connected'
|
||||
},
|
||||
'Using proxied socket from %s:%s to %s:%s',
|
||||
socketOptions.connection.remoteAddress,
|
||||
socketOptions.connection.remotePort,
|
||||
options.host || '',
|
||||
options.port || ''
|
||||
);
|
||||
|
||||
options = assign(false, options);
|
||||
Object.keys(socketOptions).forEach(key => {
|
||||
options[key] = socketOptions[key];
|
||||
});
|
||||
}
|
||||
|
||||
this.connection = new SMTPConnection(options);
|
||||
|
||||
this.connection.once('error', err => {
|
||||
this.emit('error', err);
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
return callback(err);
|
||||
});
|
||||
|
||||
this.connection.once('end', () => {
|
||||
this.close();
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
|
||||
let timer = setTimeout(() => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
// still have not returned, this means we have an unexpected connection close
|
||||
let err = new Error('Unexpected socket close');
|
||||
if (this.connection && this.connection._socket && this.connection._socket.upgrading) {
|
||||
// starttls connection errors
|
||||
err.code = 'ETLS';
|
||||
}
|
||||
callback(err);
|
||||
}, 1000);
|
||||
|
||||
try {
|
||||
timer.unref();
|
||||
} catch (E) {
|
||||
// Ignore. Happens on envs with non-node timer implementation
|
||||
}
|
||||
});
|
||||
|
||||
this.connection.connect(() => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.auth && (this.connection.allowsAuth || options.forceAuth)) {
|
||||
this.connection.login(this.auth, err => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
|
||||
if (err) {
|
||||
this.connection.close();
|
||||
this.emit('error', err);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
this._connected = true;
|
||||
callback(null, true);
|
||||
});
|
||||
} else {
|
||||
returned = true;
|
||||
this._connected = true;
|
||||
return callback(null, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an e-mail to be sent using the selected settings
|
||||
*
|
||||
* @param {Object} mail Mail object
|
||||
* @param {Function} callback Callback function
|
||||
*/
|
||||
send(mail, callback) {
|
||||
if (!this._connected) {
|
||||
return this.connect(err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return this.send(mail, callback);
|
||||
});
|
||||
}
|
||||
|
||||
let envelope = mail.message.getEnvelope();
|
||||
let messageId = mail.message.messageId();
|
||||
|
||||
let recipients = [].concat(envelope.to || []);
|
||||
if (recipients.length > 3) {
|
||||
recipients.push('...and ' + recipients.splice(2).length + ' more');
|
||||
}
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'send',
|
||||
messageId,
|
||||
cid: this.id
|
||||
},
|
||||
'Sending message %s using #%s to <%s>',
|
||||
messageId,
|
||||
this.id,
|
||||
recipients.join(', ')
|
||||
);
|
||||
|
||||
if (mail.data.dsn) {
|
||||
envelope.dsn = mail.data.dsn;
|
||||
}
|
||||
|
||||
this.connection.send(envelope, mail.message.createReadStream(), (err, info) => {
|
||||
this.messages++;
|
||||
|
||||
if (err) {
|
||||
this.connection.close();
|
||||
this.emit('error', err);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
info.envelope = {
|
||||
from: envelope.from,
|
||||
to: envelope.to
|
||||
};
|
||||
info.messageId = messageId;
|
||||
|
||||
setImmediate(() => {
|
||||
let err;
|
||||
if (this.messages >= this.options.maxMessages) {
|
||||
err = new Error('Resource exhausted');
|
||||
err.code = 'EMAXLIMIT';
|
||||
this.connection.close();
|
||||
this.emit('error', err);
|
||||
} else {
|
||||
this.pool._checkRateLimit(() => {
|
||||
this.available = true;
|
||||
this.emit('available');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
callback(null, info);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the connection
|
||||
*/
|
||||
close() {
|
||||
this._connected = false;
|
||||
if (this.auth && this.auth.oauth2) {
|
||||
this.auth.oauth2.removeAllListeners();
|
||||
}
|
||||
if (this.connection) {
|
||||
this.connection.close();
|
||||
}
|
||||
this.emit('close');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PoolResource;
|
416
backend/apis/nodejs/node_modules/nodemailer/lib/smtp-transport/index.js
generated
vendored
Normal file
416
backend/apis/nodejs/node_modules/nodemailer/lib/smtp-transport/index.js
generated
vendored
Normal file
@ -0,0 +1,416 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const SMTPConnection = require('../smtp-connection');
|
||||
const wellKnown = require('../well-known');
|
||||
const shared = require('../shared');
|
||||
const XOAuth2 = require('../xoauth2');
|
||||
const packageData = require('../../package.json');
|
||||
|
||||
/**
|
||||
* Creates a SMTP transport object for Nodemailer
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} options Connection options
|
||||
*/
|
||||
class SMTPTransport extends EventEmitter {
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
options = options || {};
|
||||
|
||||
if (typeof options === 'string') {
|
||||
options = {
|
||||
url: options
|
||||
};
|
||||
}
|
||||
|
||||
let urlData;
|
||||
let service = options.service;
|
||||
|
||||
if (typeof options.getSocket === 'function') {
|
||||
this.getSocket = options.getSocket;
|
||||
}
|
||||
|
||||
if (options.url) {
|
||||
urlData = shared.parseConnectionUrl(options.url);
|
||||
service = service || urlData.service;
|
||||
}
|
||||
|
||||
this.options = shared.assign(
|
||||
false, // create new object
|
||||
options, // regular options
|
||||
urlData, // url options
|
||||
service && wellKnown(service) // wellknown options
|
||||
);
|
||||
|
||||
this.logger = shared.getLogger(this.options, {
|
||||
component: this.options.component || 'smtp-transport'
|
||||
});
|
||||
|
||||
// temporary object
|
||||
let connection = new SMTPConnection(this.options);
|
||||
|
||||
this.name = 'SMTP';
|
||||
this.version = packageData.version + '[client:' + connection.version + ']';
|
||||
|
||||
if (this.options.auth) {
|
||||
this.auth = this.getAuth({});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder function for creating proxy sockets. This method immediatelly returns
|
||||
* without a socket
|
||||
*
|
||||
* @param {Object} options Connection options
|
||||
* @param {Function} callback Callback function to run with the socket keys
|
||||
*/
|
||||
getSocket(options, callback) {
|
||||
// return immediatelly
|
||||
return setImmediate(() => callback(null, false));
|
||||
}
|
||||
|
||||
getAuth(authOpts) {
|
||||
if (!authOpts) {
|
||||
return this.auth;
|
||||
}
|
||||
|
||||
let hasAuth = false;
|
||||
let authData = {};
|
||||
|
||||
if (this.options.auth && typeof this.options.auth === 'object') {
|
||||
Object.keys(this.options.auth).forEach(key => {
|
||||
hasAuth = true;
|
||||
authData[key] = this.options.auth[key];
|
||||
});
|
||||
}
|
||||
|
||||
if (authOpts && typeof authOpts === 'object') {
|
||||
Object.keys(authOpts).forEach(key => {
|
||||
hasAuth = true;
|
||||
authData[key] = authOpts[key];
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasAuth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ((authData.type || '').toString().toUpperCase()) {
|
||||
case 'OAUTH2': {
|
||||
if (!authData.service && !authData.user) {
|
||||
return false;
|
||||
}
|
||||
let oauth2 = new XOAuth2(authData, this.logger);
|
||||
oauth2.provisionCallback = (this.mailer && this.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
|
||||
oauth2.on('token', token => this.mailer.emit('token', token));
|
||||
oauth2.on('error', err => this.emit('error', err));
|
||||
return {
|
||||
type: 'OAUTH2',
|
||||
user: authData.user,
|
||||
oauth2,
|
||||
method: 'XOAUTH2'
|
||||
};
|
||||
}
|
||||
default:
|
||||
return {
|
||||
type: (authData.type || '').toString().toUpperCase() || 'LOGIN',
|
||||
user: authData.user,
|
||||
credentials: {
|
||||
user: authData.user || '',
|
||||
pass: authData.pass,
|
||||
options: authData.options
|
||||
},
|
||||
method: (authData.method || '').trim().toUpperCase() || this.options.authMethod || false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an e-mail using the selected settings
|
||||
*
|
||||
* @param {Object} mail Mail object
|
||||
* @param {Function} callback Callback function
|
||||
*/
|
||||
send(mail, callback) {
|
||||
this.getSocket(this.options, (err, socketOptions) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let returned = false;
|
||||
let options = this.options;
|
||||
if (socketOptions && socketOptions.connection) {
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'proxy',
|
||||
remoteAddress: socketOptions.connection.remoteAddress,
|
||||
remotePort: socketOptions.connection.remotePort,
|
||||
destHost: options.host || '',
|
||||
destPort: options.port || '',
|
||||
action: 'connected'
|
||||
},
|
||||
'Using proxied socket from %s:%s to %s:%s',
|
||||
socketOptions.connection.remoteAddress,
|
||||
socketOptions.connection.remotePort,
|
||||
options.host || '',
|
||||
options.port || ''
|
||||
);
|
||||
|
||||
// only copy options if we need to modify it
|
||||
options = shared.assign(false, options);
|
||||
Object.keys(socketOptions).forEach(key => {
|
||||
options[key] = socketOptions[key];
|
||||
});
|
||||
}
|
||||
|
||||
let connection = new SMTPConnection(options);
|
||||
|
||||
connection.once('error', err => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
connection.close();
|
||||
return callback(err);
|
||||
});
|
||||
|
||||
connection.once('end', () => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
|
||||
let timer = setTimeout(() => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
// still have not returned, this means we have an unexpected connection close
|
||||
let err = new Error('Unexpected socket close');
|
||||
if (connection && connection._socket && connection._socket.upgrading) {
|
||||
// starttls connection errors
|
||||
err.code = 'ETLS';
|
||||
}
|
||||
callback(err);
|
||||
}, 1000);
|
||||
|
||||
try {
|
||||
timer.unref();
|
||||
} catch (E) {
|
||||
// Ignore. Happens on envs with non-node timer implementation
|
||||
}
|
||||
});
|
||||
|
||||
let sendMessage = () => {
|
||||
let envelope = mail.message.getEnvelope();
|
||||
let messageId = mail.message.messageId();
|
||||
|
||||
let recipients = [].concat(envelope.to || []);
|
||||
if (recipients.length > 3) {
|
||||
recipients.push('...and ' + recipients.splice(2).length + ' more');
|
||||
}
|
||||
|
||||
if (mail.data.dsn) {
|
||||
envelope.dsn = mail.data.dsn;
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'send',
|
||||
messageId
|
||||
},
|
||||
'Sending message %s to <%s>',
|
||||
messageId,
|
||||
recipients.join(', ')
|
||||
);
|
||||
|
||||
connection.send(envelope, mail.message.createReadStream(), (err, info) => {
|
||||
returned = true;
|
||||
connection.close();
|
||||
if (err) {
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'send'
|
||||
},
|
||||
'Send error for %s: %s',
|
||||
messageId,
|
||||
err.message
|
||||
);
|
||||
return callback(err);
|
||||
}
|
||||
info.envelope = {
|
||||
from: envelope.from,
|
||||
to: envelope.to
|
||||
};
|
||||
info.messageId = messageId;
|
||||
try {
|
||||
return callback(null, info);
|
||||
} catch (E) {
|
||||
this.logger.error(
|
||||
{
|
||||
err: E,
|
||||
tnx: 'callback'
|
||||
},
|
||||
'Callback error for %s: %s',
|
||||
messageId,
|
||||
E.message
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
connection.connect(() => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
|
||||
let auth = this.getAuth(mail.data.auth);
|
||||
|
||||
if (auth && (connection.allowsAuth || options.forceAuth)) {
|
||||
connection.login(auth, err => {
|
||||
if (auth && auth !== this.auth && auth.oauth2) {
|
||||
auth.oauth2.removeAllListeners();
|
||||
}
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
returned = true;
|
||||
connection.close();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
sendMessage();
|
||||
});
|
||||
} else {
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies SMTP configuration
|
||||
*
|
||||
* @param {Function} callback Callback function
|
||||
*/
|
||||
verify(callback) {
|
||||
let promise;
|
||||
|
||||
if (!callback) {
|
||||
promise = new Promise((resolve, reject) => {
|
||||
callback = shared.callbackPromise(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
this.getSocket(this.options, (err, socketOptions) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let options = this.options;
|
||||
if (socketOptions && socketOptions.connection) {
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'proxy',
|
||||
remoteAddress: socketOptions.connection.remoteAddress,
|
||||
remotePort: socketOptions.connection.remotePort,
|
||||
destHost: options.host || '',
|
||||
destPort: options.port || '',
|
||||
action: 'connected'
|
||||
},
|
||||
'Using proxied socket from %s:%s to %s:%s',
|
||||
socketOptions.connection.remoteAddress,
|
||||
socketOptions.connection.remotePort,
|
||||
options.host || '',
|
||||
options.port || ''
|
||||
);
|
||||
|
||||
options = shared.assign(false, options);
|
||||
Object.keys(socketOptions).forEach(key => {
|
||||
options[key] = socketOptions[key];
|
||||
});
|
||||
}
|
||||
|
||||
let connection = new SMTPConnection(options);
|
||||
let returned = false;
|
||||
|
||||
connection.once('error', err => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
connection.close();
|
||||
return callback(err);
|
||||
});
|
||||
|
||||
connection.once('end', () => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
return callback(new Error('Connection closed'));
|
||||
});
|
||||
|
||||
let finalize = () => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
connection.quit();
|
||||
return callback(null, true);
|
||||
};
|
||||
|
||||
connection.connect(() => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
|
||||
let authData = this.getAuth({});
|
||||
|
||||
if (authData && (connection.allowsAuth || options.forceAuth)) {
|
||||
connection.login(authData, err => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
returned = true;
|
||||
connection.close();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
finalize();
|
||||
});
|
||||
} else if (!authData && connection.allowsAuth && options.forceAuth) {
|
||||
let err = new Error('Authentication info was not provided');
|
||||
err.code = 'NoAuth';
|
||||
|
||||
returned = true;
|
||||
connection.close();
|
||||
return callback(err);
|
||||
} else {
|
||||
finalize();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases resources
|
||||
*/
|
||||
close() {
|
||||
if (this.auth && this.auth.oauth2) {
|
||||
this.auth.oauth2.removeAllListeners();
|
||||
}
|
||||
this.emit('close');
|
||||
}
|
||||
}
|
||||
|
||||
// expose to the world
|
||||
module.exports = SMTPTransport;
|
135
backend/apis/nodejs/node_modules/nodemailer/lib/stream-transport/index.js
generated
vendored
Normal file
135
backend/apis/nodejs/node_modules/nodemailer/lib/stream-transport/index.js
generated
vendored
Normal file
@ -0,0 +1,135 @@
|
||||
'use strict';
|
||||
|
||||
const packageData = require('../../package.json');
|
||||
const shared = require('../shared');
|
||||
|
||||
/**
|
||||
* Generates a Transport object for streaming
|
||||
*
|
||||
* Possible options can be the following:
|
||||
*
|
||||
* * **buffer** if true, then returns the message as a Buffer object instead of a stream
|
||||
* * **newline** either 'windows' or 'unix'
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} optional config parameter
|
||||
*/
|
||||
class StreamTransport {
|
||||
constructor(options) {
|
||||
options = options || {};
|
||||
|
||||
this.options = options || {};
|
||||
|
||||
this.name = 'StreamTransport';
|
||||
this.version = packageData.version;
|
||||
|
||||
this.logger = shared.getLogger(this.options, {
|
||||
component: this.options.component || 'stream-transport'
|
||||
});
|
||||
|
||||
this.winbreak = ['win', 'windows', 'dos', '\r\n'].includes((options.newline || '').toString().toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles a mailcomposer message and forwards it to handler that sends it
|
||||
*
|
||||
* @param {Object} emailMessage MailComposer object
|
||||
* @param {Function} callback Callback function to run when the sending is completed
|
||||
*/
|
||||
send(mail, done) {
|
||||
// We probably need this in the output
|
||||
mail.message.keepBcc = true;
|
||||
|
||||
let envelope = mail.data.envelope || mail.message.getEnvelope();
|
||||
let messageId = mail.message.messageId();
|
||||
|
||||
let recipients = [].concat(envelope.to || []);
|
||||
if (recipients.length > 3) {
|
||||
recipients.push('...and ' + recipients.splice(2).length + ' more');
|
||||
}
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'send',
|
||||
messageId
|
||||
},
|
||||
'Sending message %s to <%s> using %s line breaks',
|
||||
messageId,
|
||||
recipients.join(', '),
|
||||
this.winbreak ? '<CR><LF>' : '<LF>'
|
||||
);
|
||||
|
||||
setImmediate(() => {
|
||||
let stream;
|
||||
|
||||
try {
|
||||
stream = mail.message.createReadStream();
|
||||
} catch (E) {
|
||||
this.logger.error(
|
||||
{
|
||||
err: E,
|
||||
tnx: 'send',
|
||||
messageId
|
||||
},
|
||||
'Creating send stream failed for %s. %s',
|
||||
messageId,
|
||||
E.message
|
||||
);
|
||||
return done(E);
|
||||
}
|
||||
|
||||
if (!this.options.buffer) {
|
||||
stream.once('error', err => {
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'send',
|
||||
messageId
|
||||
},
|
||||
'Failed creating message for %s. %s',
|
||||
messageId,
|
||||
err.message
|
||||
);
|
||||
});
|
||||
return done(null, {
|
||||
envelope: mail.data.envelope || mail.message.getEnvelope(),
|
||||
messageId,
|
||||
message: stream
|
||||
});
|
||||
}
|
||||
|
||||
let chunks = [];
|
||||
let chunklen = 0;
|
||||
stream.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = stream.read()) !== null) {
|
||||
chunks.push(chunk);
|
||||
chunklen += chunk.length;
|
||||
}
|
||||
});
|
||||
|
||||
stream.once('error', err => {
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'send',
|
||||
messageId
|
||||
},
|
||||
'Failed creating message for %s. %s',
|
||||
messageId,
|
||||
err.message
|
||||
);
|
||||
return done(err);
|
||||
});
|
||||
|
||||
stream.on('end', () =>
|
||||
done(null, {
|
||||
envelope: mail.data.envelope || mail.message.getEnvelope(),
|
||||
messageId,
|
||||
message: Buffer.concat(chunks, chunklen)
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StreamTransport;
|
47
backend/apis/nodejs/node_modules/nodemailer/lib/well-known/index.js
generated
vendored
Normal file
47
backend/apis/nodejs/node_modules/nodemailer/lib/well-known/index.js
generated
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
'use strict';
|
||||
|
||||
const services = require('./services.json');
|
||||
const normalized = {};
|
||||
|
||||
Object.keys(services).forEach(key => {
|
||||
let service = services[key];
|
||||
|
||||
normalized[normalizeKey(key)] = normalizeService(service);
|
||||
|
||||
[].concat(service.aliases || []).forEach(alias => {
|
||||
normalized[normalizeKey(alias)] = normalizeService(service);
|
||||
});
|
||||
|
||||
[].concat(service.domains || []).forEach(domain => {
|
||||
normalized[normalizeKey(domain)] = normalizeService(service);
|
||||
});
|
||||
});
|
||||
|
||||
function normalizeKey(key) {
|
||||
return key.replace(/[^a-zA-Z0-9.-]/g, '').toLowerCase();
|
||||
}
|
||||
|
||||
function normalizeService(service) {
|
||||
let filter = ['domains', 'aliases'];
|
||||
let response = {};
|
||||
|
||||
Object.keys(service).forEach(key => {
|
||||
if (filter.indexOf(key) < 0) {
|
||||
response[key] = service[key];
|
||||
}
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves SMTP config for given key. Key can be a name (like 'Gmail'), alias (like 'Google Mail') or
|
||||
* an email address (like 'test@googlemail.com').
|
||||
*
|
||||
* @param {String} key [description]
|
||||
* @returns {Object} SMTP config or false if not found
|
||||
*/
|
||||
module.exports = function (key) {
|
||||
key = normalizeKey(key.split('@').pop());
|
||||
return normalized[key] || false;
|
||||
};
|
370
backend/apis/nodejs/node_modules/nodemailer/lib/well-known/services.json
generated
vendored
Normal file
370
backend/apis/nodejs/node_modules/nodemailer/lib/well-known/services.json
generated
vendored
Normal file
@ -0,0 +1,370 @@
|
||||
{
|
||||
"1und1": {
|
||||
"host": "smtp.1und1.de",
|
||||
"port": 465,
|
||||
"secure": true,
|
||||
"authMethod": "LOGIN"
|
||||
},
|
||||
|
||||
"Aliyun": {
|
||||
"domains": ["aliyun.com"],
|
||||
"host": "smtp.aliyun.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"AOL": {
|
||||
"domains": ["aol.com"],
|
||||
"host": "smtp.aol.com",
|
||||
"port": 587
|
||||
},
|
||||
|
||||
"Bluewin": {
|
||||
"host": "smtpauths.bluewin.ch",
|
||||
"domains": ["bluewin.ch"],
|
||||
"port": 465
|
||||
},
|
||||
|
||||
"DebugMail": {
|
||||
"host": "debugmail.io",
|
||||
"port": 25
|
||||
},
|
||||
|
||||
"DynectEmail": {
|
||||
"aliases": ["Dynect"],
|
||||
"host": "smtp.dynect.net",
|
||||
"port": 25
|
||||
},
|
||||
|
||||
"Ethereal": {
|
||||
"aliases": ["ethereal.email"],
|
||||
"host": "smtp.ethereal.email",
|
||||
"port": 587
|
||||
},
|
||||
|
||||
"FastMail": {
|
||||
"domains": ["fastmail.fm"],
|
||||
"host": "smtp.fastmail.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"Forward Email": {
|
||||
"aliases": ["FE", "ForwardEmail"],
|
||||
"domains": ["forwardemail.net"],
|
||||
"host": "smtp.forwardemail.net",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"Feishu Mail": {
|
||||
"aliases": ["Feishu", "FeishuMail"],
|
||||
"domains": ["www.feishu.cn"],
|
||||
"host": "smtp.feishu.cn",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"GandiMail": {
|
||||
"aliases": ["Gandi", "Gandi Mail"],
|
||||
"host": "mail.gandi.net",
|
||||
"port": 587
|
||||
},
|
||||
|
||||
"Gmail": {
|
||||
"aliases": ["Google Mail"],
|
||||
"domains": ["gmail.com", "googlemail.com"],
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"Godaddy": {
|
||||
"host": "smtpout.secureserver.net",
|
||||
"port": 25
|
||||
},
|
||||
|
||||
"GodaddyAsia": {
|
||||
"host": "smtp.asia.secureserver.net",
|
||||
"port": 25
|
||||
},
|
||||
|
||||
"GodaddyEurope": {
|
||||
"host": "smtp.europe.secureserver.net",
|
||||
"port": 25
|
||||
},
|
||||
|
||||
"hot.ee": {
|
||||
"host": "mail.hot.ee"
|
||||
},
|
||||
|
||||
"Hotmail": {
|
||||
"aliases": ["Outlook", "Outlook.com", "Hotmail.com"],
|
||||
"domains": ["hotmail.com", "outlook.com"],
|
||||
"host": "smtp-mail.outlook.com",
|
||||
"port": 587
|
||||
},
|
||||
|
||||
"iCloud": {
|
||||
"aliases": ["Me", "Mac"],
|
||||
"domains": ["me.com", "mac.com"],
|
||||
"host": "smtp.mail.me.com",
|
||||
"port": 587
|
||||
},
|
||||
|
||||
"Infomaniak": {
|
||||
"host": "mail.infomaniak.com",
|
||||
"domains": ["ik.me", "ikmail.com", "etik.com"],
|
||||
"port": 587
|
||||
},
|
||||
"Loopia": {
|
||||
"host": "mailcluster.loopia.se",
|
||||
"port": 465
|
||||
},
|
||||
"mail.ee": {
|
||||
"host": "smtp.mail.ee"
|
||||
},
|
||||
|
||||
"Mail.ru": {
|
||||
"host": "smtp.mail.ru",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"Mailcatch.app": {
|
||||
"host": "sandbox-smtp.mailcatch.app",
|
||||
"port": 2525
|
||||
},
|
||||
|
||||
"Maildev": {
|
||||
"port": 1025,
|
||||
"ignoreTLS": true
|
||||
},
|
||||
|
||||
"Mailgun": {
|
||||
"host": "smtp.mailgun.org",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"Mailjet": {
|
||||
"host": "in.mailjet.com",
|
||||
"port": 587
|
||||
},
|
||||
|
||||
"Mailosaur": {
|
||||
"host": "mailosaur.io",
|
||||
"port": 25
|
||||
},
|
||||
|
||||
"Mailtrap": {
|
||||
"host": "live.smtp.mailtrap.io",
|
||||
"port": 587
|
||||
},
|
||||
|
||||
"Mandrill": {
|
||||
"host": "smtp.mandrillapp.com",
|
||||
"port": 587
|
||||
},
|
||||
|
||||
"Naver": {
|
||||
"host": "smtp.naver.com",
|
||||
"port": 587
|
||||
},
|
||||
|
||||
"One": {
|
||||
"host": "send.one.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"OpenMailBox": {
|
||||
"aliases": ["OMB", "openmailbox.org"],
|
||||
"host": "smtp.openmailbox.org",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"Outlook365": {
|
||||
"host": "smtp.office365.com",
|
||||
"port": 587,
|
||||
"secure": false
|
||||
},
|
||||
|
||||
"OhMySMTP": {
|
||||
"host": "smtp.ohmysmtp.com",
|
||||
"port": 587,
|
||||
"secure": false
|
||||
},
|
||||
|
||||
"Postmark": {
|
||||
"aliases": ["PostmarkApp"],
|
||||
"host": "smtp.postmarkapp.com",
|
||||
"port": 2525
|
||||
},
|
||||
|
||||
"Proton": {
|
||||
"aliases": ["ProtonMail", "Proton.me", "Protonmail.com", "Protonmail.ch"],
|
||||
"domains": ["proton.me", "protonmail.com", "pm.me", "protonmail.ch"],
|
||||
"host": "smtp.protonmail.ch",
|
||||
"port": 587,
|
||||
"requireTLS": true
|
||||
},
|
||||
|
||||
"qiye.aliyun": {
|
||||
"host": "smtp.mxhichina.com",
|
||||
"port": "465",
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"QQ": {
|
||||
"domains": ["qq.com"],
|
||||
"host": "smtp.qq.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"QQex": {
|
||||
"aliases": ["QQ Enterprise"],
|
||||
"domains": ["exmail.qq.com"],
|
||||
"host": "smtp.exmail.qq.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"SendCloud": {
|
||||
"host": "smtp.sendcloud.net",
|
||||
"port": 2525
|
||||
},
|
||||
|
||||
"SendGrid": {
|
||||
"host": "smtp.sendgrid.net",
|
||||
"port": 587
|
||||
},
|
||||
|
||||
"SendinBlue": {
|
||||
"aliases": ["Brevo"],
|
||||
"host": "smtp-relay.brevo.com",
|
||||
"port": 587
|
||||
},
|
||||
|
||||
"SendPulse": {
|
||||
"host": "smtp-pulse.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"SES": {
|
||||
"host": "email-smtp.us-east-1.amazonaws.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"SES-US-EAST-1": {
|
||||
"host": "email-smtp.us-east-1.amazonaws.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"SES-US-WEST-2": {
|
||||
"host": "email-smtp.us-west-2.amazonaws.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"SES-EU-WEST-1": {
|
||||
"host": "email-smtp.eu-west-1.amazonaws.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"SES-AP-SOUTH-1": {
|
||||
"host": "email-smtp.ap-south-1.amazonaws.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"SES-AP-NORTHEAST-1": {
|
||||
"host": "email-smtp.ap-northeast-1.amazonaws.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"SES-AP-NORTHEAST-2": {
|
||||
"host": "email-smtp.ap-northeast-2.amazonaws.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"SES-AP-NORTHEAST-3": {
|
||||
"host": "email-smtp.ap-northeast-3.amazonaws.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"SES-AP-SOUTHEAST-1": {
|
||||
"host": "email-smtp.ap-southeast-1.amazonaws.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"SES-AP-SOUTHEAST-2": {
|
||||
"host": "email-smtp.ap-southeast-2.amazonaws.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"Seznam": {
|
||||
"aliases": ["Seznam Email"],
|
||||
"domains": ["seznam.cz", "email.cz", "post.cz", "spoluzaci.cz"],
|
||||
"host": "smtp.seznam.cz",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"Sparkpost": {
|
||||
"aliases": ["SparkPost", "SparkPost Mail"],
|
||||
"domains": ["sparkpost.com"],
|
||||
"host": "smtp.sparkpostmail.com",
|
||||
"port": 587,
|
||||
"secure": false
|
||||
},
|
||||
|
||||
"Tipimail": {
|
||||
"host": "smtp.tipimail.com",
|
||||
"port": 587
|
||||
},
|
||||
|
||||
"Yahoo": {
|
||||
"domains": ["yahoo.com"],
|
||||
"host": "smtp.mail.yahoo.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"Yandex": {
|
||||
"domains": ["yandex.ru"],
|
||||
"host": "smtp.yandex.ru",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"Zoho": {
|
||||
"host": "smtp.zoho.com",
|
||||
"port": 465,
|
||||
"secure": true,
|
||||
"authMethod": "LOGIN"
|
||||
},
|
||||
|
||||
"126": {
|
||||
"host": "smtp.126.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"163": {
|
||||
"host": "smtp.163.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
}
|
||||
}
|
376
backend/apis/nodejs/node_modules/nodemailer/lib/xoauth2/index.js
generated
vendored
Normal file
376
backend/apis/nodejs/node_modules/nodemailer/lib/xoauth2/index.js
generated
vendored
Normal file
@ -0,0 +1,376 @@
|
||||
'use strict';
|
||||
|
||||
const Stream = require('stream').Stream;
|
||||
const nmfetch = require('../fetch');
|
||||
const crypto = require('crypto');
|
||||
const shared = require('../shared');
|
||||
|
||||
/**
|
||||
* XOAUTH2 access_token generator for Gmail.
|
||||
* Create client ID for web applications in Google API console to use it.
|
||||
* See Offline Access for receiving the needed refreshToken for an user
|
||||
* https://developers.google.com/accounts/docs/OAuth2WebServer#offline
|
||||
*
|
||||
* Usage for generating access tokens with a custom method using provisionCallback:
|
||||
* provisionCallback(user, renew, callback)
|
||||
* * user is the username to get the token for
|
||||
* * renew is a boolean that if true indicates that existing token failed and needs to be renewed
|
||||
* * callback is the callback to run with (error, accessToken [, expires])
|
||||
* * accessToken is a string
|
||||
* * expires is an optional expire time in milliseconds
|
||||
* If provisionCallback is used, then Nodemailer does not try to attempt generating the token by itself
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} options Client information for token generation
|
||||
* @param {String} options.user User e-mail address
|
||||
* @param {String} options.clientId Client ID value
|
||||
* @param {String} options.clientSecret Client secret value
|
||||
* @param {String} options.refreshToken Refresh token for an user
|
||||
* @param {String} options.accessUrl Endpoint for token generation, defaults to 'https://accounts.google.com/o/oauth2/token'
|
||||
* @param {String} options.accessToken An existing valid accessToken
|
||||
* @param {String} options.privateKey Private key for JSW
|
||||
* @param {Number} options.expires Optional Access Token expire time in ms
|
||||
* @param {Number} options.timeout Optional TTL for Access Token in seconds
|
||||
* @param {Function} options.provisionCallback Function to run when a new access token is required
|
||||
*/
|
||||
class XOAuth2 extends Stream {
|
||||
constructor(options, logger) {
|
||||
super();
|
||||
|
||||
this.options = options || {};
|
||||
|
||||
if (options && options.serviceClient) {
|
||||
if (!options.privateKey || !options.user) {
|
||||
setImmediate(() => this.emit('error', new Error('Options "privateKey" and "user" are required for service account!')));
|
||||
return;
|
||||
}
|
||||
|
||||
let serviceRequestTimeout = Math.min(Math.max(Number(this.options.serviceRequestTimeout) || 0, 0), 3600);
|
||||
this.options.serviceRequestTimeout = serviceRequestTimeout || 5 * 60;
|
||||
}
|
||||
|
||||
this.logger = shared.getLogger(
|
||||
{
|
||||
logger
|
||||
},
|
||||
{
|
||||
component: this.options.component || 'OAuth2'
|
||||
}
|
||||
);
|
||||
|
||||
this.provisionCallback = typeof this.options.provisionCallback === 'function' ? this.options.provisionCallback : false;
|
||||
|
||||
this.options.accessUrl = this.options.accessUrl || 'https://accounts.google.com/o/oauth2/token';
|
||||
this.options.customHeaders = this.options.customHeaders || {};
|
||||
this.options.customParams = this.options.customParams || {};
|
||||
|
||||
this.accessToken = this.options.accessToken || false;
|
||||
|
||||
if (this.options.expires && Number(this.options.expires)) {
|
||||
this.expires = this.options.expires;
|
||||
} else {
|
||||
let timeout = Math.max(Number(this.options.timeout) || 0, 0);
|
||||
this.expires = (timeout && Date.now() + timeout * 1000) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns or generates (if previous has expired) a XOAuth2 token
|
||||
*
|
||||
* @param {Boolean} renew If false then use cached access token (if available)
|
||||
* @param {Function} callback Callback function with error object and token string
|
||||
*/
|
||||
getToken(renew, callback) {
|
||||
if (!renew && this.accessToken && (!this.expires || this.expires > Date.now())) {
|
||||
return callback(null, this.accessToken);
|
||||
}
|
||||
|
||||
let generateCallback = (...args) => {
|
||||
if (args[0]) {
|
||||
this.logger.error(
|
||||
{
|
||||
err: args[0],
|
||||
tnx: 'OAUTH2',
|
||||
user: this.options.user,
|
||||
action: 'renew'
|
||||
},
|
||||
'Failed generating new Access Token for %s',
|
||||
this.options.user
|
||||
);
|
||||
} else {
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'OAUTH2',
|
||||
user: this.options.user,
|
||||
action: 'renew'
|
||||
},
|
||||
'Generated new Access Token for %s',
|
||||
this.options.user
|
||||
);
|
||||
}
|
||||
callback(...args);
|
||||
};
|
||||
|
||||
if (this.provisionCallback) {
|
||||
this.provisionCallback(this.options.user, !!renew, (err, accessToken, expires) => {
|
||||
if (!err && accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
this.expires = expires || 0;
|
||||
}
|
||||
generateCallback(err, accessToken);
|
||||
});
|
||||
} else {
|
||||
this.generateToken(generateCallback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates token values
|
||||
*
|
||||
* @param {String} accessToken New access token
|
||||
* @param {Number} timeout Access token lifetime in seconds
|
||||
*
|
||||
* Emits 'token': { user: User email-address, accessToken: the new accessToken, timeout: TTL in seconds}
|
||||
*/
|
||||
updateToken(accessToken, timeout) {
|
||||
this.accessToken = accessToken;
|
||||
timeout = Math.max(Number(timeout) || 0, 0);
|
||||
this.expires = (timeout && Date.now() + timeout * 1000) || 0;
|
||||
|
||||
this.emit('token', {
|
||||
user: this.options.user,
|
||||
accessToken: accessToken || '',
|
||||
expires: this.expires
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new XOAuth2 token with the credentials provided at initialization
|
||||
*
|
||||
* @param {Function} callback Callback function with error object and token string
|
||||
*/
|
||||
generateToken(callback) {
|
||||
let urlOptions;
|
||||
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 = {
|
||||
iss: this.options.serviceClient,
|
||||
scope: this.options.scope || 'https://mail.google.com/',
|
||||
sub: this.options.user,
|
||||
aud: this.options.accessUrl,
|
||||
iat,
|
||||
exp: iat + this.options.serviceRequestTimeout
|
||||
};
|
||||
let token;
|
||||
try {
|
||||
token = this.jwtSignRS256(tokenData);
|
||||
} catch (err) {
|
||||
return callback(new Error('Can\x27t generate token. Check your auth options'));
|
||||
}
|
||||
|
||||
urlOptions = {
|
||||
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||
assertion: token
|
||||
};
|
||||
|
||||
loggedUrlOptions = {
|
||||
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||
assertion: tokenData
|
||||
};
|
||||
} else {
|
||||
if (!this.options.refreshToken) {
|
||||
return callback(new Error('Can\x27t create new access token for user'));
|
||||
}
|
||||
|
||||
// web app - https://developers.google.com/identity/protocols/OAuth2WebServer
|
||||
urlOptions = {
|
||||
client_id: this.options.clientId || '',
|
||||
client_secret: this.options.clientSecret || '',
|
||||
refresh_token: this.options.refreshToken,
|
||||
grant_type: 'refresh_token'
|
||||
};
|
||||
|
||||
loggedUrlOptions = {
|
||||
client_id: this.options.clientId || '',
|
||||
client_secret: (this.options.clientSecret || '').substr(0, 6) + '...',
|
||||
refresh_token: (this.options.refreshToken || '').substr(0, 6) + '...',
|
||||
grant_type: 'refresh_token'
|
||||
};
|
||||
}
|
||||
|
||||
Object.keys(this.options.customParams).forEach(key => {
|
||||
urlOptions[key] = this.options.customParams[key];
|
||||
loggedUrlOptions[key] = this.options.customParams[key];
|
||||
});
|
||||
|
||||
this.logger.debug(
|
||||
{
|
||||
tnx: 'OAUTH2',
|
||||
user: this.options.user,
|
||||
action: 'generate'
|
||||
},
|
||||
'Requesting token using: %s',
|
||||
JSON.stringify(loggedUrlOptions)
|
||||
);
|
||||
|
||||
this.postRequest(this.options.accessUrl, urlOptions, this.options, (error, body) => {
|
||||
let data;
|
||||
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
try {
|
||||
data = JSON.parse(body.toString());
|
||||
} catch (E) {
|
||||
return callback(E);
|
||||
}
|
||||
|
||||
if (!data || typeof data !== 'object') {
|
||||
this.logger.debug(
|
||||
{
|
||||
tnx: 'OAUTH2',
|
||||
user: this.options.user,
|
||||
action: 'post'
|
||||
},
|
||||
'Response: %s',
|
||||
(body || '').toString()
|
||||
);
|
||||
return callback(new Error('Invalid authentication response'));
|
||||
}
|
||||
|
||||
let logData = {};
|
||||
Object.keys(data).forEach(key => {
|
||||
if (key !== 'access_token') {
|
||||
logData[key] = data[key];
|
||||
} else {
|
||||
logData[key] = (data[key] || '').toString().substr(0, 6) + '...';
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.debug(
|
||||
{
|
||||
tnx: 'OAUTH2',
|
||||
user: this.options.user,
|
||||
action: 'post'
|
||||
},
|
||||
'Response: %s',
|
||||
JSON.stringify(logData)
|
||||
);
|
||||
|
||||
if (data.error) {
|
||||
// Error Response : https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
let errorMessage = data.error;
|
||||
if (data.error_description) {
|
||||
errorMessage += ': ' + data.error_description;
|
||||
}
|
||||
if (data.error_uri) {
|
||||
errorMessage += ' (' + data.error_uri + ')';
|
||||
}
|
||||
return callback(new Error(errorMessage));
|
||||
}
|
||||
|
||||
if (data.access_token) {
|
||||
this.updateToken(data.access_token, data.expires_in);
|
||||
return callback(null, this.accessToken);
|
||||
}
|
||||
|
||||
return callback(new Error('No access token'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an access_token and user id into a base64 encoded XOAuth2 token
|
||||
*
|
||||
* @param {String} [accessToken] Access token string
|
||||
* @return {String} Base64 encoded token for IMAP or SMTP login
|
||||
*/
|
||||
buildXOAuth2Token(accessToken) {
|
||||
let authData = ['user=' + (this.options.user || ''), 'auth=Bearer ' + (accessToken || this.accessToken), '', ''];
|
||||
return Buffer.from(authData.join('\x01'), 'utf-8').toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom POST request handler.
|
||||
* This is only needed to keep paths short in Windows – usually this module
|
||||
* is a dependency of a dependency and if it tries to require something
|
||||
* like the request module the paths get way too long to handle for Windows.
|
||||
* As we do only a simple POST request we do not actually require complicated
|
||||
* logic support (no redirects, no nothing) anyway.
|
||||
*
|
||||
* @param {String} url Url to POST to
|
||||
* @param {String|Buffer} payload Payload to POST
|
||||
* @param {Function} callback Callback function with (err, buff)
|
||||
*/
|
||||
postRequest(url, payload, params, callback) {
|
||||
let returned = false;
|
||||
|
||||
let chunks = [];
|
||||
let chunklen = 0;
|
||||
|
||||
let req = nmfetch(url, {
|
||||
method: 'post',
|
||||
headers: params.customHeaders,
|
||||
body: payload,
|
||||
allowErrorResponse: true
|
||||
});
|
||||
|
||||
req.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = req.read()) !== null) {
|
||||
chunks.push(chunk);
|
||||
chunklen += chunk.length;
|
||||
}
|
||||
});
|
||||
|
||||
req.once('error', err => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
return callback(err);
|
||||
});
|
||||
|
||||
req.once('end', () => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
return callback(null, Buffer.concat(chunks, chunklen));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a buffer or a string into Base64url format
|
||||
*
|
||||
* @param {Buffer|String} data The data to convert
|
||||
* @return {String} The encoded string
|
||||
*/
|
||||
toBase64URL(data) {
|
||||
if (typeof data === 'string') {
|
||||
data = Buffer.from(data);
|
||||
}
|
||||
|
||||
return data
|
||||
.toString('base64')
|
||||
.replace(/[=]+/g, '') // remove '='s
|
||||
.replace(/\+/g, '-') // '+' → '-'
|
||||
.replace(/\//g, '_'); // '/' → '_'
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JSON Web Token signed with RS256 (SHA256 + RSA)
|
||||
*
|
||||
* @param {Object} payload The payload to include in the generated token
|
||||
* @return {String} The generated and signed token
|
||||
*/
|
||||
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);
|
||||
return payload + '.' + this.toBase64URL(signature);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = XOAuth2;
|
Reference in New Issue
Block a user