add support for transactional html/text emails
- Add support for welcome email on user creation (both OAuth and password users) - Add support for html/text templating on verify email. - Add support for html/text templating on reset password. - Add handlers for converting Handlebars templates to HTML and text (template agnostic). - Add helper function sendEmail() for compiling and sending templates via Email.send() (SMTP).
This commit is contained in:
parent
aa14a26bba
commit
379f7dcd76
17 changed files with 2234 additions and 804 deletions
|
|
@ -5,7 +5,7 @@ import editProfile from './edit-profile';
|
|||
import rateLimit from '../../../modules/rate-limit';
|
||||
|
||||
Meteor.methods({
|
||||
'users.sendVerificationEmail': function usersResendVerification() {
|
||||
'users.sendVerificationEmail': function usersSendVerificationEmail() {
|
||||
return Accounts.sendVerificationEmail(this.userId);
|
||||
},
|
||||
'users.editProfile': function usersEditProfile(profile) {
|
||||
|
|
|
|||
25
imports/api/Users/server/send-welcome-email.js
Normal file
25
imports/api/Users/server/send-welcome-email.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import sendEmail from '../../../modules/server/send-email';
|
||||
import getOAuthProfile from '../../../modules/get-oauth-profile';
|
||||
|
||||
export default (options, user) => {
|
||||
const OAuthProfile = getOAuthProfile(options, user);
|
||||
|
||||
const applicationName = 'Application Name';
|
||||
const firstName = OAuthProfile ? OAuthProfile.name.first : options.profile.name.first;
|
||||
const emailAddress = OAuthProfile ? OAuthProfile.email : options.email;
|
||||
|
||||
return sendEmail({
|
||||
to: emailAddress,
|
||||
from: `${applicationName} <support@application.com>`,
|
||||
subject: `[${applicationName}] Welcome, ${firstName}!`,
|
||||
template: 'welcome',
|
||||
templateVars: {
|
||||
applicationName,
|
||||
firstName,
|
||||
welcomeUrl: Meteor.absoluteUrl('documents'), // e.g., returns http://localhost:3000/documents
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
throw new Meteor.Error('500', `${error}`);
|
||||
});
|
||||
};
|
||||
42
imports/modules/get-oauth-profile.js
Normal file
42
imports/modules/get-oauth-profile.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
const parseGoogleData = service => {
|
||||
return {
|
||||
email: service.email,
|
||||
name: {
|
||||
first: service.given_name,
|
||||
last: service.family_name,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const parseGithubData = (profile, service) => {
|
||||
const name = profile.name.split(' ');
|
||||
return {
|
||||
email: service.email,
|
||||
name: {
|
||||
first: name[0],
|
||||
last: name[1],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const parseFacebookData = service => {
|
||||
return {
|
||||
email: service.email,
|
||||
name: {
|
||||
first: service.first_name,
|
||||
last: service.last_name,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const getDataForService = (profile, services) => {
|
||||
if (services.facebook) return parseFacebookData(services.facebook);
|
||||
if (services.github) return parseGithubData(profile, services.github);
|
||||
if (services.google) return parseGoogleData(services.google);
|
||||
};
|
||||
|
||||
export default (options, user) => {
|
||||
const isOAuth = !options.password;
|
||||
const serviceData = isOAuth ? getDataForService(options.profile, user.services) : null;
|
||||
return isOAuth ? serviceData : null;
|
||||
};
|
||||
11
imports/modules/server/handlebars-email-to-html.js
Normal file
11
imports/modules/server/handlebars-email-to-html.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import handlebars from 'handlebars';
|
||||
import juice from 'juice';
|
||||
|
||||
export default (handlebarsMarkup, context, options) => {
|
||||
if (handlebarsMarkup && context) {
|
||||
const template = handlebars.compile(handlebarsMarkup);
|
||||
return options && !options.inlineCss ? template(context) : juice(template(context)); // Use juice to inline CSS <style></style> styles from <head> unless disabled.
|
||||
}
|
||||
|
||||
throw new Error('Please pass Handlebars markup to compile and a context object with data mapping to the Handlebars expressions used in your template.');
|
||||
};
|
||||
10
imports/modules/server/handlebars-email-to-text.js
Normal file
10
imports/modules/server/handlebars-email-to-text.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import handlebars from 'handlebars';
|
||||
|
||||
export default (handlebarsMarkup, context) => {
|
||||
if (handlebarsMarkup && context) {
|
||||
const template = handlebars.compile(handlebarsMarkup);
|
||||
return template(context);
|
||||
}
|
||||
|
||||
throw new Error('Please pass Handlebars markup to compile and a context object with data mapping to the Handlebars expressions used in your template.');
|
||||
};
|
||||
26
imports/modules/server/send-email.js
Normal file
26
imports/modules/server/send-email.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { Email } from 'meteor/email';
|
||||
import getPrivateFile from './get-private-file';
|
||||
import templateToText from './handlebars-email-to-text';
|
||||
import templateToHTML from './handlebars-email-to-html';
|
||||
|
||||
const sendEmail = (options, { resolve, reject }) => {
|
||||
try {
|
||||
Meteor.defer(() => Email.send(options));
|
||||
if (callback) resolve();
|
||||
} catch (exception) {
|
||||
reject(exception);
|
||||
}
|
||||
};
|
||||
|
||||
export default ({ text, html, template, templateVars, ...rest }, callback) => {
|
||||
if (text || html || template) {
|
||||
return new Promise((resolve, reject) => {
|
||||
sendEmail({
|
||||
...rest,
|
||||
text: template ? templateToText(getPrivateFile(`email-templates/${template}.txt`), (templateVars || {})) : text,
|
||||
html: template ? templateToHTML(getPrivateFile(`email-templates/${template}.html`), (templateVars || {})) : html,
|
||||
}, { resolve, reject });
|
||||
});
|
||||
}
|
||||
throw new Error('Please pass an HTML string, text, or template name to compile for your message\'s body.');
|
||||
};
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
import { Meteor } from 'meteor/meteor';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import getPrivateFile from '../../../modules/server/get-private-file';
|
||||
import templateToHTML from '../../../modules/server/handlebars-email-to-html';
|
||||
import templateToText from '../../../modules/server/handlebars-email-to-text';
|
||||
|
||||
const name = 'Application Name';
|
||||
const email = '<support@application.com>';
|
||||
|
|
@ -13,11 +16,21 @@ emailTemplates.verifyEmail = {
|
|||
subject() {
|
||||
return `[${name}] Verify Your Email Address`;
|
||||
},
|
||||
html(user, url) {
|
||||
return templateToHTML(getPrivateFile('email-templates/verify-email.html'), {
|
||||
applicationName: name,
|
||||
firstName: user.profile.name.first,
|
||||
verifyUrl: url.replace('#/', '')
|
||||
});
|
||||
},
|
||||
text(user, url) {
|
||||
const userEmail = user.emails[0].address;
|
||||
const urlWithoutHash = url.replace('#/', '');
|
||||
if (Meteor.isDevelopment) console.info(`Verify Email Link: ${urlWithoutHash}`); // eslint-disable-line
|
||||
return `Hey, ${user.profile.name.first}! Welcome to ${name}.\n\nTo verify your email address (${userEmail}), click the link below:\n\n${urlWithoutHash}\n\nIf you feel something is wrong, please contact our support team: ${email}.`;
|
||||
return templateToText(getPrivateFile('email-templates/verify-email.txt'), {
|
||||
applicationName: name,
|
||||
firstName: user.profile.name.first,
|
||||
verifyUrl: urlWithoutHash,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -25,10 +38,22 @@ emailTemplates.resetPassword = {
|
|||
subject() {
|
||||
return `[${name}] Reset Your Password`;
|
||||
},
|
||||
html(user, url) {
|
||||
return templateToHTML(getPrivateFile('email-templates/reset-password.html'), {
|
||||
firstName: user.profile.name.first,
|
||||
applicationName: name,
|
||||
emailAddress: user.emails[0].address,
|
||||
resetUrl: url.replace('#/', ''),
|
||||
});
|
||||
},
|
||||
text(user, url) {
|
||||
const userEmail = user.emails[0].address;
|
||||
const urlWithoutHash = url.replace('#/', '');
|
||||
if (Meteor.isDevelopment) console.info(`Reset Password Link: ${urlWithoutHash}`); // eslint-disable-line
|
||||
return `A password reset has been requested for the account related to this address (${userEmail}).\n\nTo reset the password, visit the following link: \n\n${urlWithoutHash}\n\n If you did not request this reset, please ignore this email. If you feel something is wrong, please contact our support team: ${email}.`;
|
||||
return templateToText(getPrivateFile('email-templates/reset-password.txt'), {
|
||||
firstName: user.profile.name.first,
|
||||
applicationName: name,
|
||||
emailAddress: user.emails[0].address,
|
||||
resetUrl: urlWithoutHash,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { Accounts } from 'meteor/accounts-base';
|
||||
import sendWelcomeEmail from '../../../api/Users/server/send-welcome-email';
|
||||
|
||||
Accounts.onCreateUser((options, user) => {
|
||||
const userToCreate = user;
|
||||
if (options.profile) userToCreate.profile = options.profile;
|
||||
sendWelcomeEmail(options, user);
|
||||
return userToCreate;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert } from 'react-bootstrap';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { Bert } from 'meteor/themeteorchef:bert';
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ class VerifyEmail extends React.Component {
|
|||
setTimeout(() => {
|
||||
Bert.alert('All set, thanks!', 'success');
|
||||
history.push('/documents');
|
||||
}, 2000);
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue