├── .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 | [](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 |
--------------------------------------------------------------------------------