/* 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; } } }