Ever wanted to remind yourself of something, out of the blue? Well, we're building a quick tool to do that!
We're going to use Mailgun, but any transactional provider will do.
$ npm i --save mailgun-js
We're already using Handlebars.js in our app's views, so may as well use it in emails too!
(reduce, re-use, recycle)
$ mkdir emails
$ touch index.js
$ touch reminder.hbs
{{!-- emails/reminder.hbs --}}
🎯 REMINDER from {{timeago reminder.created_at}}
---
<p>Hi there, {{email}}!</p>
<p>{{message}}</p>
<p><small>You are receiving this email because you asked <a href="https://remind.ist?ref=email">{{app_name}}</a> to remind you! Now that this message is sent, it has self-destructed in our database.</small></p>
// emails/index.js
const { readFileSync } = require('fs')
const Handlebars = require('handlebars')
const glob = require('glob')
const hbsHelpers = require('../lib/handlebar-helpers')
Object.keys(hbsHelpers).map(helper => {
Handlebars.registerHelper(helper, hbsHelpers[helper])
})
const mails = {}
for (const mail of glob.sync('./emails/**.hbs')) {
const pathTokens = mail.split('/')
const key = pathTokens[pathTokens.length - 1].slice(0, -4)
const [ subject, body ] = readFileSync(mail, { encoding: 'utf8' }).split('---')
mails[key] = { subject, body }
}
// emails/index.js
const compile = (emailName, context) => {
const mail = mails[emailName]
if (!mail) throw new Error(`Email ${emailName} not found.`)
const body = Handlebars.compile(mail.body)
const subjectTemplate = Handlebars.compile(mail.subject)
const from = MAILGUN_SEND_EMAIL_FROM || `Remind.ist <bot@${MAILGUN_DOMAIN}>`
const compileContext = {
app_name: APP_NAME,
...context
}
const subject = subjectTemplate(compileContext)
const message = {
from,
to: context.email,
subject,
html: body({ ...compileContext, subject }),
'o:tag': [ emailName ],
'o:tracking': 'True'
}
return message
}
// emails/index.js
// Near the top...
const MailgunClient = require('mailgun-js')
const { MAILGUN_API_KEY, MAILGUN_DOMAIN, APP_NAME, MAILGUN_SEND_EMAIL_FROM } = process.env
if (!MAILGUN_API_KEY || !MAILGUN_DOMAIN) throw new Error(
'MAILGUN_API_KEY & MAILGUN_DOMAIN are required env variables.'
)
const client = MailgunClient({
apiKey: MAILGUN_API_KEY,
domain: MAILGUN_DOMAIN,
username: 'api'
})
// Anywhere, really...
const send = async email => {
try {
const send = await client.messages().send(email)
console.log(`=> Email queued: ${send.id}`)
} catch (error) {
console.error(`=> Error sending email: ${error.message}`)
if (error.stack) console.log(error.stack)
}
}
// End
module.exports = { compile, send }
$ mkdir bin/reminders
$ touch bin/reminders/send
$ chmod +x bin/reminders/send
A daily task designed to be run in the server environment will send the mails for us by using our compile and send functions!
#!/usr/bin/env node
if (!process.env.DATABASE_URL) require('dotenv').config()
const db = require('../../initialisers/postgres')
const { compile, send } = require('../../emails')
const run = () => new Promise(async resolve => {
const now = new Date()
now.setUTCHours(0)
now.setUTCMinutes(0)
now.setUTCSeconds(0)
now.setUTCMilliseconds(0)
console.log(`=> Running mailer job for ${now.toISOString()}`)
const { rows: reminders } = await db.query(
'SELECT * FROM reminders WHERE send_at = $1',
[ now ]
)
console.log(` ${reminders.length} outstanding emails found.`)
if (reminders.length < 1) {
console.log(` Zoinks – no emails need sending, let's get out of here!`)
return resolve()
}
for (const reminder of reminders) {
const message = await compile('reminder', reminder)
await send(message)
await db.query('DELETE FROM reminders WHERE reminder_id = $1', [ reminder.reminder_id ])
}
return resolve(0)
})
run()
.then(process.exit)
.catch(error => {
console.error(error)
process.exit(2)
})
$ bin/reminders/send
A "cronjob" is the simplest way of doing this:
0 9 * * * /path_to_app/bin/reminders/send
* Or, we could use Heroku's scheduler
I guess...like...we're kinda done?
I guess we've done it!
It doesn't have to end – I am going to gather feedback, see what people want, and then maybe continue. Let me know what you think please!
Telegram channel: https://t.me/frameworkless
Twitter: @mtimofiiv
See today's code: https://github.com/frameworkless-js/remind.ist/tree/stage/6
See today's lesson running live: https://remind.ist