Added working Polymail app and default example newsletter. You can send newsletters in markdown with HTML generation with this!
commit
3c1a783bca
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
*.swp
|
|
@ -0,0 +1,222 @@
|
|||
/* 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 = {};
|
||||
this.repo = {};
|
||||
this.subscribers = [];
|
||||
this.options = {};
|
||||
this.transport_options = {
|
||||
host: 'localhost',
|
||||
port: 25
|
||||
};
|
||||
this.transport = null;
|
||||
// merge passed Object into Newsletter
|
||||
for (attr in data) {
|
||||
if (this[attr]) this[attr] = mergeObjects(this[attr], data[attr]);
|
||||
else this[attr] = data[attr];
|
||||
}
|
||||
}
|
||||
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;
|
||||
};
|
||||
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 : '')
|
||||
});
|
||||
};
|
||||
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 (var attr in obj2) { obj3[attr] = obj2[attr]; }
|
||||
|
||||
return obj3;
|
||||
};
|
||||
exports.mergeObjects = mergeObjects;
|
||||
/* Function: sendNewsletter(newsletter object, options, letter, file)
|
||||
````````````````````````````````
|
||||
General function that iteratively calls:
|
||||
build
|
||||
send
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"options": {
|
||||
"from": "Polymail <user@domain>",
|
||||
"heading": "Polymail"
|
||||
},
|
||||
"subscribers": [
|
||||
"user@domain"
|
||||
],
|
||||
"transport_options": {
|
||||
"host": "",
|
||||
"port": 25,
|
||||
"maxConnections": 5,
|
||||
"maxMessages": 10,
|
||||
"rateLimit": 5
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/* ==== MODULE ============================================================== */
|
||||
var polymail = require.main.exports;
|
||||
var newsletter = new polymail.Newsletter(require('./config.json'));
|
||||
/* ==== REQUIRES ============================================================ */
|
||||
var fs = require('fs');
|
||||
var nodemailer = require('nodemailer');
|
||||
var smtpPool = require('nodemailer-smtp-pool');
|
||||
|
||||
var doT = require('dot');
|
||||
var md = require('markdown-it')({
|
||||
typographer: true
|
||||
});
|
||||
|
||||
/* ==== NEWSLETTER ========================================================== */
|
||||
// our custom build step
|
||||
newsletter.build = function(file, options, cb) {
|
||||
// clear potential old newsletter
|
||||
newsletter.temp = {};
|
||||
// merge passed options with newsletter defaults
|
||||
var opts = polymail.mergeObjects(newsletter.options, options);
|
||||
// read our provided content file
|
||||
var content = fs.readFileSync(file);
|
||||
opts.content = content;
|
||||
// read our markdown template and build it
|
||||
var md_source = fs.readFileSync(__dirname+'/templates/md.dot');
|
||||
var md_template = doT.template(md_source);
|
||||
var md_result = md_template(opts);
|
||||
// convert our content md into html
|
||||
opts.content = md.render(opts.content.toString());
|
||||
// read our html template and build it
|
||||
var html_source = fs.readFileSync(__dirname+'/templates/html.dot');
|
||||
var html_template = doT.template(html_source);
|
||||
var html_result = html_template(opts);
|
||||
// set our temporary mail objects
|
||||
newsletter.temp.subject = opts.heading + (opts.title ? ' '+opts.title : '');
|
||||
newsletter.temp.text = md_result;
|
||||
newsletter.temp.html = html_result;
|
||||
};
|
||||
|
||||
// create our transport
|
||||
newsletter.transport = nodemailer.createTransport(smtpPool(newsletter.transport_options));
|
||||
|
||||
module.exports = newsletter;
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"dot": "^1.0.3",
|
||||
"markdown-it": "^5.0.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>{{=it.heading || ''}} {{=it.title || ''}}</title>
|
||||
<style>
|
||||
p {
|
||||
color: #111;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{=it.heading || ''}} {{=it.title || ''}}</h1>
|
||||
<small>{{=it.timestamp}}</small>
|
||||
{{=it.content || ''}}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,3 @@
|
|||
# {{=it.heading || ''}} {{=it.title || ''}} {{=it.timestamp || ''}}
|
||||
|
||||
{{=it.content || ''}}
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "polymail",
|
||||
"version": "0.0.1",
|
||||
"description": "Fairly basic newsletter service",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://git.kettek.net/G/polymail.git"
|
||||
},
|
||||
"keywords": [
|
||||
"newsletter",
|
||||
"service",
|
||||
"email"
|
||||
],
|
||||
"author": "kts of kettek",
|
||||
"license": "GPLv3",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.0",
|
||||
"nodemailer": "^1.10.0",
|
||||
"nodemailer-smtp-pool": "^1.1.5"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue