├── .gitignore ├── index.js ├── circle.yml ├── test ├── fixtures │ └── views │ │ └── user │ │ └── welcome.ejs ├── test.js ├── views │ └── notifier.eco └── mailman.test.js ├── Makefile ├── lib ├── util.js └── mailman.js ├── package.json └── Readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./build/mailman'); 2 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 0.11.14 4 | -------------------------------------------------------------------------------- /test/fixtures/views/user/welcome.ejs: -------------------------------------------------------------------------------- 1 | Welcome, <%= full_name %> -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | require('babel/register'); 2 | 3 | require('./mailman.test'); 4 | -------------------------------------------------------------------------------- /test/views/notifier.eco: -------------------------------------------------------------------------------- 1 | Hey, <%= @name %>
2 | 3 | We finally launched, thanks for all your support! 4 | 5 | You rock! -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRC = $(wildcard lib/*.js) 2 | DEST = $(SRC:lib/%.js=build/%.js) 3 | 4 | build: $(DEST) 5 | build/%.js: lib/%.js 6 | mkdir -p $(@D) 7 | ./node_modules/.bin/babel --optional runtime $< -o $@ 8 | 9 | clean: 10 | rm -rf build 11 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | 5 | var fs = require('fs'); 6 | 7 | 8 | /** 9 | * Expose utilities 10 | */ 11 | 12 | var exports = module.exports; 13 | 14 | exports.walk = walk; 15 | 16 | 17 | /** 18 | * Utilities 19 | */ 20 | 21 | function walk (path) { 22 | let files = []; 23 | 24 | // readdir returns only file names 25 | // convert them to full path 26 | files = fs.readdirSync(path).map(file => `${path}/${file}`); 27 | 28 | let index = 0; 29 | let file; 30 | 31 | while (file = files[index++]) { 32 | let stat = fs.lstatSync(file); 33 | // if directory append its contents 34 | // after current array item 35 | if (stat.isDirectory()) { 36 | let args = walk(file); 37 | args.unshift(index - 1, 1); 38 | files.splice.apply(files, args); 39 | } 40 | } 41 | 42 | return files; 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mailman", 3 | "version": "0.3.3", 4 | "description": "Send emails in a comfortable way via models.", 5 | "keywords": [ 6 | "co", 7 | "co-mail", 8 | "co-email", 9 | "mail", 10 | "email", 11 | "sendmail", 12 | "smtp", 13 | "imap", 14 | "sendgrid", 15 | "es6" 16 | ], 17 | "author": "Vadim Demedes ", 18 | "main": "./index.js", 19 | "scripts": { 20 | "test": "./node_modules/.bin/mocha --harmony test/test" 21 | }, 22 | "license": "MIT", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/vdemedes/mailman" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/vdemedes/mailman/issues" 29 | }, 30 | "engines": { 31 | "node": ">= 0.11.x", 32 | "iojs": ">= 2.0.1" 33 | }, 34 | "devDependencies": { 35 | "babel": "^5.6.4", 36 | "babel-runtime": "^5.6.4", 37 | "chai": "^1.10.0", 38 | "co": "^4.0.0", 39 | "ejs": "^1.0.0", 40 | "mocha": "^2.0.1", 41 | "mocha-generators": "^1.0.0" 42 | }, 43 | "dependencies": { 44 | "class-extend": "^0.1.1", 45 | "co-views": "^0.2.0", 46 | "nodemailer": "^1.3.0", 47 | "thunkify": "^2.1.2" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/mailman.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | 5 | var Mailman = require('../'); 6 | 7 | require('chai').should(); 8 | require('mocha-generators').install(); 9 | 10 | /** 11 | * Tests 12 | */ 13 | 14 | describe ('Mailman', function () { 15 | var user = 'test_user'; 16 | var password = 'test_password'; 17 | var sender = 'sender@test.com'; 18 | var receiver = 'receiver@test.com'; 19 | 20 | before (function () { 21 | Mailman.options.views.path = __dirname + '/fixtures/views'; 22 | }); 23 | 24 | it ('should configure known transport', function () { 25 | // configuring gmail as an example 26 | Mailman.configure('gmail', { user, password }); 27 | 28 | var transport = Mailman.transport.transporter.options; 29 | transport.service.should.equal('gmail'); 30 | transport.auth.user.should.equal(user); 31 | transport.auth.pass.should.equal(password); 32 | transport.host.should.equal('smtp.gmail.com'); 33 | transport.port.should.equal(465); 34 | transport.secure.should.equal(true); 35 | }); 36 | 37 | it ('should configure smtp transport', function () { 38 | Mailman.configure({ 39 | host: 'smtp.gmail.com', 40 | port: 587, 41 | auth: { user, pass: password } 42 | }); 43 | 44 | var transport = Mailman.transport.transporter.options; 45 | transport.auth.user.should.equal(user); 46 | transport.auth.pass.should.equal(password); 47 | transport.host.should.equal('smtp.gmail.com'); 48 | transport.port.should.equal(587); 49 | }); 50 | 51 | it ('should define a mailer and mail', function () { 52 | class UserMailer extends Mailman.Mailer { 53 | get name () { return 'user'; } 54 | get from () { return sender; } 55 | 56 | welcome () { 57 | this.full_name = 'John Doe'; 58 | } 59 | } 60 | 61 | var mail = new UserMailer({ to: receiver }).welcome(); 62 | mail.locals.full_name.should.equal('John Doe'); 63 | mail.options.view.should.equal('user/welcome.ejs'); 64 | mail.options.from.should.equal(sender); 65 | }); 66 | 67 | describe ('Backwards compatibility', function () { 68 | it ('should define a mailer and mail with ES5-ish API', function () { 69 | let UserMailer = Mailman.Mailer.extend({ 70 | name: 'user', 71 | from: sender, 72 | 73 | welcome: function () { 74 | this.full_name = 'John Doe'; 75 | } 76 | }); 77 | 78 | var mail = new UserMailer({ to: receiver }).welcome(); 79 | mail.locals.full_name.should.equal('John Doe'); 80 | mail.options.view.should.equal('user/welcome.ejs'); 81 | mail.options.from.should.equal(sender); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Mailman 2 | 3 | Send emails in a comfortable way via models. 4 | 5 | [![Circle CI](https://circleci.com/gh/vdemedes/mailman.svg?style=svg)](https://circleci.com/gh/vdemedes/mailman) 6 | 7 | ## Features 8 | 9 | - Uses nodemailer under the hood to send out emails 10 | - All nodemailer transports (including 3rd-party ones) are supported 11 | - Uses [consolidate.js](https://github.com/tj/consolidate.js) to render email templates (views) 12 | - ActiveMailer-inspired API, very comfortable 13 | - Clean, simple and lightweight code base (162 sloc) 14 | 15 | ## Installation 16 | 17 | ``` 18 | npm install mailman --save 19 | ``` 20 | 21 | **Warning**: Only Node.js v0.11.x and higher with --harmony enabled is required: 22 | 23 | ``` 24 | node --harmony something.js 25 | ``` 26 | 27 | **Note**: In order for the following examples to work, you need use something like [co](https://github.com/tj/co) to run generators. 28 | **Another note**: If you want to use ES6 classes (like in the following examples), use [babel](https://github.com/babel/babel). If not, there is an alternative API left from previous versions of Mailman. 29 | 30 | 31 | ## Usage 32 | 33 | ### Configuration 34 | 35 | To configure Mailman's basic options: 36 | 37 | ```javascript 38 | var Mailman = require('mailman'); 39 | 40 | // path to a folder where 41 | // your views are stored 42 | Mailman.options.views.path = 'path_to_views/'; 43 | 44 | // cache templates or not 45 | Mailman.options.views.cache = false; 46 | 47 | // default template engine 48 | // guesses by extension otherwise 49 | Mailman.options.views.default = 'ejs'; 50 | ``` 51 | 52 | To setup a transport: 53 | 54 | - Configuring one of the [known services](https://github.com/andris9/nodemailer-wellknown#supported-services): 55 | 56 | ```javascript 57 | Mailman.configure('gmail', { 58 | user: 'user@gmail.com', 59 | password: 'password' 60 | }); 61 | ``` 62 | 63 | - Configuring default SMTP transport (options passed directly to nodemailer): 64 | 65 | ```javascript 66 | Mailman.configure({ 67 | host: 'smtp.gmail.com', 68 | port: 465, 69 | secure: true, 70 | auth: { 71 | user: 'user@gmail.com', 72 | pass: 'password' 73 | } 74 | }); 75 | ``` 76 | 77 | - Configuring with 3rd-party nodemailer transport: 78 | 79 | ```javascript 80 | // assuming that transport is 81 | // initialized nodemailer transport 82 | 83 | Mailman.configure(transport); 84 | ``` 85 | 86 | ### Views 87 | 88 | Mailman uses [consolidate.js](https://github.com/tj/consolidate.js) to render many template engines easily. 89 | Mailman expects, that your folder with views is structured like this: 90 | 91 | ``` 92 | - user/ 93 | - welcome.ejs 94 | - forgot_password.ejs 95 | - reset_password.ejs 96 | ``` 97 | 98 | In this folder structure, it is clear that User mailer has welcome, forgot_password and reset\_password emails. 99 | 100 | ### Defining Mailer and sending 101 | 102 | To send out emails, you need to define mailer first. 103 | Mailer is a Class, that contains emails for certain entity. 104 | For example, User mailer may contain welcome, forgot password, reset password emails. 105 | Each of the emails is represented as a usual function, which should set template variables for a view. 106 | 107 | **Note**: Email function name **must** be the same as its view name (camelCased) 108 | 109 | ```javascript 110 | class UserMailer extends Mailman.Mailer { 111 | // need to manually set mailer name 112 | // UserMailer => user 113 | get name () { return 'user'; } 114 | 115 | // default from for all emails 116 | get from () { return 'sender@sender.com'; } 117 | 118 | // default subject for all emails 119 | get subject () { return 'Hello World'; } 120 | 121 | // welcome email 122 | welcome () { 123 | // set all your template variables 124 | // on this 125 | 126 | this.full_name = 'John Doe'; 127 | this.currentDate = new Date(); 128 | } 129 | 130 | // forgot password email 131 | forgotPassword () { 132 | this.token = 12345; 133 | } 134 | } 135 | ``` 136 | 137 | To send out each of these emails, simply: 138 | 139 | ```javascript 140 | var mail; 141 | 142 | mail = new UserMailer({ to: 'receiver@receiver.com' }).welcome(); 143 | yield mail.deliver(); 144 | 145 | mail = new UserMailer({ to: 'receiver@receiver.com' }).forgotPassword(); 146 | yield mail.deliver(); 147 | ``` 148 | 149 | 150 | ## Tests 151 | 152 | ``` 153 | npm test 154 | ``` 155 | 156 | 157 | # License 158 | 159 | Mailman is released under the MIT License. 160 | -------------------------------------------------------------------------------- /lib/mailman.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | 5 | var Class = require('class-extend'); 6 | 7 | var nodemailer = require('nodemailer'); 8 | var thunkify = require('thunkify'); 9 | var basename = require('path').basename; 10 | var views = require('co-views'); 11 | var util = require('./util'); 12 | 13 | var walk = util.walk; 14 | 15 | 16 | /** 17 | * Mailman 18 | */ 19 | 20 | class Mailman { 21 | static configure (service, auth) { 22 | let transport; 23 | 24 | // if service is a string, assume that it's 25 | // one of the nodemailer-wellknown 26 | // supported services 27 | // see https://github.com/andris9/nodemailer-wellknown#supported-services 28 | if ('string' === typeof service) { 29 | transport = nodemailer.createTransport({ 30 | service: service, 31 | auth: { 32 | user: auth.user, 33 | pass: auth.password 34 | } 35 | }); 36 | } 37 | 38 | if ('object' === typeof service) { 39 | if ('undefined' === typeof service.sendMail) { 40 | // if no sendMail property, assume 41 | // that it's just normal object 42 | 43 | transport = nodemailer.createTransport(service); 44 | } else { 45 | // else, it must be initialized 46 | // 3rd-party transport 47 | transport = service; 48 | } 49 | } 50 | 51 | // transform sendMail into co-compatible fn 52 | transport.sendMail = thunkify(transport.sendMail); 53 | 54 | // set this transport as a default 55 | return this.transport = transport; 56 | } 57 | 58 | static scanViews () { 59 | let path = this.options.views.path; 60 | 61 | if (!path) { 62 | throw new Error('Mailman.options.views.path is empty'); 63 | } 64 | 65 | let files = walk(path); 66 | let views = {}; 67 | 68 | files.forEach(file => { 69 | // /views/mailer_name/view_name.ejs => mailer_name 70 | let [mailerName] = file.replace(path + '/', '').split('/'); 71 | 72 | // /views/mailer_name/view_name.ejs => view_name 73 | let viewName = basename(file).replace(/\.[a-z]{3,4}$/g, ''); 74 | 75 | // under_score to camelCase 76 | viewName = viewName.replace(/\_[a-z0-9]/gi, function ($1) { 77 | return $1.slice(1).toUpperCase(); 78 | }); 79 | 80 | if (!views[mailerName]) views[mailerName] = {}; 81 | 82 | // register view and store it's relative path 83 | views[mailerName][viewName] = mailerName + '/' + basename(file); 84 | }); 85 | 86 | this.views = views; 87 | } 88 | 89 | static renderView (path, locals) { 90 | if (!this.render) { 91 | let options = this.options.views; 92 | 93 | this.render = views(options.path, { 94 | cache: options.cache, 95 | default: options.default, 96 | map: options.map 97 | }); 98 | } 99 | 100 | return this.render(path, locals); 101 | } 102 | 103 | static send (options) { 104 | return this.transport.sendMail(options); 105 | } 106 | } 107 | 108 | Mailman.options = { 109 | views: { 110 | path: '', 111 | cache: false, 112 | default: '', 113 | map: {} 114 | } 115 | }; 116 | 117 | 118 | /** 119 | * Mail 120 | */ 121 | 122 | 123 | class Mail { 124 | constructor (options = {}, locals = {}) { 125 | this.options = options; 126 | this.locals = locals; 127 | } 128 | 129 | * deliver () { 130 | // clone this.options 131 | let options = Object.assign({}, this.options); 132 | 133 | // render email body 134 | options.html = yield Mailman.renderView(options.view, this.locals); 135 | 136 | yield Mailman.send(options); 137 | } 138 | } 139 | 140 | 141 | /** 142 | * Mailer 143 | */ 144 | 145 | // list of supported nodemailer options 146 | var supportedOptions = [ 147 | 'from', 148 | 'to', 149 | 'cc', 150 | 'bcc', 151 | 'replyTo', 152 | 'inReplyTo', 153 | 'references', 154 | 'subject', 155 | 'headers', 156 | 'envelope', 157 | 'messageId', 158 | 'date', 159 | 'encoding' 160 | ]; 161 | 162 | // mailman properties to ignore 163 | var ignoredKeys = [ 164 | 'name', 165 | 'transport' 166 | ]; 167 | 168 | class Mailer { 169 | constructor (options = {}) { 170 | Object.assign(this, options); 171 | 172 | if (!this.transport) this.transport = Mailman.transport; 173 | 174 | if (!Mailman.views) Mailman.scanViews(); 175 | 176 | // we need to find any views defined on the Mailer 177 | for (let key in Mailman.views[this.name]) { 178 | // if this view is registered 179 | // as a method, wrap it into Mail factory 180 | if (this[key]) { 181 | this[key] = factory(this, key); 182 | } 183 | } 184 | } 185 | 186 | options () { 187 | let options = {}; 188 | 189 | // get only nodemailer supported properties 190 | supportedOptions.forEach(key => options[key] = this[key]); 191 | 192 | return options; 193 | } 194 | 195 | locals () { 196 | let locals = {}; 197 | 198 | for (let key in this) { 199 | let value = this[key]; 200 | 201 | // ignored keys, mailer name and transport 202 | let isIgnored = ignoredKeys.indexOf(key) >= 0; 203 | 204 | // nodemailer options 205 | let isOption = supportedOptions.indexOf(key) >= 0; 206 | 207 | // also, don't append functions 208 | let isFunction = 'function' === typeof value; 209 | 210 | // if none of those is true 211 | // apend new template variable 212 | if (!isIgnored && !isOption && !isFunction) { 213 | locals[key] = value; 214 | } 215 | } 216 | 217 | return locals; 218 | } 219 | } 220 | 221 | Mailer.extend = Class.extend; 222 | 223 | // method that replaces function in mailer 224 | // with a function that gets options and locals 225 | // and creates new Mail object and returns it 226 | function factory (mailer, method) { 227 | let fn = mailer[method]; 228 | 229 | return function () { 230 | fn.apply(mailer, arguments); 231 | 232 | let options = mailer.options(); 233 | let locals = mailer.locals(); 234 | 235 | // get path for a view 236 | options.view = Mailman.views[this.name][method]; 237 | 238 | return new Mail(options, locals); 239 | }; 240 | } 241 | 242 | 243 | /** 244 | * Expose `Mailman` 245 | */ 246 | 247 | var exports = module.exports = Mailman; 248 | 249 | exports.Mailer = Mailer; 250 | exports.Mail = Mail; 251 | --------------------------------------------------------------------------------