<commit_before>'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,
    } = 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);

    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 (typeof from === 'undefined') {
      return;
    }
    if (typeof from !== 'string') {
      throw new Error('String expected for `from`');
    }
    this.from = EmailAddress.create(from);
  }

  /**
   * Set reply to
   */
  setReplyTo(replyTo) {
    if (typeof replyTo === 'undefined') {
      return;
    }
    if (typeof replyTo !== 'string') {
      throw new Error('String expected for `replyTo`');
    }
    this.replyTo = EmailAddress.create(replyTo);
  }

  /**
   * Set subject
   */
  setSubject(subject) {
    if (typeof subject === 'undefined') {
      return;
    }
    if (typeof subject !== 'string') {
      throw new Error('String expected for `subject`');
    }
    this.subject = subject;
  }

  /**
   * Set send at
   */
  setSendAt(sendAt) {
    if (typeof sendAt === 'undefined') {
      return;
    }
    if (!Number.isInteger(sendAt)) {
      throw new Error('Integer expected for `sendAt`');
    }
    this.sendAt = sendAt;
  }

  /**
   * Set template ID, also checks if the template is dynamic or legacy
   */
  setTemplateId(templateId) {
    if (typeof templateId === 'undefined') {
      return;
    }
    if (typeof templateId !== 'string') {
      throw new Error('String expected for `templateId`');
    }

    if (templateId.indexOf('d-') === 0) {
      this.isDynamic = true;
    }

    this.templateId = templateId;
  }

  /**
   * Set batch ID
   */
  setBatchId(batchId) {
    if (typeof batchId === 'undefined') {
      return;
    }
    if (typeof batchId !== 'string') {
      throw new Error('String expected for `batchId`');
    }
    this.batchId = batchId;
  }

  /**
   * Set IP pool name
   */
  setIpPoolName(ipPoolName) {
    if (typeof ipPoolName === 'undefined') {
      return;
    }
    if (typeof ipPoolName !== 'string') {
      throw new Error('String expected for `ipPoolName`');
    }
    this.ipPoolName = ipPoolName;
  }

  /**
   * Set ASM
   */
  setAsm(asm) {
    if (typeof asm === 'undefined') {
      return;
    }
    if (typeof asm !== 'object') {
      throw new Error('Object expected for `asm`');
    }
    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 (typeof personalizations === 'undefined') {
      return;
    }
    if (!Array.isArray(personalizations) ||
      !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) {
    if (typeof substitutions === 'undefined') {
      return;
    }
    if (typeof substitutions !== 'object') {
      throw new Error('Object expected for `substitutions`');
    }
    this.substitutions = substitutions;
  }

  /**
   * Set substitution wrappers
   */
  setSubstitutionWrappers(wrappers) {
    if (typeof wrappers === 'undefined') {
      return;
    }
    if (!Array.isArray(wrappers) || wrappers.length !== 2) {
      throw new Error(
        'Array expected with two elements for `substitutionWrappers`'
      );
    }
    this.substitutionWrappers = wrappers;
  }

  /**
   * 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 (typeof content === 'undefined') {
      return;
    }
    if (!Array.isArray(content)) {
      throw new Error('Array expected for `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 (typeof content !== 'object') {
      throw new Error('Object expected for `content`');
    }
    this.content.push(content);
  }

  /**
   * Add text content
   */
  addTextContent(text) {
    if (typeof text === 'undefined') {
      return;
    }
    if (typeof text !== 'string') {
      throw new Error('String expected for `text`');
    }
    this.addContent({
      value: text,
      type: 'text/plain',
    });
  }

  /**
   * Add HTML content
   */
  addHtmlContent(html) {
    if (typeof html === 'undefined') {
      return;
    }
    if (typeof html !== 'string') {
      throw new Error('String expected for `html`');
    }
    this.addContent({
      value: html,
      type: 'text/html',
    });
  }

  /**
   * Set attachments
   */
  setAttachments(attachments) {
    if (typeof attachments === 'undefined') {
      return;
    }
    if (!Array.isArray(attachments)) {
      throw new Error('Array expected for `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 (typeof attachment !== 'object') {
      throw new Error('Object expected for `attachment`');
    }
    this.attachments.push(attachment);
  }

  /**
   * Set categories
   */
  setCategories(categories) {
    if (typeof categories === 'undefined') {
      return;
    }
    if (typeof categories === 'string') {
      categories = [categories];
    }
    if (!Array.isArray(categories) ||
      !categories.every(cat => typeof cat === 'string')) {
      throw new Error('Array of strings expected for `categories`');
    }
    this.categories = categories;
  }

  /**
   * Add category
   */
  addCategory(category) {
    if (typeof category !== 'string') {
      throw new Error('String expected for `category`');
    }
    this.categories.push(category);
  }

  /**
   * Set headers
   */
  setHeaders(headers) {
    if (typeof headers === 'undefined') {
      return;
    }
    if (typeof headers !== 'object') {
      throw new Error('Object expected for `headers`');
    }
    this.headers = headers;
  }

  /**
   * Add a header
   */
  addHeader(key, value) {
    if (typeof key !== 'string') {
      throw new Error('String expected for header key');
    }
    if (typeof value !== 'string') {
      throw new Error('String expected for header value');
    }
    this.headers[key] = value;
  }

  /**
   * Set sections
   */
  setSections(sections) {
    if (typeof sections === 'undefined') {
      return;
    }
    if (typeof sections !== 'object') {
      throw new Error('Object expected for `sections`');
    }
    this.sections = sections;
  }

  /**
   * Set custom args
   */
  setCustomArgs(customArgs) {
    if (typeof customArgs === 'undefined') {
      return;
    }
    if (typeof customArgs !== 'object') {
      throw new Error('Object expected for `customArgs`');
    }
    this.customArgs = customArgs;
  }

  /**
   * 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,
    } = 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;
    }

    //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);
  }
}

//Export class
module.exports = Mail;
<commit_msg>Allow either strings or "email objects" in `from` and `reply-to`<commit_after>'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,
    } = 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);

    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 (typeof from === 'undefined') {
      return;
    }
    if (typeof from !== 'string' &&
      !(typeof from === 'object' && 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 (typeof replyTo === 'undefined') {
      return;
    }
    if (typeof replyTo !== 'string' &&
      !(typeof replyTo === 'object' && typeof replyTo.email === 'string')) {
      throw new Error('String or address object expected for `replyTo`');
    }
    this.replyTo = EmailAddress.create(replyTo);
  }

  /**
   * Set subject
   */
  setSubject(subject) {
    if (typeof subject === 'undefined') {
      return;
    }
    if (typeof subject !== 'string') {
      throw new Error('String expected for `subject`');
    }
    this.subject = subject;
  }

  /**
   * Set send at
   */
  setSendAt(sendAt) {
    if (typeof sendAt === 'undefined') {
      return;
    }
    if (!Number.isInteger(sendAt)) {
      throw new Error('Integer expected for `sendAt`');
    }
    this.sendAt = sendAt;
  }

  /**
   * Set template ID, also checks if the template is dynamic or legacy
   */
  setTemplateId(templateId) {
    if (typeof templateId === 'undefined') {
      return;
    }
    if (typeof templateId !== 'string') {
      throw new Error('String expected for `templateId`');
    }

    if (templateId.indexOf('d-') === 0) {
      this.isDynamic = true;
    }

    this.templateId = templateId;
  }

  /**
   * Set batch ID
   */
  setBatchId(batchId) {
    if (typeof batchId === 'undefined') {
      return;
    }
    if (typeof batchId !== 'string') {
      throw new Error('String expected for `batchId`');
    }
    this.batchId = batchId;
  }

  /**
   * Set IP pool name
   */
  setIpPoolName(ipPoolName) {
    if (typeof ipPoolName === 'undefined') {
      return;
    }
    if (typeof ipPoolName !== 'string') {
      throw new Error('String expected for `ipPoolName`');
    }
    this.ipPoolName = ipPoolName;
  }

  /**
   * Set ASM
   */
  setAsm(asm) {
    if (typeof asm === 'undefined') {
      return;
    }
    if (typeof asm !== 'object') {
      throw new Error('Object expected for `asm`');
    }
    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 (typeof personalizations === 'undefined') {
      return;
    }
    if (!Array.isArray(personalizations) ||
      !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) {
    if (typeof substitutions === 'undefined') {
      return;
    }
    if (typeof substitutions !== 'object') {
      throw new Error('Object expected for `substitutions`');
    }
    this.substitutions = substitutions;
  }

  /**
   * Set substitution wrappers
   */
  setSubstitutionWrappers(wrappers) {
    if (typeof wrappers === 'undefined') {
      return;
    }
    if (!Array.isArray(wrappers) || wrappers.length !== 2) {
      throw new Error(
        'Array expected with two elements for `substitutionWrappers`'
      );
    }
    this.substitutionWrappers = wrappers;
  }

  /**
   * 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 (typeof content === 'undefined') {
      return;
    }
    if (!Array.isArray(content)) {
      throw new Error('Array expected for `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 (typeof content !== 'object') {
      throw new Error('Object expected for `content`');
    }
    this.content.push(content);
  }

  /**
   * Add text content
   */
  addTextContent(text) {
    if (typeof text === 'undefined') {
      return;
    }
    if (typeof text !== 'string') {
      throw new Error('String expected for `text`');
    }
    this.addContent({
      value: text,
      type: 'text/plain',
    });
  }

  /**
   * Add HTML content
   */
  addHtmlContent(html) {
    if (typeof html === 'undefined') {
      return;
    }
    if (typeof html !== 'string') {
      throw new Error('String expected for `html`');
    }
    this.addContent({
      value: html,
      type: 'text/html',
    });
  }

  /**
   * Set attachments
   */
  setAttachments(attachments) {
    if (typeof attachments === 'undefined') {
      return;
    }
    if (!Array.isArray(attachments)) {
      throw new Error('Array expected for `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 (typeof attachment !== 'object') {
      throw new Error('Object expected for `attachment`');
    }
    this.attachments.push(attachment);
  }

  /**
   * Set categories
   */
  setCategories(categories) {
    if (typeof categories === 'undefined') {
      return;
    }
    if (typeof categories === 'string') {
      categories = [categories];
    }
    if (!Array.isArray(categories) ||
      !categories.every(cat => typeof cat === 'string')) {
      throw new Error('Array of strings expected for `categories`');
    }
    this.categories = categories;
  }

  /**
   * Add category
   */
  addCategory(category) {
    if (typeof category !== 'string') {
      throw new Error('String expected for `category`');
    }
    this.categories.push(category);
  }

  /**
   * Set headers
   */
  setHeaders(headers) {
    if (typeof headers === 'undefined') {
      return;
    }
    if (typeof headers !== 'object') {
      throw new Error('Object expected for `headers`');
    }
    this.headers = headers;
  }

  /**
   * Add a header
   */
  addHeader(key, value) {
    if (typeof key !== 'string') {
      throw new Error('String expected for header key');
    }
    if (typeof value !== 'string') {
      throw new Error('String expected for header value');
    }
    this.headers[key] = value;
  }

  /**
   * Set sections
   */
  setSections(sections) {
    if (typeof sections === 'undefined') {
      return;
    }
    if (typeof sections !== 'object') {
      throw new Error('Object expected for `sections`');
    }
    this.sections = sections;
  }

  /**
   * Set custom args
   */
  setCustomArgs(customArgs) {
    if (typeof customArgs === 'undefined') {
      return;
    }
    if (typeof customArgs !== 'object') {
      throw new Error('Object expected for `customArgs`');
    }
    this.customArgs = customArgs;
  }

  /**
   * 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,
    } = 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;
    }

    //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);
  }
}

//Export class
module.exports = Mail;

{'fraction_non_alphanumeric': 0.13812314176024015, 'fraction_numerical': 0.0006350489276332881, 'mean_word_length': 2.735202156334232, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 53, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5915438', 'n_tokens_mistral': 10258, 'n_tokens_neox': 10037, 'n_words': 2495}