webhandle-emailer
Version:
Creates functions to email the results of form submission
387 lines (341 loc) • 10.5 kB
JavaScript
const filog = require('filter-log')
let log = filog('webhandle:webhandle-emailer')
const moment = require('moment')
const striptags = require('striptags')
const path = require('path')
const nodemailer = require('nodemailer')
const grecaptchaRequest = require('./grecaptcha-request')
const stream = require('stream');
let example = {
transport: {
service: "gmail",
auth: {
user: "somename@gmail.com",
pass: "somepass"
}
}
}
function cleanse(body, fields) {
let o = Object.assign({}, body)
Object.assign(o, fields)
for (let key of Object.keys(o)) {
if (typeof o[key] == 'string') {
o[key] = striptags(o[key])
}
else if (Array.isArray(o[key])) {
for (let i = 0; i < o[key].length; i++) {
o[key][i] = striptags(o[key][i])
}
}
}
return o
}
function isValueTrue(context) {
if (typeof context == 'undefined') {
return false
}
if (context === null || context === undefined || context === false) {
return false
}
if (typeof context == 'string') {
if (context) {
return true
}
return false
}
if (typeof context == 'object') {
return true
}
if (typeof context == 'array') {
return context.length > 0
}
return false
}
function addTemplates(tri) {
tri.addTemplate('contact-forms/yesNo', function (context) {
return isValueTrue(context) ? 'yes' : 'no'
})
}
class Emailer {
constructor(transportOptions) {
if (transportOptions) {
this.transportOptions = transportOptions
}
else {
if (process.env.webhandleEmail) {
if (typeof process.env.webhandleEmail == 'string') {
try {
this.transportOptions = JSON.parse(process.env.webhandleEmail)
} catch (e) {
log.error('Could not parse webhandleEmail options: ' + e.message)
}
}
} else {
log.error('No transport information at process.env.webhandleEmail. Info should look like %s' + JSON.stringify(example, null, "\t"))
}
}
this.transportDef = this.transportOptions.transport
this.transport = nodemailer.createTransport(this.transportDef)
}
async findFirstNotNullValue(candidates, fArgs) {
for (let candidate of candidates) {
if (candidate) {
if (typeof candidate === 'string') {
return candidate
}
if (typeof candidate === 'function') {
return await candidate(...fArgs)
}
}
}
return null
}
/**
*
* @param {object} message The message to send
* @param {string} [message.to] String of destination addresses. Use a string of comma separated values for
* multiple recipients
* @param {string} [message.replyTo] Add a different reply to other than the sender
* @param {string|function} [message.from] The from address. Frequently left black where the sending service requires a specific address.
* @param {string|function} message.subject The subject to use or a function which creates the subject
* @param {string} [message.emailTemplate] The name of the template to use
* @param {object} [message.data] The data to render with the template
* @param {string} [message.messageHTML] The text of the message to send. Use this or emailTemplate
* @param {array} [message.attachments]
* @param {object} [options]
* @param {function} [options.cleanse]
* @param {function} [options.processFields]
* @param {function} [options.preRenderProcessor]
* @param {function} [options.addTemplates]
* @param {function} [options.preSendProcessor]
* @returns
*/
async sendEmail(message, options = {}) {
let transport = this.transport
let transportDef = this.transportDef
let self = this
let tri = webhandle.tripartite
let reqTri = tri.createBlank()
reqTri.loaders = webhandle.templateLoaders
reqTri.dataFunctions = Object.assign({}, tri.dataFunctions)
return new Promise(async (resolve, reject) => {
let dat = (options.cleanse || this.cleanse)(message.data || {}, {})
if (options.processFields) {
let ret = options.processFields(dat)
if (ret) {
dat = ret
}
}
let mailOptions = {
subject: await this.findFirstNotNullValue([message.subject, 'Contact from the website'], [message, options])
, from: await this.findFirstNotNullValue([message.from, transportDef.auth.user], [message, options])
, to: await this.findFirstNotNullValue([message.to, this.transportOptions.destDefault], [message, options])
, replyTo: await this.findFirstNotNullValue([message.replyTo], [message, options])
}
if (message.attachments) {
mailOptions.attachments = await this.findFirstNotNullValue([message.attachments], [message, options])
}
if (options.preRenderProcessor) {
options.preRenderProcessor(mailOptions, message, options, dat)
}
if (options.addTemplates) {
options.addTemplates(reqTri)
}
if (message.emailTemplate) {
reqTri.loadTemplate(message.emailTemplate, (template) => {
mailOptions.html = ''
let emailRenderStream = new stream.Writable();
emailRenderStream._write = function (chunk, encoding, done) {
mailOptions.html += chunk.toString()
done()
};
template(dat, emailRenderStream, function () {
sendWithContent()
})
})
}
else {
mailOptions.html = message.messageHTML
sendWithContent()
}
function sendWithContent() {
if (options.preSendProcessor) {
options.preSendProcessor(mailOptions, req, options)
}
if (transport) {
transport.sendMail(mailOptions, function (error, info) {
try {
if (error) {
log.error('Could not send email: %s\n%s', error.message, error.stack)
log.error({
message: 'Email lost',
response: info.response,
contents: mailOptions,
formParms: dat
})
reject(error)
} else {
log.info({
message: 'Email sent',
response: info.response,
contents: mailOptions,
formParms: dat
})
resolve()
}
} catch (e) {
console.log(e)
}
})
}
else {
let msg = 'No transport is defined.'
log.error(msg)
reject(new Error(msg))
}
}
})
}
createFormHandler(options) {
let transport = this.transport
let transportDef = this.transportDef
let self = this
options.requiredGrecaptchaScore = options.requiredGrecaptchaScore || .5
return function (req, res, next) {
let dat
if (options.cleanse) {
dat = options.cleanse(req.body, req.fields)
}
else {
dat = self.cleanse(req.body, req.fields)
}
if (options.processFields) {
let ret = options.processFields(dat, req, res)
if (ret) {
dat = ret
}
}
function handleUserResponse(req, res, next) {
if (options.skipResponse) {
next()
}
else if (options.redirectUrl) {
res.redirect(options.redirectUrl)
}
else if (options.respondent) {
options.respondent(req, res, next)
}
else {
res.end()
}
}
function handleGRecaptchaCheck(req, res, next) {
if (options.grecaptchaPrivate) {
grecaptchaRequest(options.grecaptchaPrivate, dat.grt, (err, answer) => {
if (answer.success && answer.score >= options.requiredGrecaptchaScore) {
runEmailSend()
}
else {
handleUserResponse(req, res, next)
}
})
}
else {
runEmailSend()
}
}
async function runEmailSend() {
if (options.addTemplates) {
options.addTemplates(res.tri)
}
res.tri.loadTemplate(options.emailTemplate || 'contact-email', async (template) => {
try {
let mailOptions = {
from: transportDef.auth.user
}
mailOptions.subject = await self.findFirstNotNullValue([options.subject, 'Contact from the website'], [req, res, mailOptions, dat, options])
mailOptions.from = await self.findFirstNotNullValue([options.from, transportDef.auth.user], [req, res, mailOptions, dat, options])
mailOptions.to = await self.findFirstNotNullValue([options.to, transportDef.destDefault], [req, res, mailOptions, dat, options])
mailOptions.replyTo = await self.findFirstNotNullValue([options.replyTo, dat.email], [req, res, mailOptions, dat, options])
if (options.attachments && typeof options.attachments == 'function') {
mailOptions.attachments = options.attachments(req, res)
}
else if (options.attachments && options.attachments.length) {
mailOptions.attachments = options.attachments
}
if (options.preRenderProcessor) {
options.preRenderProcessor(mailOptions, req, options, dat)
}
function sendContents() {
if (options.preSendProcessor) {
options.preSendProcessor(mailOptions, req, options)
}
if (transport) {
transport.sendMail(mailOptions, function (error, info) {
try {
if (error) {
log.error('Could not send email: %s\n%s', error.message, error.stack)
log.info({
message: 'Email lost',
response: info.response,
contents: mailOptions,
formParms: dat
})
} else {
log.info({
message: 'Email sent',
response: info.response,
contents: mailOptions,
formParms: dat
})
}
} catch (e) {
console.log(e)
}
})
} else {
log.error('No transport is defined.')
}
handleUserResponse(req, res, next)
}
mailOptions.html = ''
let emailRenderStream = new stream.Writable();
emailRenderStream._write = function (chunk, encoding, done) {
mailOptions.html += chunk.toString()
done()
};
template(dat, emailRenderStream, function () {
sendContents()
})
} catch (ex) {
console.log(ex)
}
})
}
// if we're supposed to use a vrf, check the vrf. If it's not right, log it and redirect them to the
// success page.
if (!options.noVrf && (dat.vrf != (options.vrf || '12'))) {
log.error('Verification could did not match. ' + dat.vrf + ' did not equal ' + (options.vrf || '12'))
handleUserResponse(req, res, next)
return
}
if (options.spamCheck) {
options.spamCheck(req, res, (err) => {
if (!err) {
handleGRecaptchaCheck(req, res, next)
}
else {
handleUserResponse(req, res, next)
}
})
}
else {
handleGRecaptchaCheck(req, res, next)
}
}
}
cleanse(body, fields) {
return cleanse(body, fields)
}
}
module.exports = Emailer