UNPKG

@sendgrid/helpers

Version:

Twilio SendGrid NodeJS internal helpers

690 lines (610 loc) 18.7 kB
'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;