forked from lookback/meteor-emails
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmailer.js
303 lines (256 loc) · 8.46 KB
/
mailer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
//`lookback:emails` is a small package for Meteor which helps you
// tremendously in the process of building, testing and debugging
// HTML emails in Meteor applications.
//
// See the [GitHub repo](https://github.com/lookback/meteor-emails) for README.
// Made by Johan Brook for [Lookback](https://github.com/lookback).
const TAG = 'mailer';
// ## Setup
// Main exported symbol with some initial settings:
//
// - `routePrefix` is the top level path for the preview and send routes (see further down).
// - `baseUrl` is what root domain to base relative paths from.
// - `testEmail`, when testing emails, set this variable.
// - `logger`, optionally inject an external logger. Defaults to `console`.
// - `disabled`, optionally disable the actual email sending. Useful for E2E testing. Defaults to `false`.
// - `addRoutes`, should we add preview and send routes? Defaults to `true` in development.
Mailer = {
settings: {
silent: false,
routePrefix: 'emails',
baseUrl: process.env.ROOT_URL,
testEmail: null,
logger: console,
disabled: false,
addRoutes: process.env.NODE_ENV === 'development',
language: 'html',
plainText: true,
plainTextOpts: {},
juiceOpts: {
preserveMediaQueries: true,
removeStyleTags: true,
webResources: {
images: false
}
}
},
middlewares: [],
use(middleware) {
if(!_.isFunction(middleware)) {
console.error('Middleware must be a function!');
} else {
this.middlewares.push(middleware);
}
return this;
},
config(newSettings) {
this.settings = _.extend(this.settings, newSettings);
return this;
}
};
// # The factory
//
// This is the "blueprint" of the Mailer object. It has the following interface:
//
// - `precompile`
// - `render`
// - `send`
//
// As you can see, the mailer takes care of precompiling and rendering templates
// with data, as well as sending emails from those templates.
const factory = (options) => {
check(options, Match.ObjectIncluding({
// Mailer *must* take a `templates` object with template names as keys.
templates: Object,
// Take optional template helpers.
helpers: Match.Optional(Object),
// Take an optional layout template object.
layout: Match.Optional(Match.OneOf(Object, Boolean))
}));
const settings = _.extend({}, Mailer.settings, options.settings);
const blazeHelpers = typeof Blaze !== 'undefined' ? Blaze._globalHelpers : {};
const globalHelpers = _.extend({}, TemplateHelpers, blazeHelpers, options.helpers);
Utils.setupLogger(settings.logger, {
suppressInfo: settings.silent
});
// Use the built-in helpers, any global Blaze helpers, and injected helpers
// from options, and *additional* template helpers, and apply them to
// the template.
const addHelpers = (template) => {
check(template.name, String);
check(template.helpers, Match.Optional(Object));
return Template[template.name].helpers(_.extend({}, globalHelpers, template.helpers));
};
// ## Compile
//
// Function for compiling a template with a name and path to
// a HTML file to a template function, to be placed
// in the Template namespace.
//
// A `template` must have a path to a template HTML file, and
// can optionally have paths to any SCSS and CSS stylesheets.
const compile = (template) => {
check(template, Match.ObjectIncluding({
path: String,
name: String,
packageFolderName: Match.Optional(String),
scss: Match.Optional(String),
css: Match.Optional(String),
layout: Match.Optional(Match.OneOf(Boolean, {
name: String,
path: String,
scss: Match.Optional(String),
css: Match.Optional(String)
}))
}));
let content = null;
try {
content = Utils.readFile(template.path, template.packageFolderName);
} catch (ex) {
Utils.Logger.error(`Could not read template file: ${template.path}`, TAG);
return false;
}
const layout = template.layout || options.layout;
if (layout && template.layout !== false) {
const layoutContent = Utils.readFile(layout.path, template.packageFolderName);
SSR.compileTemplate(layout.name, layoutContent, {
language: settings.language
});
addHelpers(layout);
}
// This will place the template function in
//
// Template.<template.name>
const tmpl = SSR.compileTemplate(template.name, content, {
language: settings.language
});
// Add helpers to template.
addHelpers(template);
return tmpl;
};
// ## Render
//
// Render a template by name, with optional data context.
// Will compile the template if not done already.
const render = (templateName, data) => {
check(templateName, String);
check(data, Match.Optional(Object));
const template = _.findWhere(options.templates, {
name: templateName
});
if (!(templateName in Template)) {
compile(template);
}
const tmpl = Template[templateName];
if (!tmpl) {
throw new Meteor.Error(500, `Could not find template: ${templateName}`);
}
let rendered = SSR.render(tmpl, data);
const layout = template.layout || options.layout;
if (layout && template.layout !== false) {
let preview = null;
let css = null;
// When applying to a layout, some info from the template
// (like the first preview lines) needs to be applied to the
// layout scope as well.
//
// Thus we fetch a `preview` helper from the template or
// `preview` prop in the data context to apply to the layout.
if (tmpl.__helpers.has('preview')) {
preview = tmpl.__helpers.get('preview');
} else if (data.preview) {
preview = data.preview;
}
// The `extraCSS` property on a `template` is applied to
// the layout in `<style>` tags. Ideal for media queries.
if (template.extraCSS) {
try {
css = Utils.readFile(template.extraCSS, template.packageFolderName);
} catch (ex) {
Utils.Logger.error(`Could not add extra CSS when rendering ${templateName}: ${ex.message}`, TAG);
}
}
const layoutData = _.extend({}, data, {
body: rendered,
css,
preview
});
rendered = SSR.render(layout.name, layoutData);
rendered = Utils.addStylesheets(template, rendered, settings.juiceOpts);
rendered = Utils.addStylesheets(layout, rendered, settings.juiceOpts);
} else {
rendered = Utils.addStylesheets(template, rendered, settings.juiceOpts);
}
rendered = Utils.addDoctype(rendered);
return rendered;
};
// ## Send
//
// The main sending-email function. Takes a set of usual email options,
// including the template name and optional data object.
const sendEmail = (options) => {
check(options, {
to: String,
subject: String,
template: String,
replyTo: Match.Optional(String),
from: Match.Optional(String),
data: Match.Optional(Object),
headers: Match.Optional(Object)
});
const defaults = {
from: settings.from
};
if (settings.replyTo) {
defaults.replyTo = settings.replyTo;
}
const opts = _.extend({}, defaults, options);
// Render HTML with optional data context and optionally
// create plain-text version from HTML.
try {
opts.html = render(options.template, options.data);
if (settings.plainText) {
opts.text = Utils.toText(opts.html, settings.plainTextOpts);
}
} catch (ex) {
Utils.Logger.error(`Could not render email before sending: ${ex.message}`, TAG);
return false;
}
try {
if (!settings.disabled) {
Email.send(opts);
}
return true;
} catch (ex) {
Utils.Logger.error(`Could not send email: ${ex.message}`, TAG);
return false;
}
};
const initFactory = () => {
if (options.templates) {
_.each(options.templates, (template, name) => {
template.name = name;
compile(template);
Mailer.middlewares.forEach(func => {
func(template, settings, render, compile);
});
});
}
};
return {
precompile: compile,
render: render,
send: sendEmail,
initFactory
};
};
// Init routine. We create a new "instance" from the factory.
// Any middleware needs to be called upon before we run the
// inner `init()` function.
Mailer.init = function(opts) {
const obj = _.extend(this, factory(opts));
if(obj.settings.addRoutes) {
obj.use(Routing);
}
obj.initFactory();
};