polymail/app.js

242 lines
8.5 KiB
JavaScript

/* app.js - Polymail
````````````````````````````````````````````````````````````````````````````````
Polymail is a console Node.js application for sending newsletters.
Intent:
* store newsletters as Markdown
* use git as an optional backend for storing sent newsletters
* create HTML from Markdown
* send both Markdown and HTML as separate body parts
Newsletter:
* archive (git repo?)
* template file
* from, title
* recipients
* transport (smtp, etc.)
Usage:
./app.js polymathic_letters send 'my_markdown' --title="My Fine Time"
./app.js NEWSLETTER COMMAND FILE --VAR=VALUE
*/
/* ==== REQUIRES ============================================================ */
var fs = require('fs');
var path = require('path');
var nodemailer = require('nodemailer');
var argv = require('minimist')(process.argv.slice(2));
/* ==== CLASS EXPORTS ======================================================= */
/* Class: Newsletter
````````````````````````````````
This class is the core class that custom newsletters are based upon.
IMPORTANT NOTE:
This object can be extended to create additional functionality in a Newsletter
This can include, but is not limited to, alternative transport(s), different
build steps for using formats such as Markdown or otherwise, different send
steps for using alternative sending logic, and different archiving methods
such as using git or subversion repositories.
In general, newsletters are composed of a directory in the newsletters/
directory with index.js populated with an extended Newsletter module
inheriting from this Newsletter object.
The Newsletter object has the following properties:
* subscribers
- array of subscriber emails
* repo
- optional object containing information pertaining to a repo archive
* options
- options object of key=>value pairs for template-based generation of the
newsletter text and/or HTML output.
* transport_options
- All options used for the transport - basically what should be passed to
nodemailer.createTransport or similar.
* transport
- nodemailer Transport object used during the `send` step
* temp
- object containing text and html properties for the processed newsletter -
this is what is sent during the `send` step
There are three exposed method(s) that may be replaced by user implementation.
These are:
build(file, options, cb)
* Step for compiling the newsletter from the given file applying the
appropriate options during creation. This populates the temp property of the
Newsletter object.
send()
* Used to send the given newsletter email to all recipients. The newsletter
is compiled by the `build` step as the temp property of the Newsletter.
archive(letter, file)
* Optional step used to archive the newsletter to an archive location. This
can just be a location on the filesystem or a git repository if desired.
*/
function Newsletter(data) {
this.temp = {}; // temporary Letter object (html, text, subject)
this.repo = {};
this.subscribers = []; // array of email addresses
this.options = {}; // key=>value pairs used during building
this.transport = null; // future Transport object
this.transport_options = { // options to be passed to the Transport object
host: 'localhost',
port: 25
};
// merge passed Object into Newsletter
for (var attr in data) {
if (this[attr]) this[attr] = mergeObjects(this[attr], data[attr]);
else this[attr] = data[attr];
}
}
/* Method: Newsletter.build(file, options, cb)
````````````````````````````````
This method is the first step during the sending of a newsletter. It attempts
to read in the given file name, optionally replacing particular keys in the file
with the variables stored in the options object, and eventually calling cb once
finished.
*/
Newsletter.prototype.build = function(file, options, cb) {
// clear potential old newsletter
this.temp = {};
// merge passed options with this defaults
var opts = mergeObjects(options, this.options);
// read our provided content file
var content = fs.readFileSync(file);
// create the data to be sent
this.temp.subject = opts.heading + (opts.title ? ' '+opts.title : '');
this.temp.text = content;
this.temp.html = content;
};
/* Method: Newsletter.send()
````````````````````````````````
This method is the second step of sending a newsletter. It takes the data built
during the `build` method, as stored in the `temp` property, and attempts to
send it to all the emails stored in the `subscribers` property through the
nodemailer Transport as stored in the `transport` property.
*/
Newsletter.prototype.send = function() {
var i = 0;
var len = this.subscribers.length;
var sendMail = function(newsletter, i, len, data) {
data.to = newsletter.subscribers[i];
if (i++ < len) {
process.stdout.clearLine();
process.stdout.cursorTo(0);
process.stdout.write(i+'/'+len+': '+data.to+'... ');
newsletter.transport.sendMail(data, function (err, info) {
if (err) {
console.log('Error\n'+err);
} else {
if (info.accepted && info.accepted.length > 0) {
console.log('Sent');
} else if (info.rejected && info.rejected.length > 0) {
console.log('Rejected');
} else if (info.pending && info.pending.length > 0) {
console.log('Pending');
}
sendMail(newsletter, i, len, data);
}
});
} else {
console.log('Finished!');
newsletter.transport.close();
}
};
sendMail(this, i, len, {
from: this.options.from,
subject: this.temp.subject,
text: (this.temp.text ? this.temp.text : ''),
html: (this.temp.html ? this.temp.html : '')
});
};
/* Method: archive(letter, file)
````````````````````````````````
This method is the third step of sending a newsletter. It simply archives the
given file in the 'archive/' directory of the provided newsletter name.
*/
Newsletter.prototype.archive = function(letter, file) {
fs.writeFileSync('./newsletters/'+letter+'/archive/'+path.basename(file), fs.readFileSync(file));
};
exports.Newsletter = Newsletter;
/* Function: mergeObjects(obj1, obj2)
````````````````````````````````
This function returns a new object that is the result of merging object 2 into
object 1.
*/
var mergeObjects = function(obj1, obj2) {
if (obj1 instanceof Array) obj3 = [];
else obj3 = {};
for (var attr in obj1) { obj3[attr] = obj1[attr]; }
for (attr in obj2) { obj3[attr] = obj2[attr]; }
return obj3;
};
exports.mergeObjects = mergeObjects;
/* Function: sendNewsletter(newsletter object, options, letter, file)
````````````````````````````````
General function that iteratively calls:
newsletter.build
newsletter.send
newsletter.archive
*/
var sendNewsletter = function(newsletter, options, letter, file) {
newsletter.build(file, options, function(err) {});
newsletter.send();
newsletter.archive(letter, file);
};
exports.sendNewsletter = sendNewsletter;
/* ==== MAIN ================================================================ */
if (require.main == module) { // run as an app only if this is the main module
if (argv._.length < 3) {
console.log('err, 3 args required');
} else if (argv._.length > 3) {
console.log('err, too many args');
} else {
var letter = argv._[0];
var cmd = argv._[1];
var file = argv._[2];
// remove letter, cmd, and file from argv so we just pass argv as the options
delete argv._;
// attempt to load the given letter
try {
var nl = require('./newsletters/'+letter+'/index.js');
if (nl.build == Newsletter.prototype.build) {
console.log('I: Newsletter is using default `build` step');
}
if (nl.archive == Newsletter.prototype.archive) {
console.log('I: Newsletter is using default `archive` step');
}
if (nl.transport === null) {
// create default transport
console.log('I: Newsletter has no custom transport, creating default');
nl.transport = nodemailer.createTransport(nl.transport_options);
}
} catch(ex) {
console.log("E: Failure while loading newsletter:\n\t"+ex.stack);
process.exit(1);
}
// run the provided command
switch(cmd) {
case 'send':
try {
sendNewsletter(nl, argv, letter, file);
} catch (ex) {
console.log("E: Failure while sending newsletter:\n\t"+ex.stack);
process.exit(100);
}
break;
default:
console.log('err, unhandled cmd: '+cmd);
break;
}
}
}