diff --git a/.gitignore b/.gitignore index add4ce7..d00798d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules .idea -secrets.json \ No newline at end of file +config.json +secrets.json diff --git a/Gruntfile.js b/Gruntfile.js index 89c8ebb..a6ffb5f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -3,35 +3,76 @@ * https://github.com/wildbit/grunt-postmark.git */ -module.exports = function(grunt) { +module.exports = function (grunt) { + var secret = grunt.file.readJSON('secrets.json'); + var config = grunt.file.readJSON('config.json'); grunt.initConfig({ - secrets: grunt.file.readJSON('secrets.json'), + secret: secret, + config: config, /* Postmark - ------------------------------------------------- */ + ------------------------------------------------- */ postmark: { options: { - serverToken: '<%= secrets.serverToken %>' + serverToken: "<%= secret.postmark.server_token %>" }, email: { - from: 'you@youremail.com', - to: 'you@youremail.com', - subject: 'Yo', - src: ['test/email.html'] + from: "<%= config.postmark.from %>", + to: "<%= config.postmark.to %>", + subject: "<%= config.postmark.subject %>", + src: ["test/email.html"] }, bulk: { - from: 'you@youremail.com', - to: 'you@youremail.com', - subject: 'Hey', - src: ['test/*.html'] + from: "<%= config.postmark.from %>", + to: "<%= config.postmark.to %>", + subject: "<%= config.postmark.subject %>", + src: ["test/*.html"] } - } + }, + + "postmark-templates-upload": { + options: { + ephemeralUploadResultsProperty: "<%= config.templates && config.templates.ephemeralUploadResultsProperty %>", + serverToken: "<%= secret.postmark.server_token %>" + }, + test_email: { + name: "testing-postmark-templates-js1-" + new Date().valueOf(), + subject: "Testing grunt-postmark-templates", + htmlSrc: "test/email.html", + textSrc: "test/email.txt" + }, + test_email_again: { + name: "testing-postmark-templates-js2-" + new Date().valueOf(), + subject: "Testing grunt-postmark-templates (again)", + htmlSrc: "test/email.html", + textSrc: "test/email.txt" + }, + test_email_inline_body: { + name: "testing-postmark-templates-js3-" + new Date().valueOf(), + subject: "Testing grunt-postmark-templates (inline body)", + htmlBody: "

Another email test

", + textBody: "Hello from grunt-postmark-templates\n" + } + }, + "postmark-templates-output": { + options: { + cleanOutput: "<%= config.templates && config.templates.cleanOutput %>", + outputFile: "<%= config.templates && config.templates.outputFile || config.templates && config.templates.file %>", + ephemeralUploadResultsProperty: "<%= config.templates && config.templates.ephemeralUploadResultsProperty %>" + } + }, + + "postmark-templates-parse": { + options: { + inputFile: "<%= config.templates && config.templates.inputFile || config.templates && config.templates.file %>" + } + } }); - grunt.loadTasks('tasks'); + grunt.loadTasks("tasks"); - grunt.registerTask('default', ['postmark']); + grunt.registerTask("default", ["postmark", "postmark-templates"]); }; diff --git a/README.md b/README.md index 5239406..f99dca1 100644 --- a/README.md +++ b/README.md @@ -20,38 +20,39 @@ After the plugin is installed, it can be enabled in your Gruntfile: grunt.loadNpmTasks('grunt-postmark'); ``` +You'll need to add a [`config.json`](https://github.com/wildbit/mailmason/wiki/Getting-Started#create-configjson-required) and a [`secrets.json`](https://github.com/wildbit/mailmason/wiki/Getting-Started#create-secretsjson-optional) per the `mailmason` configuration. + ## Postmark task _Run this task with the `grunt postmark` command._ -## Options - +### Options -### serverToken +#### serverToken Your server token can be found on your server’s credentials page on [Postmark’s](http://postmarkapp.com) app. Type: `String` -### from +#### from This is the from address you are using to send the email. This must be a confirmed address that's set up on [Postmark’s](http://postmarkapp.com) sender signatures. Type: `String` -### to +#### to The address you’re sending to. Type: `String` -### subject +#### subject Type: `String` -## Examples +### Examples -### Options specified through target +#### Options specified through target ```javascript grunt.initConfig({ @@ -67,7 +68,7 @@ grunt.initConfig({ }); ``` -### Specify options through targets or globally +#### Specify options through targets or globally Options specified through a target will always take precedence over global options. ```javascript @@ -89,4 +90,181 @@ grunt.initConfig({ } } }); +``` + +## Postmark templates task +_Run this task with the `grunt postmark-templates` command._ + +The `postmark-templates` task is an alias of the `postmark-templates-from-targets` task which is itself a two stepped task – `postmark-templates-upload` followed by `postmark-templates-output`. + +`postmark-templates` (`postmark-templates-from-targets`) is intended for programmatic usage from other grunt tasks. + +### `postmark-templates-upload` Targets + +#### name +The name of your template. + +Type: `String` + + +#### subject +The subject line of your template. + +Type: `String` + +#### htmlSrc +A path to the generated HTML for your template. *Not used if `htmlBody` is specified.* + +Type: `String` + + +#### textSrc +A path to the generated plain text for your template. *Not used if `textBody` is specified.* + +Type: `String` + +#### htmlBody +The generated HTML content of your template. *Not required if `htmlSrc` is specified.* + +Type: `String` + + +#### textBody +The generated plain text content of your template. *Not required if `textSrc` is specified.* + +Type: `String` + + +### `postmark-templates-upload` Options + +#### serverToken +Your server token can be found on your server’s credentials page on [Postmark’s](http://postmarkapp.com) app. + +Type: `String` + + +#### ephemeralUploadResultsProperty +This is the name of a temporary grunt task configuration property used to communicate the upload results between `postmark-templates-upload` and `postmark-templates-output` without having to write a temporary file. **This should be the same value as `ephemeralUploadResultsProperty` for `postmark-templates-output`.** + +Type: `String` + + +### `postmark-templates-output` Options + +#### outputFile +The name of a file to output the results of the upload to Postmark. + +Type: `String` + + +#### cleanOutput +If `true`, do not export `htmlBody`, `htmlSrc`, `textBody` or `textSrc` in the specified `outputFile`. + +Type: `Boolean` + + +#### ephemeralUploadResultsProperty +This is the name of a temporary grunt task configuration property used to communicate the upload results between `postmark-templates-upload` and `postmark-templates-output` without having to write a temporary file. **This should be the same value as `ephemeralUploadResultsProperty` for `postmark-templates-upload`.** + +Type: `String` + + +### Example + +```javascript +grunt.initConfig({ + 'postmark-templates-upload': { + options: { + serverToken: 'POSTMARK_API_TEST', + ephemeralUploadResultsProperty: 'temp' + }, + test_email: { + name: 'testing-postmark-templates-js1', + subject: 'Testing grunt-postmark-templates', + htmlSrc: 'test/email.html', + textSrc: 'test/email.txt' + }, + test_email_inline_body: { + name: 'testing-postmark-templates-js3', + subject: 'Testing grunt-postmark-templates (inline body)', + htmlBody: '

Another email test

', + textBody: 'Hello from grunt-postmark-templates\n' + } + }, + 'postmark-templates-output': { + options: { + cleanOutput: true, + outputFile: 'templates.json', + ephemeralUploadResultsProperty: 'temp' + } + } +}); +``` + +## Postmark templates (from file) task +_Run this task with the `grunt postmark-templates-from-file` command._ + +The `postmark-templates-from-file` task invokes the `postmark-templates` task with targets read from a JSON file (via `postmark-templates-parse`). + +This task is intended for standalone, manual usage. + + +### `postmark-templates-parse` Options + +#### inputFile +The name of a file that specifies templates for uploading to Postmark. These templates take the same shape as defined by `postmark-templates-upload`. This should usually be the same value as `outputFile` for `postmark-templates-output`. + +Type: `String` + + +### Example + +In your `Gruntfile`: + +```javascript +grunt.initConfig({ + 'postmark-templates-parse': { + options: { + inputFile: 'templates.json' + } + }, + 'postmark-templates-upload': { + options: { + serverToken: 'POSTMARK_API_TEST', + ephemeralUploadResultsProperty: 'temp' + } + }, + 'postmark-templates-output': { + options: { + cleanOutput: true, + outputFile: 'templates.json', + ephemeralUploadResultsProperty: 'temp' + } + } +}); +``` + +In the file specified by `inputFile`, in this case, `templates.json`: + +```json +{ + "test_email": { + "name": "testing-postmark-templates-js1", + "subject": "Testing grunt-postmark-templates", + "htmlSrc": "test/email.html", + "textSrc": "test/email.txt" + }, + "test_email_again": { + "name": "testing-postmark-templates-js2", + "subject": "Testing grunt-postmark-templates (again)", + "htmlSrc": "test/email.html", + "textSrc": "test/email.txt" + }, + "test_email_inline_body": { + "name": "testing-postmark-templates-js3", + "subject": "Testing grunt-postmark-templates (inline body)", + "htmlBody": "

Another email test

", + "textBody": "Hello from grunt-postmark-templates\n" + } +} ``` \ No newline at end of file diff --git a/example_config.json b/example_config.json new file mode 100644 index 0000000..04c79a0 --- /dev/null +++ b/example_config.json @@ -0,0 +1,16 @@ +// 1. Copy this file to "config.json" for your own version that won't be +// tracked in source control. +// +// 2. Delete these comments from the configuration so Grunt doesn't get confused +{ + "postmark": { + "from": "jane@example.com", + "to": "jon@example.com", + "subject": "Test Email" + }, + "templates": { + "ephemeralUploadResultsProperty": "postmark-templates-upload-results", + "cleanOutput": true, + "file": "templates.json" + } +} diff --git a/example_secrets.json b/example_secrets.json new file mode 100644 index 0000000..57db34e --- /dev/null +++ b/example_secrets.json @@ -0,0 +1,9 @@ +// 1. Copy this file to "secrets.json" for your own version that won't be +// tracked in source control. +// +// 2. Delete these comments from the configuration so Grunt doesn't get confused +{ + "postmark": { + "server_token": "YOUR_POSTMARK_SERVER_TOKEN_HERE" + } +} diff --git a/example_templates.json b/example_templates.json new file mode 100644 index 0000000..9ccda38 --- /dev/null +++ b/example_templates.json @@ -0,0 +1,20 @@ +{ + "test_email": { + "name": "testing-postmark-templates-js1", + "subject": "Testing grunt-postmark-templates", + "htmlSrc": "test/email.html", + "textSrc": "test/email.txt" + }, + "test_email_again": { + "name": "testing-postmark-templates-js2", + "subject": "Testing grunt-postmark-templates (again)", + "htmlSrc": "test/email.html", + "textSrc": "test/email.txt" + }, + "test_email_inline_body": { + "name": "testing-postmark-templates-js3", + "subject": "Testing grunt-postmark-templates (inline body)", + "htmlBody": "

Another email test

", + "textBody": "Hello from grunt-postmark-templates\n" + } +} \ No newline at end of file diff --git a/package.json b/package.json index caed3b0..f7bea9a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "grunt-postmark", - "version": "0.0.7", + "version": "0.0.8", "description": "Send emails through Postmark using GruntJS", "main": "Gruntfile.js", "homepage": "https://github.com/wildbit/grunt-postmark.git", @@ -18,7 +18,6 @@ "name": "Derek Rushforth", "email": "derek@wildbit.com" }, - "main": "Gruntfile.js", "license": "MIT", "devDependencies": { "grunt": "^1.0.1" diff --git a/secrets_example.json b/secrets_example.json deleted file mode 100644 index a108905..0000000 --- a/secrets_example.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "server_token": "POSTMARK_API_TEST" -} \ No newline at end of file diff --git a/tasks/grunt-postmark-templates.js b/tasks/grunt-postmark-templates.js new file mode 100644 index 0000000..2003fd9 --- /dev/null +++ b/tasks/grunt-postmark-templates.js @@ -0,0 +1,154 @@ +/* + * grunt-postmark-templates + * push templates to a Postmark server for use with SendTemplatedEmail + * + * https://github.com/wildbit/grunt-postmark.git + */ + +module.exports = function (grunt) { + var DEFAULT_EPHEMERAL_UPLOAD_RESULTS_PROPERTY = 'postmark-templates-upload-results'; + var DEFAULT_OUTPUT_FILE_NAME = 'templates.json'; + var DEFAULT_CLEAN_OUTPUT = false; + + grunt.registerTask('postmark-templates-parse', 'Parse Postmark templates for update', function () { + var options = this.options({ + inputFile: DEFAULT_OUTPUT_FILE_NAME + }); + var templateObjects = grunt.file.readJSON(options.inputFile || DEFAULT_OUTPUT_FILE_NAME); + + grunt.config.set('postmark-templates-upload', Object.assign( + templateObjects, + { + options: grunt.config.get('postmark-templates-upload.options') + } + )); + }); + + grunt.registerMultiTask('postmark-templates-upload', 'Create or update Postmark templates', function () { + var done = this.async(); + var options = this.options({ + ephemeralUploadResultsProperty: DEFAULT_EPHEMERAL_UPLOAD_RESULTS_PROPERTY + }); + var ephemeralUploadResultsProperty = options.ephemeralUploadResultsProperty || DEFAULT_EPHEMERAL_UPLOAD_RESULTS_PROPERTY; + var template = this.data; + + var serverToken = options.serverToken || grunt.config('secret.postmark.server_token'); + + if (!serverToken) { + grunt.fail.warn('Missing Postmark server token \n'); + } + + if (!template.name) { + grunt.fail.warn('Missing required template property "name" \n'); + } + + if (!template.subject) { + grunt.fail.warn('Missing required template property "subject" \n'); + } + + if (!template.htmlBody && !template.htmlSrc) { + grunt.log.error('Missing template property "htmlBody" or "htmlSrc"\n'); + } + + if (!template.textBody && !template.textSrc) { + grunt.log.error('Missing template property "textBody" or "textSrc"\n'); + } + + var postmark = require('postmark'); + var client = new postmark.Client(serverToken); + + // read the referenced files, but hold on to the original filenames + var expanded = { + Name: template.name, + Subject: template.subject, + HtmlBody: template.htmlBody || grunt.file.read(template.htmlSrc), + TextBody: template.textBody || grunt.file.read(template.textSrc), + TemplateId: template.templateId + }; + + if (expanded.TemplateId) { + client.editTemplate(expanded.TemplateId, expanded, function (err, response) { + if (err && err.code === 1101) { + grunt.log.warn('Template ' + expanded.TemplateId + ' not found, so attempting create'); + delete template.templateId; + delete expanded.TemplateId; + client.createTemplate(expanded.TemplateId, function (err, response) { + grunt.log.writeln('Template ' + expanded.Name + ' created: ' + JSON.stringify(response.TemplateId)); + handleResponse(err, done, response, template, ephemeralUploadResultsProperty); + }); + } else { + grunt.log.writeln('Template ' + expanded.Name + ' updated: ' + JSON.stringify(response.TemplateId)); + handleResponse(err, done, response, template, ephemeralUploadResultsProperty); + } + }); + } else { + client.createTemplate(expanded, function (err, response) { + grunt.log.writeln('Template ' + expanded.Name + ' created: ' + JSON.stringify(response.TemplateId)); + handleResponse(err, done, response, template, ephemeralUploadResultsProperty); + }); + } + }); + + function handleResponse(err, done, response, template, ephemeralUploadResultsProperty) { + if (err){ + errorMessage(err); + done(); + } else { + template.templateId = response.TemplateId; + // compile the templates for use by the `postmark-templates-output` task + var updatedTemplates = grunt.config.get(ephemeralUploadResultsProperty) || {}; + updatedTemplates[template.name] = template; + grunt.config.set(ephemeralUploadResultsProperty, updatedTemplates); + + done(); + } + } + + function errorMessage(err) { + if (err.message) { + grunt.log.warn('Error: ' + err.message); + } else { + grunt.log.warn('Error: ' + JSON.stringify(err)); + } + } + + // invoke this task after postmark-templates to get an output file containing the resulting template IDs + // this is in the same format as the postmark-templates config. + + grunt.registerTask('postmark-templates-output', 'Write out the resulting template IDs', function () { + var options = this.options({ + cleanOutput: DEFAULT_CLEAN_OUTPUT, + outputFile: DEFAULT_OUTPUT_FILE_NAME, + ephemeralUploadResultsProperty: DEFAULT_EPHEMERAL_UPLOAD_RESULTS_PROPERTY + }); + var ephemeralUploadResultsProperty = options.ephemeralUploadResultsProperty || DEFAULT_EPHEMERAL_UPLOAD_RESULTS_PROPERTY; + var outputFile = options.outputFile || DEFAULT_OUTPUT_FILE_NAME; + var cleanOutput = options.cleanOutput || DEFAULT_CLEAN_OUTPUT; + var updatedTemplates = grunt.config(ephemeralUploadResultsProperty); + var oldTemplates = grunt.file.exists(outputFile) + ? grunt.file.read(outputFile) + : {}; + + Object.keys(updatedTemplates).forEach(function (updatedTemplateKey) { + updatedTemplates[updatedTemplateKey] = Object.assign( + oldTemplates[updatedTemplateKey] || {}, + updatedTemplates[updatedTemplateKey] + ); + + if (cleanOutput) { + delete updatedTemplates[updatedTemplateKey].htmlBody; + delete updatedTemplates[updatedTemplateKey].textBody; + delete updatedTemplates[updatedTemplateKey].htmlSrc; + delete updatedTemplates[updatedTemplateKey].textSrc; + } + }); + + grunt.file.write(outputFile, JSON.stringify(updatedTemplates, null, 2)); + grunt.log.writeln("Updated template information written to " + outputFile); + }); + + // you can also get a JSON report of uploaded templates + grunt.registerTask('postmark-templates-from-targets', ['postmark-templates-upload', 'postmark-templates-output']); + grunt.registerTask('postmark-templates-from-file', ['postmark-templates-parse', 'postmark-templates-from-targets']); + grunt.registerTask('postmark-templates', ['postmark-templates-from-targets']); +}; diff --git a/tasks/grunt-postmark.js b/tasks/grunt-postmark.js index ccf7f8b..e076ac9 100644 --- a/tasks/grunt-postmark.js +++ b/tasks/grunt-postmark.js @@ -3,10 +3,10 @@ * https://github.com/wildbit/grunt-postmark.git */ -'use strict'; - module.exports = function(grunt) { + 'use strict'; + grunt.registerMultiTask('postmark', 'Send emails through Postmark', function() { var done = this.async(); @@ -14,7 +14,7 @@ module.exports = function(grunt) { var _data = this.data; // Check for server token - if (!options.serverToken && !_data.serverToken) { + if (!options.serverToken && !_data.serverToken) { grunt.fail.warn('Missing Postmark server token \n'); } @@ -25,8 +25,8 @@ module.exports = function(grunt) { if (this.files.length > 0) { var message = { - 'From': _data.from || options.from, - 'To': _data.to || options.to, + 'From': _data.from || options.from, + 'To': _data.to || options.to, 'Subject': _data.subject || options.subject }; @@ -56,7 +56,7 @@ module.exports = function(grunt) { handleResponse(err, response, done); }); } - + } else { // Warn about no files being passed to task grunt.log.warn('No src file found \n'); @@ -66,7 +66,13 @@ module.exports = function(grunt) { function handleResponse(err, response, done) { - err ? errorMessage(err) : successMessage(response); + if (err) { + errorMessage(err); + + } else { + successMessage(response); + } + done(); } diff --git a/test/email.txt b/test/email.txt new file mode 100644 index 0000000..e5494a8 --- /dev/null +++ b/test/email.txt @@ -0,0 +1 @@ +Hello from grunt-postmark-templates