Skip to content

Instantly share code, notes, and snippets.

@samholmes

samholmes/app.js Secret

Last active July 4, 2020 09:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save samholmes/388ca4552c5936b52c5d to your computer and use it in GitHub Desktop.
Save samholmes/388ca4552c5936b52c5d to your computer and use it in GitHub Desktop.
// Load config
global.config = require('./config');
var http = require('http'),
emailer = require('./emailer'),
mysql = require('mysql'),
fs = require('fs'),
paralyze = require('paralyze'),
async = require('async');
var APP_PORT = process.env.APP_PORT || 8000;
/**
* Database
*/
var db = mysql.createConnection({
host: global.config.db.host,
database: global.config.db.database,
user: global.config.db.user,
password: global.config.db.password
});
db.config.queryFormat = function(query, values){
if (!values) return query;
return query.replace(/\:(\w+)/g, function (txt, key) {
if (values.hasOwnProperty(key)) {
return this.escape(values[key]);
}
return txt;
}.bind(this));
};
/**
* Functions
*/
var funcs = require('./funcs.js')(db);
/**
* HTTP Template Previews
*/
var server = http.createServer();
server
.on('request', function(req, res){
var parts = req.url.substr(1).split('/'),
template = parts[0],
domain = parts[1];
if (!template || !domain) {
return error("Please provide a template name and domain in the URL (e.g. SECRET)");
}
funcs.getEmailTemplateLocals({
domain: domain
},
getEmailTemplateLocalsCallback)
function getEmailTemplateLocalsCallback(err, locals){
if (err) return error(err);
emailer.renderEmail({
template: template,
locals: locals
},
renderEmailCallback);
}
function renderEmailCallback(err, html){
if (err) return error(err);
res.end(html);
}
function error(err){
if (typeof err == 'string')
err = new Error(err);
res.statusCode = 500;
res.end(err.stack);
}
})
.listen(APP_PORT,
function(){
console.log("HTTP server listening on " + APP_PORT)
startReplicon();
});
/**
* Replicon Commands
*/
function startReplicon(){
var replicon = require('./replicon')
/**
* Emailing
*
* send-email
* Sends an email template from a specified site to a specified subscriber.
*
* blast-emails
* Sends an email template to subscribers for a specified site (by domain).
*/
replicon
.add('send-email', function(template, domain, subscriberId){
return function(cb){
funcs.getEmailTemplateLocals({
domain: domain,
subscriberId: subscriberId
},
getEmailTemplateLocalsCallback);
function getEmailTemplateLocalsCallback(err, locals){
if (err) return error(err);
emailer.sendTemplate({
to: locals.subscriber.email,
from: locals.site.title+" <"+locals.site.email+">",
template: template,
locals: locals
},
sendTemplateCallback);
}
function sendTemplateCallback(err){
if (err) return error(err);
cb(null);
}
function error(err){
cb(err);
}
}
})
.doc('send-email', "\n\
Sends an email template from a specified site to a specified subscriber.\n\
\n\
Syntax:\n\
send-email [template] [domain] [subscriberId]");
replicon
.add('blast-emails', function(template, domain, offset){
offset = offset || 0;
return function(cb){
var subscribers
funcs.getEmailTemplateLocals({
domain: domain
},
getEmailTemplateLocalsCallback);
function getEmailTemplateLocalsCallback(err, locals){
if (err) return error(err);
// First warn about NULL accessKeys.
// Count number of rows with NULL accessKeys
db.query("SELECT COUNT(*) nullAccessKeysCount \
FROM subscribers \
JOIN site_subscribers ON site_subscribers.subscriberId = subscribers.id \
WHERE \
site_subscribers.siteId = :siteId \
AND NOT subscribers.dnd \
AND subscribers.accessKey IS NULL",
{
siteId: locals.site.id
},
selectCountOfNullAccessKeysQueryHandler);
function selectCountOfNullAccessKeysQueryHandler(err, results){
if (err) return error(err);
var row = results[0];
if (row && row.nullAccessKeysCount) {
return error("Some subscribers have NULL access keys. Run gen-access-keys command.");
}
db.query("SELECT subscribers.* \
FROM subscribers \
JOIN site_subscribers ON site_subscribers.subscriberId = subscribers.id \
WHERE \
site_subscribers.siteId = :siteId \
AND NOT subscribers.dnd \
LIMIT 1000000000 OFFSET :offset",
{
siteId: locals.site.id,
offset: offset
},
selectSubscribersQueryHandler)
}
function selectSubscribersQueryHandler(err, results){
if (err) return error(err);
subscribers = results;
async.eachSeries(subscribers, function(subscriber, cb){
locals.subscriber = subscriber;
locals.subscriber.akey = locals.subscriber.accessKey.toString('base64');
emailer.sendTemplate({
to: locals.subscriber.email,
from: locals.site.title+" <"+locals.site.email+">",
template: template,
locals: locals
},
function(err){
if (err) return cb(err);
console.log("Sent to "+subscriber.email);
cb();
});
},
function(err){
if (err) return error(err);
console.log("Finished!");
done();
});
}
}
function done(){
cb(null, "Sent "+subscribers.length+" emails.");
}
function error(err){
err = new Error(err);
cb(err.stack);
}
}
})
.doc('blast-emails', "\n\
Sends an email template to subscribers for a specified site (by domain) at an optional offset.\n\
\n\
Syntax:\n\
blast-emails [template] [domain] [offset]");
/**
* Access Keys
*
* gen-access-keys
* Generates accessKey column for all subscribers in the database
* that have a NULL accessKey.
*
* get-access-key
* Returns the accessKey for a specified subscriber encoded in base64.
*/
replicon
.add('gen-access-keys', function(){
var crypto = require('crypto');
return function(cb){
db.query("SELECT id FROM subscribers WHERE accessKey IS NULL",
selectSubscribersQueryHandler);
function selectSubscribersQueryHandler(err, results){
if (err) return cb(err);
var wait = paralyze(done);
results.forEach(function(row){
var subscriberId = row.id;
crypto.randomBytes(256, wait(function(err, buf) {
if (err) return cb(err);
var accessKey = buf;
db.query("UPDATE subscribers \
SET accessKey = :accessKey \
WHERE id = :subscriberId",
{
accessKey: accessKey,
subscriberId: subscriberId
},
wait(function(err, results){
if (err) return cb(err);
}));
}));
})
}
function done(){
cb(null);
}
}
})
.doc('gen-access-keys', "\n\
Generates accessKey column for all subscribers in the database\n\
that have a NULL accessKey.\n\
\n\
Syntax:\n\
gen-access-keys");
replicon
.add('get-access-key', function(subscriberId){
return function(cb){
db.query("SELECT accessKey FROM subscribers WHERE id = :subscriberId LIMIT 1",
{
subscriberId: subscriberId
},
function(err, results){
if (err) return cb(err);
if (!results.length)
return cb(new Error("Could not find subscriber with that ID"));
var accessKeyBuffer = results[0].accessKey;
var accessKey = accessKeyBuffer.toString('base64');
cb(null, accessKey);
})
}
})
.doc('get-access-key', "\n\
Returns the accessKey for a specified subscriber encoded in base64.\n\
\n\
Syntax:\n\
get-access-key [subscriberId]")
/**
* Templates
*
* template-list
* List all available email templates.
*/
replicon
.add('template-ls', function(){
return function(cb){
emailer.getEmailTemplateNames(function(err, names){
if (err) return cb(err);
cb(null, names.join(", "));
})
}
})
.doc('template-ls', "\n\
Returns a list of all template names.\n\
\n\
Syntax:\n\
template-ls");
/**
* Subscribers
*
* count-subscribers
* Counts the number of subscribers for a specific site or for all sites.
*/
replicon
.add('count-subscribers', function(domain){
return function(cb){
db.query("SELECT dnd, COUNT(*) subscriberCount \
FROM site_subscribers \
JOIN sites ON sites.id = site_subscribers.siteId \
JOIN subscribers ON subscribers.id = site_subscribers.subscriberId \
WHERE \
IF (LENGTH(:domain), sites.domain = :domain, TRUE) \
GROUP BY dnd",
{
domain: domain
},
function(err, results){
if (err) return error(err);
var subscriberCount = 0;
var dndCount = 0;
results.forEach(function(row){
if (row.dnd) {
dndCount = row.subscriberCount;
}
else {
subscriberCount = row.subscriberCount;
}
})
var output = subscriberCount + (dndCount && " ("+dndCount+" dnd)") + " for ";
output += domain ? domain : "all sites";
cb(null, output);
});
function error(err){
cb(new Error(err));
}
}
})
.doc('count-subscribers', "\n\
Counts the number of subscribers for a specific domain.\n\
\n\
Syntax:\n\
count-subscribers [domain]");
// End Replicon
}
var nodemailer = require('nodemailer'),
mandrillTransport = require('nodemailer-mandrill-transport'),
htmlToText = require('nodemailer-html-to-text').htmlToText,
stylus = require('stylus'),
nib = require('nib'),
jade = require('jade'),
juice = require('juice'),
async = require('async'),
fs = require('fs'),
path = require('path'),
cheerio = require('cheerio');
var templateDir = './emails/';
var transport = nodemailer.createTransport(mandrillTransport({
auth: {
apiKey: global.config.smtp.password
}
}));
transport.use('compile', htmlToText());
function sendTemplate(info, cb){
cb = cb || new Function;
if (!info.to)
return error(new Error('missing info.to'));
if (!info.from)
return error(new Error('missing info.from'));
if (!info.template && !info.templateFiles)
return error(new Error('missing info.template or info.templateFiles'));
var locals = info.locals || {};
renderEmail({
template: info.template,
templateInfo: info.templateInfo,
locals: locals
}, renderEmailHandler);
function renderEmailHandler(err, html){
if (err) return error(err);
if (!info.subject) {
var $ = cheerio.load(html);
info.subject = $('title').text();
}
transport.sendMail({
from: info.from,
to: info.to,
subject: info.subject,
html: html
},
function(err, responseStatus){
if (err) return error(err);
cb(null);
})
}
function error(err){
cb(err);
}
}
function renderEmail(info, cb){
if (info.template) {
getEmailTemplateFiles({
template: info.template
},
function(err, templateFiles){
if (err) return error(err);
processTemplateFiles(templateFiles);
})
}
else if (info.templateFiles) {
processTemplateFiles(info.templateFiles);
}
else {
return error(new Error('missing info.template or info.templateFiles'));
}
function processTemplateFiles(templateFiles){
var locals = info.locals || {};
// Store jade filename in locals (required by jade for includes and layouts)
locals.filename = templateFiles.jadeFilename;
jade.render(templateFiles.jadeContent, locals, function(err, html){
if (err) return error(err);
html = juice.inlineContent(html, templateFiles.cssContent);
cb(null, html);
});
}
function error(err){
cb(new Error(err));
}
}
function getEmailTemplateFiles(info, cb){
if (!info.template)
return error(new Error('missing info.template'));
var templateName = info.template,
jadeFilename = templateDir + templateName + '/content.jade',
stylFilename = templateDir + templateName + '/style.styl';
var outfo = {
jadeFilename: jadeFilename
};
// Get Jade and Styl files
async.parallel({
jade: async.apply(fs.readFile, jadeFilename, 'utf8'),
styl: async.apply(fs.readFile, stylFilename, 'utf8')
},
function(err, fileContents){
if (err) return error(err);
// Jade
outfo.jadeContent = fileContents.jade;
// CSS
stylus(fileContents.styl)
.set('compile', true)
.use(nib())
.render(function(err, css){
if (err) return error(err);
outfo.cssContent = css;
cb(null, outfo);
});
});
function error(err){
cb(new Error(err));
}
}
function getEmailTemplateNames(info, cb){
if (typeof info === 'function') {
cb = info;
info = {};
}
fs.readdir(templateDir, function(err, files){
if (err) return error(err);
var files = files.filter(function(file){
var stat = fs.statSync(path.join(templateDir, file));
return stat.isDirectory();
});
cb(null, files);
})
function error(err){
cb(new Error);
}
}
exports.sendTemplate = sendTemplate;
exports.renderEmail = renderEmail;
exports.getEmailTemplateFiles = getEmailTemplateFiles;
exports.getEmailTemplateNames = getEmailTemplateNames;
var paralyze = require('paralyze'),
fs = require('fs');
module.exports = function(db){
funcs.db = db;
return funcs;
}
var funcs = {};
/**
* Get Email Template Locals
*/
funcs.getEmailTemplateLocals = function(info, cb){
var locals = {};
if (!info.domain)
return error("Missing info.domain");
// To be used for URLs (must get rid of invalid filename characters)
locals.domainFilename = info.domain.replace(/[^a-z0-9.-]/gi, '');
// www subdomain support
if (locals.domainFilename.indexOf('www.') === 0)
locals.domainFilename = locals.domainFilename.substr(4);
var wait = paralyze(done);
this.db.query("SELECT * FROM sites WHERE domain = :domain LIMIT 1",
{
domain: info.domain
},
wait(function(err, results){
if (err) return error(err);
if (!results.length)
return error("Site not found");
locals.site = results[0];
try {
locals.siteData = JSON.parse(locals.site.siteData);
}
catch (err) {
return error(err);
}
}));
if (info.subscriberId) {
this.db.query("SELECT * FROM subscribers WHERE id = :subscriberId LIMIT 1",
{
subscriberId: info.subscriberId
},
wait(function(err, results){
if (err) return error(err);
if (!results.length)
return error("Subscriber not found");
locals.subscriber = results[0];
locals.subscriber.akey = locals.subscriber.accessKey.toString('base64');
}));
}
function done(){
cb(null, locals);
}
function error(err){
err = new Error(err);
cb(err);
}
}
0 info it worked if it ends with ok
1 verbose cli [ '/root/.nvm/versions/io.js/v1.6.1/bin/iojs',
1 verbose cli '/root/.nvm/versions/io.js/v1.6.1/bin/npm',
1 verbose cli 'run',
1 verbose cli 'live' ]
2 info using npm@2.7.1
3 info using node@v1.6.1
4 verbose run-script [ 'prelive', 'live', 'postlive' ]
5 info prelive emailer@0.0.0
6 info live emailer@0.0.0
7 verbose unsafe-perm in lifecycle true
8 info emailer@0.0.0 Failed to exec live script
9 verbose stack Error: emailer@0.0.0 live: `NODE_ENV=production node app.js`
9 verbose stack Exit status 137
9 verbose stack at EventEmitter.<anonymous> (/root/.nvm/versions/io.js/v1.6.1/lib/node_modules/npm/lib/utils/lifecycle.js:213:16)
9 verbose stack at emitTwo (events.js:87:13)
9 verbose stack at EventEmitter.emit (events.js:169:7)
9 verbose stack at ChildProcess.<anonymous> (/root/.nvm/versions/io.js/v1.6.1/lib/node_modules/npm/lib/utils/spawn.js:14:12)
9 verbose stack at emitTwo (events.js:87:13)
9 verbose stack at ChildProcess.emit (events.js:169:7)
9 verbose stack at maybeClose (child_process.js:984:16)
9 verbose stack at Process.ChildProcess._handle.onexit (child_process.js:1057:5)
10 verbose pkgid emailer@0.0.0
11 verbose cwd /apps/emailer
12 error Linux 3.2.0-4-amd64
13 error argv "/root/.nvm/versions/io.js/v1.6.1/bin/iojs" "/root/.nvm/versions/io.js/v1.6.1/bin/npm" "run" "live"
14 error node v1.6.1
15 error npm v2.7.1
16 error code ELIFECYCLE
17 error emailer@0.0.0 live: `NODE_ENV=production node app.js`
17 error Exit status 137
18 error Failed at the emailer@0.0.0 live script 'NODE_ENV=production node app.js'.
18 error This is most likely a problem with the emailer package,
18 error not with npm itself.
18 error Tell the author that this fails on your system:
18 error NODE_ENV=production node app.js
18 error You can get their info via:
18 error npm owner ls emailer
18 error There is likely additional logging output above.
19 verbose exit [ 1, true ]
{
"name": "emailer",
"version": "0.0.0",
"description": "SECRET emailer program",
"main": "emailer.js",
"scripts": {
"dev": "APP_PORT=8001 node app.js",
"live": "NODE_ENV=production node app.js"
},
"author": "Samuel Holmes",
"license": "ISC",
"dependencies": {
"async": "^0.9.0",
"cheerio": "^0.19.0",
"jade": "^1.9.2",
"juice": "^1.0.0",
"mysql": "^2.6.2",
"nib": "^1.1.0",
"nodemailer": "^1.3.2",
"nodemailer-html-to-text": "^1.0.1",
"nodemailer-mandrill-transport": "^0.1.1",
"paralyze": "^1.0.0",
"stylus": "^0.50.0"
}
}
var readline = require('readline');
/**
* Init
*/
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
completer: completer
});
rl
.on('line', processLine)
.on('close', function() {
echo('')
process.exit(0);
});
rl.setPrompt('# ');
rl.prompt();
/**
* Private Variables
*/
var _commands = {},
_docs = {};
/**
* Private Methods
*/
function completer(line) {
var completions = Object.keys(_commands);
var hits = completions.filter(function(c) { return c.indexOf(line) == 0 })
// show all completions if none found
return [hits, line]
}
function processLine(line){
var args = prepareArgs(line);
var commandName = args.shift(),
args = washArguments(args);
var command = _commands[commandName];
if (typeof command === 'function') {
var cb = command.apply(null, args);
// When command returned a function, it means command is async
if (typeof cb === 'function') {
// Pause the readline interface...
rl.pause();
// ...until command is finished
cb(function(err, output){
if (err) echo(err);
if (typeof output !== 'undefined') echo(output);
rl.prompt();
});
}
else {
// Otherwise, the command already executed, therefore initiate another prompt
rl.prompt();
}
}
else {
echo("Command not found: "+commandName);
}
}
function prepareArgs(line){
var args = [];
// Created an array were even elements are double-quoted strings
var stripedArray = line.split('"');
// Transform striped array into final args array
stripedArray.forEach(function(e, i){
// Don't process even elements because even elements are strings
if (i % 2) {
// Even elements are directly pushed into args array (don't mess with a string's spaces).
// However, add the double quotes back to explicitly say this is a string type
args.push('"'+e+'"');
}
else {
// Odd elements are processed for spaces and then concatenated to args
e = e.trim();
if (e) {
args = args.concat(e.split(/\s+/));
}
}
});
return args;
}
function washArguments(args){
var washedArgs = [];
args.forEach(function(arg){
arg = arg.trim();
// Numbers
if (/^\d+(\.\d+)?$/.test(arg)) {
arg = Number(arg);
}
// Explicit Strings
else if (/^".*"$/.test(arg)) {
arg = arg.substring(1, arg.length-1)
}
washedArgs.push(arg);
});
return washedArgs;
}
function echo(){
console.log.apply(console, arguments);
}
/**
* Public Methods
*/
exports.add = function(commandName, fn){
_commands[commandName] = fn;
return exports;
};
exports.doc = function(commandName, docs){
// Support for removal of preceeding tabs/spaces on multi-line strings
// For mult-line tab/space truncation, strings must begin with a new-line
if (docs[0] === '\n') {
var measurements = /^\n(\t+|\s+)/.exec(docs);
docs = docs.replace(RegExp(measurements[0]), '').replace(RegExp(measurements[1], 'g'), '');
}
_docs[commandName] = docs;
return exports;
};
/**
* Built-in Commands
*/
exports.add('help', function(commandName){
if (!commandName) {
var methods = Object.keys(_commands);
echo("Available commands: \n");
echo(methods.join(', ')+"\n");
echo("Type `help command` for more information.");
}
else {
var command = _commands[commandName]
if (typeof command !== 'function')
return echo("Command not found: "+commandName);
if (_docs[commandName]) {
echo(_docs[commandName]);
}
else {
var sig = /^function\s*\(([^)]*)\)/.exec(command.toString())[1].replace(',', '');
echo([commandName, sig].join(' '));
}
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment