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