mirror of
https://github.com/wassname/talk.git
synced 2026-07-25 13:30:59 +08:00
improved mailer
This commit is contained in:
+136
-52
@@ -1,63 +1,147 @@
|
||||
const mailer = require('../services/mailer');
|
||||
const { task } = require('../services/mailer');
|
||||
const nodemailer = require('nodemailer');
|
||||
const debug = require('debug')('talk:jobs:mailer');
|
||||
const Context = require('../graph/context');
|
||||
const { get } = require('lodash');
|
||||
|
||||
const {
|
||||
SMTP_HOST,
|
||||
SMTP_USERNAME,
|
||||
SMTP_PORT,
|
||||
SMTP_PASSWORD,
|
||||
SMTP_FROM_ADDRESS,
|
||||
} = require('../../config');
|
||||
|
||||
// parseSMTPPort will return the port for SMTP.
|
||||
const parseSMTPPort = () => {
|
||||
if (!SMTP_PORT) {
|
||||
return 25;
|
||||
}
|
||||
|
||||
try {
|
||||
return parseInt(SMTP_PORT);
|
||||
} catch (e) {
|
||||
throw new Error('TALK_SMTP_PORT is not an integer');
|
||||
}
|
||||
};
|
||||
|
||||
// createTransport will create a new transport.
|
||||
const createTransport = () => {
|
||||
const options = {
|
||||
host: SMTP_HOST,
|
||||
};
|
||||
|
||||
if (SMTP_USERNAME && SMTP_PASSWORD) {
|
||||
options.auth = {
|
||||
user: SMTP_USERNAME,
|
||||
pass: SMTP_PASSWORD,
|
||||
};
|
||||
}
|
||||
|
||||
// Get the SMTP port.
|
||||
options.port = parseSMTPPort();
|
||||
|
||||
return nodemailer.createTransport(options);
|
||||
};
|
||||
|
||||
// sharedTransport is the transport singleton.
|
||||
let sharedTransport;
|
||||
|
||||
// getTransport will retrieve the mailer transport singleton.
|
||||
const getTransport = () => {
|
||||
if (sharedTransport) {
|
||||
return sharedTransport;
|
||||
}
|
||||
|
||||
// enabled is true when the required configuration is available. When testing
|
||||
// is enabled, we will be simulating that emails are being sent, because in a
|
||||
// production system, emails should and would be sent.
|
||||
|
||||
// If the transport details aren't available, we will return null as the
|
||||
// transport.
|
||||
if (!SMTP_HOST || !SMTP_FROM_ADDRESS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create the transport.
|
||||
sharedTransport = createTransport();
|
||||
|
||||
return sharedTransport;
|
||||
};
|
||||
|
||||
// getEmailAddress will retrieve the email address to send the message to from
|
||||
// the job data.
|
||||
const getEmailAddress = async ({ email, user }) => {
|
||||
// If the message has a specific email already to sent it to, just assign
|
||||
// that to the message. If the email does not have an email, and instead has
|
||||
// a user id, then we should lookup the user with the graph and get their
|
||||
// email.
|
||||
if (email) {
|
||||
return email;
|
||||
} else {
|
||||
// Get the user to send the message to.
|
||||
const ctx = Context.forSystem();
|
||||
|
||||
const { data, errors } = await ctx.graphql(
|
||||
`
|
||||
query GetUserEmail($user: ID!) {
|
||||
user(id: $user) {
|
||||
email
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ user }
|
||||
);
|
||||
if (errors) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
const email = get(data, 'user.email');
|
||||
if (!email) {
|
||||
throw errors.ErrMissingEmail;
|
||||
}
|
||||
|
||||
return email;
|
||||
}
|
||||
};
|
||||
|
||||
// processJob will handle new jobs sent via this queue.
|
||||
const processJob = transport => async ({ id, data }, done) => {
|
||||
const { message } = data;
|
||||
|
||||
// Get the email address from the job data.
|
||||
message.to = await getEmailAddress(data);
|
||||
|
||||
debug(`Starting to send mail for Job[${id}]`);
|
||||
|
||||
// Actually send the email.
|
||||
transport.sendMail(message, err => {
|
||||
if (err) {
|
||||
debug(`Failed to send mail for Job[${id}]:`, err);
|
||||
return done(err);
|
||||
}
|
||||
|
||||
debug(`Finished sending mail for Job[${id}]`);
|
||||
return done();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Start the queue processor for the mailer job.
|
||||
*/
|
||||
module.exports = () => {
|
||||
debug(`Now processing ${mailer.task.name} jobs`);
|
||||
// Get a transport.
|
||||
const transport = getTransport();
|
||||
if (transport === null) {
|
||||
console.warn(
|
||||
new Error(
|
||||
"sending email is not enabled because required configuration is not available so we can't create"
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
return mailer.task.process(async ({ id, data }, done) => {
|
||||
const { email, user, message } = data;
|
||||
debug(`Now processing ${task.name} jobs`);
|
||||
|
||||
debug(`Starting to send mail for Job[${id}]`);
|
||||
|
||||
// If the message has a specific email already to sent it to, just assign
|
||||
// that to the message. If the email does not have an email, and instead has
|
||||
// a user id, then we should lookup the user with the graph and get their
|
||||
// email.
|
||||
if (email) {
|
||||
message.to = email;
|
||||
} else {
|
||||
// Get the user to send the message to.
|
||||
const ctx = Context.forSystem();
|
||||
|
||||
try {
|
||||
const { data, errors } = await ctx.graphql(
|
||||
`
|
||||
query GetUserEmail($user: ID!) {
|
||||
user(id: $user) {
|
||||
email
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ user }
|
||||
);
|
||||
if (errors) {
|
||||
return done(errors);
|
||||
}
|
||||
const email = get(data, 'user.email');
|
||||
if (!email) {
|
||||
return done(errors.ErrMissingEmail);
|
||||
}
|
||||
|
||||
message.to = email;
|
||||
} catch (err) {
|
||||
return done(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Actually send the email.
|
||||
mailer.transport.sendMail(message, err => {
|
||||
if (err) {
|
||||
debug(`Failed to send mail for Job[${id}]:`, err);
|
||||
return done(err);
|
||||
}
|
||||
|
||||
debug(`Finished sending mail for Job[${id}]`);
|
||||
return done();
|
||||
});
|
||||
});
|
||||
return task.process(processJob(transport));
|
||||
};
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
const kue = require('./kue');
|
||||
const i18n = require('./i18n');
|
||||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
const { get, set, merge, template, isObject } = require('lodash');
|
||||
const { TEMPLATE_LOCALS } = require('../middleware/staticTemplate');
|
||||
const debug = require('debug')('talk:services:mailer');
|
||||
|
||||
const {
|
||||
SMTP_HOST,
|
||||
SMTP_USERNAME,
|
||||
SMTP_PORT,
|
||||
SMTP_PASSWORD,
|
||||
SMTP_FROM_ADDRESS,
|
||||
EMAIL_SUBJECT_PREFIX,
|
||||
} = require('../config');
|
||||
|
||||
// load all the templates as strings
|
||||
const templates = {
|
||||
cache: {},
|
||||
registered: {},
|
||||
};
|
||||
|
||||
// Registers a template with the given filename and format.
|
||||
templates.register = async (filename, name, format) => {
|
||||
// Check to see if this template was already registered.
|
||||
if (get(templates.registered, [name, format], null) !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = await fs.readFile(filename, 'utf8');
|
||||
const view = template(file);
|
||||
|
||||
set(templates.registered, [name, format], view);
|
||||
};
|
||||
|
||||
// load the templates per request during development
|
||||
templates.render = async (name, format = 'txt', context) => {
|
||||
// Check to see if the template is a registered template (provided by a plugin
|
||||
// ) and prefer that first.
|
||||
let view = get(templates.registered, [name, format], null);
|
||||
if (view !== null) {
|
||||
return view(context);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// If we are in production mode, check the view cache.
|
||||
const view = get(templates.cache, [name, format], null);
|
||||
if (view !== null) {
|
||||
return view(context);
|
||||
}
|
||||
}
|
||||
|
||||
// Template was not registered and was not cached. Let's try and find it!
|
||||
const filename = path.join(
|
||||
__dirname,
|
||||
'email',
|
||||
[name, format, 'ejs'].join('.')
|
||||
);
|
||||
const file = await fs.readFile(filename, 'utf8');
|
||||
view = template(file);
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// If we are in production mode, fill the view cache.
|
||||
set(templates.cache, [name, format], view);
|
||||
}
|
||||
|
||||
return view(context);
|
||||
};
|
||||
|
||||
const mailer = { templates, helpers: {} };
|
||||
|
||||
// enabled is true when the required configuration is available. When testing
|
||||
// is enabled, we will be simulating that emails are being sent, because in a
|
||||
// production system, emails should and would be sent.
|
||||
mailer.enabled =
|
||||
Boolean(SMTP_HOST && SMTP_FROM_ADDRESS) || process.env.NODE_ENV === 'test';
|
||||
|
||||
if (mailer.enabled) {
|
||||
const options = {
|
||||
host: SMTP_HOST,
|
||||
};
|
||||
|
||||
if (SMTP_USERNAME && SMTP_PASSWORD) {
|
||||
options.auth = {
|
||||
user: SMTP_USERNAME,
|
||||
pass: SMTP_PASSWORD,
|
||||
};
|
||||
}
|
||||
|
||||
if (SMTP_PORT) {
|
||||
try {
|
||||
options.port = parseInt(SMTP_PORT);
|
||||
} catch (e) {
|
||||
throw new Error('TALK_SMTP_PORT is not an integer');
|
||||
}
|
||||
} else {
|
||||
options.port = 25;
|
||||
}
|
||||
|
||||
mailer.transport = nodemailer.createTransport(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the new Task kue.
|
||||
*/
|
||||
mailer.task = new kue.Task({
|
||||
name: 'mailer',
|
||||
});
|
||||
|
||||
/**
|
||||
* registerHelpers will register the helpers on the mailer.
|
||||
*
|
||||
* @param {Object} helpers the helpers in object form that should be used by the
|
||||
* mailer.
|
||||
*/
|
||||
mailer.registerHelpers = helpers => {
|
||||
mailer.helpers = merge(mailer.helpers, helpers);
|
||||
};
|
||||
|
||||
// createMessage creates a message payload to send a email to a user.
|
||||
const createMessage = async options => {
|
||||
// Create the new locals object and attach the static locals and the i18n
|
||||
// framework.
|
||||
const locals = merge({}, mailer.helpers, options.locals, TEMPLATE_LOCALS, {
|
||||
t: i18n.t,
|
||||
});
|
||||
|
||||
// Render the templates.
|
||||
const [html, text] = await Promise.all(
|
||||
['html', 'txt'].map(fmt => {
|
||||
return mailer.templates.render(template, fmt, locals);
|
||||
})
|
||||
);
|
||||
|
||||
const subject = EMAIL_SUBJECT_PREFIX
|
||||
? `${EMAIL_SUBJECT_PREFIX} ${options.subject}`
|
||||
: options.subject;
|
||||
|
||||
return {
|
||||
html,
|
||||
text,
|
||||
subject,
|
||||
from: SMTP_FROM_ADDRESS,
|
||||
};
|
||||
};
|
||||
|
||||
mailer.queue = async (message, recipient) => {
|
||||
debug('Creating Job');
|
||||
|
||||
// Create the job to send the email later.
|
||||
const job = await mailer.task.create(
|
||||
merge(
|
||||
{
|
||||
title: 'Mail',
|
||||
message,
|
||||
},
|
||||
recipient
|
||||
)
|
||||
);
|
||||
|
||||
debug(`Created Job[${job.id}]`);
|
||||
|
||||
return job;
|
||||
};
|
||||
|
||||
// createRecipient will extract the recipient details from the options.
|
||||
const createRecipient = options => {
|
||||
// Try to get the email if it was explicitly provided.
|
||||
const email = get(options, 'email');
|
||||
if (email) {
|
||||
return { email };
|
||||
}
|
||||
|
||||
// Try to get the user if the email wasn't explicitly provided.
|
||||
const user = isObject(options.user) ? get(options.user, 'id') : options.user;
|
||||
if (user) {
|
||||
return { user };
|
||||
}
|
||||
|
||||
// If we don't have a user or a email, we can't send an email.
|
||||
throw new Error('user/email not provided');
|
||||
};
|
||||
|
||||
/**
|
||||
* send will prepare the message and queue the message to be sent.
|
||||
*/
|
||||
mailer.send = async options => {
|
||||
if (!mailer.enabled) {
|
||||
const err = new Error(
|
||||
'sending email is not enabled because required configuration is not available'
|
||||
);
|
||||
console.warn(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the recipient to sent the message to.
|
||||
const recipient = createRecipient(options);
|
||||
|
||||
// Create the message to send.
|
||||
const message = await createMessage(options);
|
||||
|
||||
// Create the job to send the message.
|
||||
return mailer.queue(message, recipient);
|
||||
};
|
||||
|
||||
module.exports = mailer;
|
||||
@@ -0,0 +1,112 @@
|
||||
const kue = require('../kue');
|
||||
const i18n = require('../i18n');
|
||||
const { get, merge, isObject } = require('lodash');
|
||||
const { TEMPLATE_LOCALS } = require('../../middleware/staticTemplate');
|
||||
const debug = require('debug')('talk:services:mailer');
|
||||
const { SMTP_FROM_ADDRESS, EMAIL_SUBJECT_PREFIX } = require('../../config');
|
||||
const templates = require('./templates');
|
||||
|
||||
// createRecipient will extract the recipient details from the options.
|
||||
const createRecipient = options => {
|
||||
// Try to get the email if it was explicitly provided.
|
||||
const email = get(options, 'email');
|
||||
if (email) {
|
||||
return { email };
|
||||
}
|
||||
|
||||
// Try to get the user if the email wasn't explicitly provided.
|
||||
const user = isObject(options.user) ? get(options.user, 'id') : options.user;
|
||||
if (user) {
|
||||
return { user };
|
||||
}
|
||||
|
||||
// If we don't have a user or a email, we can't send an email.
|
||||
throw new Error('user/email not provided');
|
||||
};
|
||||
|
||||
// createMessage creates a message payload to send a email to a user.
|
||||
const createMessage = async options => {
|
||||
// Create the new locals object and attach the static locals and the i18n
|
||||
// framework.
|
||||
const locals = merge({}, mailer.helpers, options.locals, TEMPLATE_LOCALS, {
|
||||
t: i18n.t,
|
||||
});
|
||||
|
||||
// Render the templates.
|
||||
const [html, text] = await Promise.all(
|
||||
['html', 'txt'].map(fmt => {
|
||||
return mailer.templates.render(options.template, fmt, locals);
|
||||
})
|
||||
);
|
||||
|
||||
const subject = EMAIL_SUBJECT_PREFIX
|
||||
? `${EMAIL_SUBJECT_PREFIX} ${options.subject}`
|
||||
: options.subject;
|
||||
|
||||
return {
|
||||
html,
|
||||
text,
|
||||
subject,
|
||||
from: SMTP_FROM_ADDRESS,
|
||||
};
|
||||
};
|
||||
|
||||
const mailer = { templates, helpers: {} };
|
||||
|
||||
/**
|
||||
* Create the new Task kue.
|
||||
*/
|
||||
mailer.task = new kue.Task({
|
||||
name: 'mailer',
|
||||
});
|
||||
|
||||
/**
|
||||
* registerHelpers will register the helpers on the mailer.
|
||||
*
|
||||
* @param {Object} helpers the helpers in object form that should be used by the
|
||||
* mailer.
|
||||
*/
|
||||
mailer.registerHelpers = helpers => {
|
||||
mailer.helpers = merge(mailer.helpers, helpers);
|
||||
};
|
||||
|
||||
/**
|
||||
* queue will add the message to the sending queue.
|
||||
*
|
||||
* @param {Object} message the message to be sent
|
||||
* @param {Object} recipient the recipient to send it to
|
||||
*/
|
||||
mailer.queue = async (message, recipient) => {
|
||||
debug('Creating Job');
|
||||
|
||||
// Create the job to send the email later.
|
||||
const job = await mailer.task.create(
|
||||
merge(
|
||||
{
|
||||
title: 'Mail',
|
||||
message,
|
||||
},
|
||||
recipient
|
||||
)
|
||||
);
|
||||
|
||||
debug(`Created Job[${job.id}]`);
|
||||
|
||||
return job;
|
||||
};
|
||||
|
||||
/**
|
||||
* send will prepare the message and queue the message to be sent.
|
||||
*/
|
||||
mailer.send = async options => {
|
||||
// Create the recipient to sent the message to.
|
||||
const recipient = createRecipient(options);
|
||||
|
||||
// Create the message to send.
|
||||
const message = await createMessage(options);
|
||||
|
||||
// Create the job to send the message.
|
||||
return mailer.queue(message, recipient);
|
||||
};
|
||||
|
||||
module.exports = mailer;
|
||||
@@ -0,0 +1,58 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
const { get, set, template } = require('lodash');
|
||||
|
||||
// load all the templates as strings
|
||||
const templates = {
|
||||
cache: {},
|
||||
registered: {},
|
||||
};
|
||||
|
||||
// Registers a template with the given filename and format.
|
||||
templates.register = async (filename, name, format) => {
|
||||
// Check to see if this template was already registered.
|
||||
if (get(templates.registered, [name, format], null) !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = await fs.readFile(filename, 'utf8');
|
||||
const view = template(file);
|
||||
|
||||
set(templates.registered, [name, format], view);
|
||||
};
|
||||
|
||||
// load the templates per request during development
|
||||
templates.render = async (name, format = 'txt', context) => {
|
||||
// Check to see if the template is a registered template (provided by a plugin
|
||||
// ) and prefer that first.
|
||||
let view = get(templates.registered, [name, format], null);
|
||||
if (view !== null) {
|
||||
return view(context);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// If we are in production mode, check the view cache.
|
||||
const view = get(templates.cache, [name, format], null);
|
||||
if (view !== null) {
|
||||
return view(context);
|
||||
}
|
||||
}
|
||||
|
||||
// Template was not registered and was not cached. Let's try and find it!
|
||||
const filename = path.join(
|
||||
__dirname,
|
||||
'templates',
|
||||
[name, format, 'ejs'].join('.')
|
||||
);
|
||||
const file = await fs.readFile(filename, 'utf8');
|
||||
view = template(file);
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// If we are in production mode, fill the view cache.
|
||||
set(templates.cache, [name, format], view);
|
||||
}
|
||||
|
||||
return view(context);
|
||||
};
|
||||
|
||||
module.exports = templates;
|
||||
Reference in New Issue
Block a user