243 lines
8.5 KiB
JavaScript
243 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 : ''),
|
|
attachments: (this.temp.attachments ? this.temp.attachments : [])
|
|
});
|
|
};
|
|
/* 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;
|
|
}
|
|
}
|
|
}
|