expresser
Version:
A ready-to-use platform for Node.js web apps, built on top of Express.
255 lines (208 loc) • 11 kB
text/coffeescript
# EXPRESSER MAILER
# --------------------------------------------------------------------------
# Sends and manages emails, supports templates. When parsing templates, the
# tags should be wrapped with normal brackets {}. Example: {contents}
# The base message template (which is loaded with every single sent message)
# must be saved as base.html, under the /emailtemplates folder (or whatever
# folder / base file name you have set on the settings).
# <!--
# @see Settings.mailer
# -->
class Mailer
events = require "./events.coffee"
fs = require "fs"
lodash = require "lodash"
logger = require "./logger.coffee"
mailer = require "nodemailer"
moment = require "moment"
path = require "path"
settings = require "./settings.coffee"
utils = require "./utils.coffee"
# SMTP objects will be instantiated on `init`.
smtp = null
smtp2 = null
# Templates cache to avoid disk reads.
templateCache = {}
# CONSTRUCTOR AND INIT
# --------------------------------------------------------------------------
# Mailer constructor.
constructor: ->
@setEvents() if settings.events.enabled
# Bind event listeners.
setEvents: =>
events.on "mailer.send", @send
# Init the Mailer module and create the SMTP objects.
# @param [Object] options Mailer init options.
init: (options) =>
if settings.mailer.smtp.service? and settings.mailer.smtp.service isnt ""
@setSmtp settings.mailer.smtp, false
else if settings.mailer.smtp.host? and settings.mailer.smtp.host isnt "" and settings.mailer.smtp.port > 0
@setSmtp settings.mailer.smtp, false
if settings.mailer.smtp2.service? and settings.mailer.smtp2.service isnt ""
@setSmtp settings.mailer.smtp2, true
else if settings.mailer.smtp2.host? and settings.mailer.smtp2.host isnt "" and settings.mailer.smtp2.port > 0
@setSmtp settings.mailer.smtp2, true
# Alert user if specified backup SMTP but not the main one.
if not smtp? and smtp2?
logger.warn "Mailer.init", "The secondary SMTP settings are defined but not the main one.", "Will still work, but you should set the main one instead."
# Warn if no SMTP is available for sending emails, but only when debug is enabled.
if not smtp? and not smtp2? and settings.general.debug
logger.warn "Mailer.init", "No main SMTP host/port specified.", "No emails will be sent out."
# Check if configuration for sending emails is properly set.
# @return [Boolean] Returns true if smtp server is active, otherwise false.
checkConfig: =>
if smtp or smtp2?
return true
else
return false
# OUTBOUND
# --------------------------------------------------------------------------
# Sends an email to the specified address. A callback can be specified, having (err, result).
# @param [String] options The email message options
# @option options [String] body The email body in text or HTML.
# @option options [String] subject The email subject.
# @option options [String] to The "to" address.
# @option options [String] from The "from" address, optional, if blank use default from settings.
# @option options [String] template The template file to be loaded, optional.
# @param [Function] callback Callback (err, result) when message is sent or fails.
send: (options, callback) =>
logger.debug "Mailer.send", options
if not @checkConfig()
errMsg = "SMTP transport wasn't initiated. Abort!"
logger.warn "Mailer.send", errMsg, options
callback errMsg, null if callback?
return
# Make sure "to" address is valid.
if not options.to? or options.to is false or options.to is ""
errMsg = "Option 'to' is not valid. Abort!"
logger.warn "Mailer.send", errMsg, options
callback errMsg, null if callback?
return
# Set from to default address if no `to` was set, and `logError` defaults to true.
options.from = "#{settings.general.appTitle} <#{settings.mailer.from}>" if not options.from?
options.logError = true if not options.logError?
# Set HTML to body, if passed.
html = if options.body? then options.body.toString() else ""
# Get the name of recipient based on the `to` option.
if options.to.indexOf("<") < 3
toName = options.to
else
toName = options.to.substring 0, options.to.indexOf("<") - 1
# Load template if a `template` was passed.
if options.template? and options.template isnt ""
template = @getTemplate options.template
html = @parseTemplate template, {contents: html}
# Parse template keywords if a `keywords` was passed.
if lodash.isObject options.keywords
html = @parseTemplate html, options.keywords
# Parse final template and set it on the `options`.
html = @parseTemplate html, {to: toName, appTitle: settings.general.appTitle, appUrl: settings.general.appUrl}
options.html = html
# Check if `doNotSend` flag is set, and if so, do not send anything.
if settings.mailer.doNotSend
callback null, "The 'doNotSend' setting is true, will not send anything!" if callback?
return
# Send using the main SMTP. If failed and a secondary is also set, try using the secondary.
smtpSend smtp, options, (err, result) ->
if err?
if smtp2?
smtpSend smtp2, options, (err2, result2) -> callback err2, result2
else
callback err, result if callback?
else
callback err, result if callback?
# TEMPLATES
# --------------------------------------------------------------------------
# Load and return the specified template. Get from the cache or from the disk
# if it wasn't loaded yet. Templates are stored inside the `/emailtemplates`
# folder by default and should have a .html extension. The base template,
# which is always loaded first, should be called base.html by default.
# The contents will be inserted on the {contents} tag.
# @param [String] name The template name, without .html.
# @return [String] The template HTML.
getTemplate: (name) =>
name = name.replace(".html", "") if name.indexOf(".html")
cached = templateCache[name]
# Is it already cached? If so do not hit the disk.
if cached? and cached.expires > moment()
logger.debug "Mailer.getTemplate", name, "Loaded from cache."
return templateCache[name].template
else
logger.debug "Mailer.getTemplate", name
# Set file system reading options.
readOptions = {encoding: settings.general.encoding}
baseFile = utils.getFilePath path.join(settings.path.emailTemplatesDir, settings.mailer.baseTemplateFile)
templateFile = utils.getFilePath path.join(settings.path.emailTemplatesDir, "#{name}.html")
# Read base and `name` template and merge them together.
base = fs.readFileSync baseFile, readOptions
template = fs.readFileSync templateFile, readOptions
result = @parseTemplate base, {contents: template}
# Save to cache.
templateCache[name] = {}
templateCache[name].template = result
templateCache[name].expires = moment().add "s", settings.general.ioCacheTimeout
return result
# Parse the specified template to replace keywords. The `keywords` is a set of key-values
# to be replaced. For example if keywords is `{id: 1, friendlyUrl: "abc"}` then the tags
# `{id}` and `{friendlyUrl}` will be replaced with the values 1 and abc.
# @param [String] template The template (its value, not its name!) to be parsed.
# @param [Object] keywords Object with keys to be replaced with its values.
# @return [String] The parsed template, keywords replaced with values.
parseTemplate: (template, keywords) =>
template = template.toString()
for key, value of keywords
template = template.replace new RegExp("\\{#{key}\\}", "gi"), value
return template
# HELPER METHODS
# --------------------------------------------------------------------------
# Helper to send emails using the specified transport and options.
smtpSend = (transport, options, callback) ->
try
transport.sendMail options, (err, result) ->
if err?
if options.logError
logger.error "Mailer.smtpSend", transport.host, "Could not send: #{options.subject} to #{options.to}.", err
else
logger.debug "Mailer.smtpSend", "OK", transport.host, options.subject, "to #{options.to}", "from #{options.from}."
callback err, result
catch ex
callback ex
# Helper to create a SMTP object.
createSmtp = (options) ->
options.debug = settings.general.debug if not options.debug?
options.secureConnection = options.secure if not options.secureConnection?
# Make sure auth is properly set.
if not options.auth? and options.user? and options.password?
options.auth = {user: options.user, pass: options.password}
delete options["user"]
delete options["password"]
# Check if `service` is set. If so, pass to the mailer, otheriwse use SMTP.
if options.service? and options.service isnt ""
logger.info "Mailer.createSmtp", "Service", options.service
result = mailer.createTransport options.service, options
else
logger.info "Mailer.createSmtp", options.host, options.port, options.secureConnection
result = mailer.createTransport "SMTP", options
# Sign using DKIM?
result.useDKIM settings.mailer.dkim if settings.mailer.dkim.enabled
# Return SMTP object.
return result
# Use the specified options and create a new SMTP server.
# @param [Object] options Options to be passed to SMTP creator.
# @param [Boolean] secondary If false set as the main SMTP server, if true set as secondary.
setSmtp: (options, secondary) =>
if secondary or secondary > 0
smtp2 = createSmtp options
else
smtp = createSmtp options
# Force clear the templates cache.
clearCache: =>
count = Object.keys(templateCache).length
templateCache = {}
logger.info "Mailer.clearCache", "Cleared #{count} templates."
# Singleton implementation
# --------------------------------------------------------------------------
Mailer.getInstance = ->
@instance = new Mailer() if not @instance?
return @instance
module.exports = exports = Mailer.getInstance()