Added working Polymail app and default example newsletter. You can send newsletters in markdown with HTML generation with this!

master
kts of kettek (muzukashi) 2015-12-19 03:19:23 -08:00
commit 3c1a783bca
8 changed files with 333 additions and 0 deletions

3
.gitignore vendored 100644
View File

@ -0,0 +1,3 @@
node_modules
.DS_Store
*.swp

222
app.js 100644
View File

@ -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;
}
}
}

View File

@ -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
}
}

View File

@ -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;

View File

@ -0,0 +1,6 @@
{
"dependencies": {
"dot": "^1.0.3",
"markdown-it": "^5.0.2"
}
}

View File

@ -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>

View File

@ -0,0 +1,3 @@
# {{=it.heading || ''}} {{=it.title || ''}} {{=it.timestamp || ''}}
{{=it.content || ''}}

25
package.json 100644
View File

@ -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"
}
}