690 lines
18 KiB
JavaScript
690 lines
18 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Dependencies
|
|
*/
|
|
const EmailAddress = require('./email-address');
|
|
const Personalization = require('./personalization');
|
|
const toCamelCase = require('../helpers/to-camel-case');
|
|
const toSnakeCase = require('../helpers/to-snake-case');
|
|
const deepClone = require('../helpers/deep-clone');
|
|
const arrayToJSON = require('../helpers/array-to-json');
|
|
const { DYNAMIC_TEMPLATE_CHAR_WARNING } = require('../constants');
|
|
const {validateMailSettings, validateTrackingSettings} = require('../helpers/validate-settings');
|
|
|
|
/**
|
|
* Mail class
|
|
*/
|
|
class Mail {
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
constructor(data) {
|
|
|
|
//Initialize array and object properties
|
|
this.isDynamic = false;
|
|
this.hideWarnings = false;
|
|
this.personalizations = [];
|
|
this.attachments = [];
|
|
this.content = [];
|
|
this.categories = [];
|
|
this.headers = {};
|
|
this.sections = {};
|
|
this.customArgs = {};
|
|
this.trackingSettings = {};
|
|
this.mailSettings = {};
|
|
this.asm = {};
|
|
|
|
//Helper properties
|
|
this.substitutions = null;
|
|
this.substitutionWrappers = null;
|
|
this.dynamicTemplateData = null;
|
|
|
|
//Process data if given
|
|
if (data) {
|
|
this.fromData(data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build from data
|
|
*/
|
|
fromData(data) {
|
|
|
|
//Expecting object
|
|
if (typeof data !== 'object') {
|
|
throw new Error('Expecting object for Mail data');
|
|
}
|
|
|
|
//Convert to camel case to make it workable, making a copy to prevent
|
|
//changes to the original objects
|
|
data = deepClone(data);
|
|
data = toCamelCase(data, ['substitutions', 'dynamicTemplateData', 'customArgs', 'headers', 'sections']);
|
|
|
|
//Extract properties from data
|
|
const {
|
|
to, from, replyTo, cc, bcc, sendAt, subject, text, html, content,
|
|
templateId, personalizations, attachments, ipPoolName, batchId,
|
|
sections, headers, categories, category, customArgs, asm, mailSettings,
|
|
trackingSettings, substitutions, substitutionWrappers, dynamicTemplateData, isMultiple,
|
|
hideWarnings, replyToList,
|
|
} = data;
|
|
|
|
//Set data
|
|
this.setFrom(from);
|
|
this.setReplyTo(replyTo);
|
|
this.setSubject(subject);
|
|
this.setSendAt(sendAt);
|
|
this.setTemplateId(templateId);
|
|
this.setBatchId(batchId);
|
|
this.setIpPoolName(ipPoolName);
|
|
this.setAttachments(attachments);
|
|
this.setContent(content);
|
|
this.setSections(sections);
|
|
this.setHeaders(headers);
|
|
this.setCategories(category);
|
|
this.setCategories(categories);
|
|
this.setCustomArgs(customArgs);
|
|
this.setAsm(asm);
|
|
this.setMailSettings(mailSettings);
|
|
this.setTrackingSettings(trackingSettings);
|
|
this.setHideWarnings(hideWarnings);
|
|
this.setReplyToList(replyToList);
|
|
|
|
if (this.isDynamic) {
|
|
this.setDynamicTemplateData(dynamicTemplateData);
|
|
} else {
|
|
this.setSubstitutions(substitutions);
|
|
this.setSubstitutionWrappers(substitutionWrappers);
|
|
}
|
|
|
|
//Add contents from text/html properties
|
|
this.addTextContent(text);
|
|
this.addHtmlContent(html);
|
|
|
|
//Using "to" property for personalizations
|
|
if (personalizations) {
|
|
this.setPersonalizations(personalizations);
|
|
} else if (isMultiple && Array.isArray(to)) {
|
|
//Multiple individual emails
|
|
to.forEach(to => this.addTo(to, cc, bcc));
|
|
} else {
|
|
//Single email (possibly with multiple recipients in the to field)
|
|
this.addTo(to, cc, bcc);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set from email
|
|
*/
|
|
setFrom(from) {
|
|
if (this._checkProperty('from', from, [this._checkUndefined])) {
|
|
if (typeof from !== 'string' && typeof from.email !== 'string') {
|
|
throw new Error('String or address object expected for `from`');
|
|
}
|
|
this.from = EmailAddress.create(from);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set reply to
|
|
*/
|
|
setReplyTo(replyTo) {
|
|
if (this._checkProperty('replyTo', replyTo, [this._checkUndefined])) {
|
|
if (typeof replyTo !== 'string' && typeof replyTo.email !== 'string') {
|
|
throw new Error('String or address object expected for `replyTo`');
|
|
}
|
|
this.replyTo = EmailAddress.create(replyTo);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set subject
|
|
*/
|
|
setSubject(subject) {
|
|
this._setProperty('subject', subject, 'string');
|
|
}
|
|
|
|
/**
|
|
* Set send at
|
|
*/
|
|
setSendAt(sendAt) {
|
|
if (this._checkProperty('sendAt', sendAt, [this._checkUndefined, this._createCheckThatThrows(Number.isInteger, 'Integer expected for `sendAt`')])) {
|
|
this.sendAt = sendAt;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set template ID, also checks if the template is dynamic or legacy
|
|
*/
|
|
setTemplateId(templateId) {
|
|
if (this._setProperty('templateId', templateId, 'string')) {
|
|
if (templateId.indexOf('d-') === 0) {
|
|
this.isDynamic = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set batch ID
|
|
*/
|
|
setBatchId(batchId) {
|
|
this._setProperty('batchId', batchId, 'string');
|
|
}
|
|
|
|
/**
|
|
* Set IP pool name
|
|
*/
|
|
setIpPoolName(ipPoolName) {
|
|
this._setProperty('ipPoolName', ipPoolName, 'string');
|
|
}
|
|
|
|
/**
|
|
* Set ASM
|
|
*/
|
|
setAsm(asm) {
|
|
if (this._checkProperty('asm', asm, [this._checkUndefined, this._createTypeCheck('object')])) {
|
|
if (typeof asm.groupId !== 'number') {
|
|
throw new Error('Expected `asm` to include an integer in its `groupId` field');
|
|
}
|
|
if (asm.groupsToDisplay &&
|
|
(!Array.isArray(asm.groupsToDisplay) || !asm.groupsToDisplay.every(group => typeof group === 'number'))) {
|
|
throw new Error('Array of integers expected for `asm.groupsToDisplay`');
|
|
}
|
|
this.asm = asm;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set personalizations
|
|
*/
|
|
setPersonalizations(personalizations) {
|
|
if (!this._doArrayCheck('personalizations', personalizations)) {
|
|
return;
|
|
}
|
|
|
|
if (!personalizations.every(personalization => typeof personalization === 'object')) {
|
|
throw new Error('Array of objects expected for `personalizations`');
|
|
}
|
|
|
|
//Clear and use add helper to add one by one
|
|
this.personalizations = [];
|
|
personalizations
|
|
.forEach(personalization => this.addPersonalization(personalization));
|
|
}
|
|
|
|
/**
|
|
* Add personalization
|
|
*/
|
|
addPersonalization(personalization) {
|
|
//We should either send substitutions or dynamicTemplateData
|
|
//depending on the templateId
|
|
if (this.isDynamic && personalization.substitutions) {
|
|
delete personalization.substitutions;
|
|
} else if (!this.isDynamic && personalization.dynamicTemplateData) {
|
|
delete personalization.dynamicTemplateData;
|
|
}
|
|
|
|
//Convert to class if needed
|
|
if (!(personalization instanceof Personalization)) {
|
|
personalization = new Personalization(personalization);
|
|
}
|
|
|
|
//If this is dynamic, set dynamicTemplateData, or set substitutions
|
|
if (this.isDynamic) {
|
|
this.applyDynamicTemplateData(personalization);
|
|
} else {
|
|
this.applySubstitutions(personalization);
|
|
}
|
|
|
|
//Push personalization to array
|
|
this.personalizations.push(personalization);
|
|
}
|
|
|
|
/**
|
|
* Convenience method for quickly creating personalizations
|
|
*/
|
|
addTo(to, cc, bcc) {
|
|
if (
|
|
typeof to === 'undefined' &&
|
|
typeof cc === 'undefined' &&
|
|
typeof bcc === 'undefined'
|
|
) {
|
|
throw new Error('Provide at least one of to, cc or bcc');
|
|
}
|
|
this.addPersonalization(new Personalization({to, cc, bcc}));
|
|
}
|
|
|
|
/**
|
|
* Set substitutions
|
|
*/
|
|
setSubstitutions(substitutions) {
|
|
this._setProperty('substitutions', substitutions, 'object');
|
|
}
|
|
|
|
/**
|
|
* Set substitution wrappers
|
|
*/
|
|
setSubstitutionWrappers(substitutionWrappers) {
|
|
let lengthCheck = (propertyName, value) => {
|
|
if (!Array.isArray(value) || value.length !== 2) {
|
|
throw new Error('Array expected with two elements for `' + propertyName + '`');
|
|
}
|
|
};
|
|
|
|
if (this._checkProperty('substitutionWrappers', substitutionWrappers, [this._checkUndefined, lengthCheck])) {
|
|
this.substitutionWrappers = substitutionWrappers;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper which applies globally set substitutions to personalizations
|
|
*/
|
|
applySubstitutions(personalization) {
|
|
if (personalization instanceof Personalization) {
|
|
personalization.reverseMergeSubstitutions(this.substitutions);
|
|
personalization.setSubstitutionWrappers(this.substitutionWrappers);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper which applies globally set dynamic_template_data to personalizations
|
|
*/
|
|
applyDynamicTemplateData(personalization) {
|
|
if (personalization instanceof Personalization) {
|
|
personalization.deepMergeDynamicTemplateData(this.dynamicTemplateData);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set dynamicTemplateData
|
|
*/
|
|
setDynamicTemplateData(dynamicTemplateData) {
|
|
if (typeof dynamicTemplateData === 'undefined') {
|
|
return;
|
|
}
|
|
if (typeof dynamicTemplateData !== 'object') {
|
|
throw new Error('Object expected for `dynamicTemplateData`');
|
|
}
|
|
|
|
// Check dynamic template for non-escaped characters and warn if found
|
|
if (!this.hideWarnings) {
|
|
Object.values(dynamicTemplateData).forEach(value => {
|
|
if (/['"&]/.test(value)) {
|
|
console.warn(DYNAMIC_TEMPLATE_CHAR_WARNING);
|
|
}
|
|
});
|
|
}
|
|
|
|
this.dynamicTemplateData = dynamicTemplateData;
|
|
}
|
|
|
|
/**
|
|
* Set content
|
|
*/
|
|
setContent(content) {
|
|
if (this._doArrayCheck('content', content)) {
|
|
if (!content.every(contentField => typeof contentField === 'object')) {
|
|
throw new Error('Expected each entry in `content` to be an object');
|
|
}
|
|
if (!content.every(contentField => typeof contentField.type === 'string')) {
|
|
throw new Error('Expected each `content` entry to contain a `type` string');
|
|
}
|
|
if (!content.every(contentField => typeof contentField.value === 'string')) {
|
|
throw new Error('Expected each `content` entry to contain a `value` string');
|
|
}
|
|
this.content = content;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add content
|
|
*/
|
|
addContent(content) {
|
|
if (this._checkProperty('content', content, [this._createTypeCheck('object')])) {
|
|
this.content.push(content);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add text content
|
|
*/
|
|
addTextContent(text) {
|
|
if (this._checkProperty('text', text, [this._checkUndefined, this._createTypeCheck('string')])) {
|
|
this.addContent({
|
|
value: text,
|
|
type: 'text/plain',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add HTML content
|
|
*/
|
|
addHtmlContent(html) {
|
|
if (this._checkProperty('html', html, [this._checkUndefined, this._createTypeCheck('string')])) {
|
|
this.addContent({
|
|
value: html,
|
|
type: 'text/html',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set attachments
|
|
*/
|
|
setAttachments(attachments) {
|
|
if (this._doArrayCheck('attachments', attachments)) {
|
|
if (!attachments.every(attachment => typeof attachment.content === 'string')) {
|
|
throw new Error('Expected each attachment to contain a `content` string');
|
|
}
|
|
if (!attachments.every(attachment => typeof attachment.filename === 'string')) {
|
|
throw new Error('Expected each attachment to contain a `filename` string');
|
|
}
|
|
if (!attachments.every(attachment => !attachment.type || typeof attachment.type === 'string')) {
|
|
throw new Error('Expected the attachment\'s `type` field to be a string');
|
|
}
|
|
if (!attachments.every(attachment => !attachment.disposition || typeof attachment.disposition === 'string')) {
|
|
throw new Error('Expected the attachment\'s `disposition` field to be a string');
|
|
}
|
|
this.attachments = attachments;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add attachment
|
|
*/
|
|
addAttachment(attachment) {
|
|
if (this._checkProperty('attachment', attachment, [this._checkUndefined, this._createTypeCheck('object')])) {
|
|
this.attachments.push(attachment);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set categories
|
|
*/
|
|
setCategories(categories) {
|
|
let allElementsAreStrings = (propertyName, value) => {
|
|
if (!Array.isArray(value) || !value.every(item => typeof item === 'string')) {
|
|
throw new Error('Array of strings expected for `' + propertyName + '`');
|
|
}
|
|
};
|
|
|
|
if (typeof categories === 'string') {
|
|
categories = [categories];
|
|
}
|
|
|
|
if (this._checkProperty('categories', categories, [this._checkUndefined, allElementsAreStrings])) {
|
|
this.categories = categories;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add category
|
|
*/
|
|
addCategory(category) {
|
|
if (this._checkProperty('category', category, [this._createTypeCheck('string')])) {
|
|
this.categories.push(category);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set headers
|
|
*/
|
|
setHeaders(headers) {
|
|
this._setProperty('headers', headers, 'object');
|
|
}
|
|
|
|
/**
|
|
* Add a header
|
|
*/
|
|
addHeader(key, value) {
|
|
if (this._checkProperty('key', key, [this._createTypeCheck('string')])
|
|
&& this._checkProperty('value', value, [this._createTypeCheck('string')])) {
|
|
this.headers[key] = value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set sections
|
|
*/
|
|
setSections(sections) {
|
|
this._setProperty('sections', sections, 'object');
|
|
}
|
|
|
|
/**
|
|
* Set custom args
|
|
*/
|
|
setCustomArgs(customArgs) {
|
|
this._setProperty('customArgs', customArgs, 'object');
|
|
}
|
|
|
|
/**
|
|
* Set tracking settings
|
|
*/
|
|
setTrackingSettings(settings) {
|
|
if (typeof settings === 'undefined') {
|
|
return;
|
|
}
|
|
validateTrackingSettings(settings);
|
|
this.trackingSettings = settings;
|
|
}
|
|
|
|
/**
|
|
* Set mail settings
|
|
*/
|
|
setMailSettings(settings) {
|
|
if (typeof settings === 'undefined') {
|
|
return;
|
|
}
|
|
validateMailSettings(settings);
|
|
this.mailSettings = settings;
|
|
}
|
|
|
|
/**
|
|
* Set hide warnings
|
|
*/
|
|
setHideWarnings(hide) {
|
|
if (typeof hide === 'undefined') {
|
|
return;
|
|
}
|
|
if (typeof hide !== 'boolean') {
|
|
throw new Error('Boolean expected for `hideWarnings`');
|
|
}
|
|
this.hideWarnings = hide;
|
|
}
|
|
|
|
/**
|
|
* To JSON
|
|
*/
|
|
toJSON() {
|
|
|
|
//Extract properties from self
|
|
const {
|
|
from, replyTo, sendAt, subject, content, templateId,
|
|
personalizations, attachments, ipPoolName, batchId, asm,
|
|
sections, headers, categories, customArgs, mailSettings,
|
|
trackingSettings, replyToList,
|
|
} = this;
|
|
|
|
//Initialize with mandatory values
|
|
const json = {
|
|
from, subject,
|
|
personalizations: arrayToJSON(personalizations),
|
|
};
|
|
|
|
//Array properties
|
|
if (Array.isArray(attachments) && attachments.length > 0) {
|
|
json.attachments = arrayToJSON(attachments);
|
|
}
|
|
if (Array.isArray(categories) && categories.length > 0) {
|
|
json.categories = categories.filter(cat => cat !== '');
|
|
}
|
|
if (Array.isArray(content) && content.length > 0) {
|
|
json.content = arrayToJSON(content);
|
|
}
|
|
|
|
//Object properties
|
|
if (Object.keys(headers).length > 0) {
|
|
json.headers = headers;
|
|
}
|
|
if (Object.keys(mailSettings).length > 0) {
|
|
json.mailSettings = mailSettings;
|
|
}
|
|
if (Object.keys(trackingSettings).length > 0) {
|
|
json.trackingSettings = trackingSettings;
|
|
}
|
|
if (Object.keys(customArgs).length > 0) {
|
|
json.customArgs = customArgs;
|
|
}
|
|
if (Object.keys(sections).length > 0) {
|
|
json.sections = sections;
|
|
}
|
|
if (Object.keys(asm).length > 0) {
|
|
json.asm = asm;
|
|
}
|
|
|
|
//Simple properties
|
|
if (typeof replyTo !== 'undefined') {
|
|
json.replyTo = replyTo;
|
|
}
|
|
if (typeof sendAt !== 'undefined') {
|
|
json.sendAt = sendAt;
|
|
}
|
|
if (typeof batchId !== 'undefined') {
|
|
json.batchId = batchId;
|
|
}
|
|
if (typeof templateId !== 'undefined') {
|
|
json.templateId = templateId;
|
|
}
|
|
if (typeof ipPoolName !== 'undefined') {
|
|
json.ipPoolName = ipPoolName;
|
|
}
|
|
if(typeof replyToList !== 'undefined') {
|
|
json.replyToList = replyToList;
|
|
}
|
|
|
|
//Return as snake cased object
|
|
return toSnakeCase(json, ['substitutions', 'dynamicTemplateData', 'customArgs', 'headers', 'sections']);
|
|
}
|
|
|
|
/**************************************************************************
|
|
* Static helpers
|
|
***/
|
|
|
|
/**
|
|
* Create a Mail instance from given data
|
|
*/
|
|
static create(data) {
|
|
|
|
//Array?
|
|
if (Array.isArray(data)) {
|
|
return data
|
|
.filter(item => !!item)
|
|
.map(item => this.create(item));
|
|
}
|
|
|
|
//Already instance of Mail class?
|
|
if (data instanceof Mail) {
|
|
return data;
|
|
}
|
|
|
|
//Create instance
|
|
return new Mail(data);
|
|
}
|
|
|
|
/**************************************************************************
|
|
* helpers for property-setting checks
|
|
***/
|
|
|
|
/**
|
|
* Perform a set of checks on the new property value. Returns true if all
|
|
* checks complete successfully without throwing errors or returning true.
|
|
*/
|
|
_checkProperty(propertyName, value, checks) {
|
|
return !checks.some((e) => e(propertyName, value));
|
|
}
|
|
|
|
/**
|
|
* Set a property with normal undefined and type-checks
|
|
*/
|
|
_setProperty(propertyName, value, propertyType) {
|
|
let propertyChecksPassed = this._checkProperty(
|
|
propertyName,
|
|
value,
|
|
[this._checkUndefined, this._createTypeCheck(propertyType)]);
|
|
|
|
if (propertyChecksPassed) {
|
|
this[propertyName] = value;
|
|
}
|
|
|
|
return propertyChecksPassed;
|
|
}
|
|
|
|
/**
|
|
* Fail if the value is undefined.
|
|
*/
|
|
_checkUndefined(propertyName, value) {
|
|
return typeof value === 'undefined';
|
|
}
|
|
|
|
/**
|
|
* Create and return a function that checks for a given type
|
|
*/
|
|
_createTypeCheck(propertyType) {
|
|
return (propertyName, value) => {
|
|
if (typeof value !== propertyType) {
|
|
throw new Error(propertyType + ' expected for `' + propertyName + '`');
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a check out of a callback. If the callback
|
|
* returns false, the check will throw an error.
|
|
*/
|
|
_createCheckThatThrows(check, errorString) {
|
|
return (propertyName, value) => {
|
|
if (!check(value)) {
|
|
throw new Error(errorString);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Set an array property after checking that the new value is an
|
|
* array.
|
|
*/
|
|
_setArrayProperty(propertyName, value) {
|
|
if (this._doArrayCheck(propertyName, value)) {
|
|
this[propertyName] = value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check that a value isn't undefined and is an array.
|
|
*/
|
|
_doArrayCheck(propertyName, value) {
|
|
return this._checkProperty(
|
|
propertyName,
|
|
value,
|
|
[this._checkUndefined, this._createCheckThatThrows(Array.isArray, 'Array expected for`' + propertyName + '`')]);
|
|
}
|
|
|
|
/**
|
|
* Set the replyToList from email body
|
|
*/
|
|
setReplyToList(replyToList) {
|
|
if (this._doArrayCheck('replyToList', replyToList) && replyToList.length) {
|
|
if (!replyToList.every(replyTo => replyTo && typeof replyTo.email === 'string')) {
|
|
throw new Error('Expected each replyTo to contain an `email` string');
|
|
}
|
|
this.replyToList = replyToList;
|
|
}
|
|
}
|
|
}
|
|
|
|
//Export class
|
|
module.exports = Mail;
|