Skip to content

Commit

Permalink
Implementing HTML emails
Browse files Browse the repository at this point in the history
closes TryGhost#3082
- no more in-line HTML strings
- adding files for "welcome", "reset password", and "invite user" emails
- added mail.generateContent() to create HTML and plain-text email content
- refactored methods that trigger emails to send both HTML and plain-text emails
  • Loading branch information
morficus committed Jul 29, 2014
1 parent 9343bcc commit e30e29b
Show file tree
Hide file tree
Showing 12 changed files with 860 additions and 69 deletions.
69 changes: 35 additions & 34 deletions core/server/api/authentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,24 +46,26 @@ authentication = {
return dataProvider.User.generateResetToken(email, expires, dbHash);
}).then(function (resetToken) {
var baseUrl = config.forceAdminSSL ? (config.urlSSL || config.url) : config.url,
siteLink = '<a href="' + baseUrl + '">' + baseUrl + '</a>',
resetUrl = baseUrl.replace(/\/$/, '') + '/ghost/reset/' + resetToken + '/',
resetLink = '<a href="' + resetUrl + '">' + resetUrl + '</a>',
payload = {
emailData = {
resetUrl: resetUrl
};

mail.generateContent({data: emailData, template: 'reset-password'}).then(function (emailContent) {
var payload = {
mail: [{
message: {
to: email,
subject: 'Reset Password',
html: '<p><strong>Hello!</strong></p>' +
'<p>A request has been made to reset the password on the site ' + siteLink + '.</p>' +
'<p>Please follow the link below to reset your password:<br><br>' + resetLink + '</p>' +
'<p>Ghost</p>'
html: emailContent.html,
text: emailContent.text
},
options: {}
}]
};
return mail.send(payload, {context: {internal: true}});
});

return mail.send(payload, {context: {internal: true}});
}).then(function () {
return when.resolve({passwordreset: [{message: 'Check your email for further instructions.'}]});
}).otherwise(function (error) {
Expand Down Expand Up @@ -212,33 +214,32 @@ authentication = {
setupUser = user.toJSON();
return settings.edit({settings: userSettings}, {context: {user: setupUser.id}});
}).then(function () {
var message = {
to: setupUser.email,
subject: 'Your New Ghost Blog',
html: '<p><strong>Hello!</strong></p>' +
'<p>Good news! You\'ve successfully created a brand new Ghost blog over on ' + config.url + '</p>' +
'<p>You can log in to your admin account with the following details:</p>' +
'<p> Email Address: ' + setupUser.email + '<br>' +
'Password: The password you chose when you signed up</p>' +
'<p>Keep this email somewhere safe for future reference, and have fun!</p>' +
'<p>xoxo</p>' +
'<p>Team Ghost<br>' +
'<a href="https://ghost.org">https://ghost.org</a></p>'
},
payload = {
mail: [{
message: message,
options: {}
}]
};

return mail.send(payload, {context: {internal: true}}).otherwise(function (error) {
errors.logError(
error.message,
"Unable to send welcome email, your blog will continue to function.",
"Please see http://docs.ghost.org/mail/ for instructions on configuring email."
);
var data = {
ownerEmail: setupUser.email
};

mail.generateContent({data: data, template: 'welcome'}).then(function (emailContent) {
var message = {
to: setupUser.email,
subject: 'Your New Ghost Blog',
html: emailContent.html,
text: emailContent.text
},
payload = {
mail: [{
message: message,
options: {}
}]
};
return mail.send(payload, {context: {internal: true}}).otherwise(function (error) {
errors.logError(
error.message,
"Unable to send welcome email, your blog will continue to function.",
"Please see http://docs.ghost.org/mail/ for instructions on configuring email."
);
});
});

}).then(function () {
return when.resolve({ users: [setupUser]});
});
Expand Down
65 changes: 53 additions & 12 deletions core/server/api/mail.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
// # Mail API
// API for sending Mail
var when = require('when'),
_ = require('lodash'),
config = require('../config'),
canThis = require('../permissions').canThis,
errors = require('../errors'),
path = require('path'),
fs = require('fs'),
templatesDir = path.resolve(__dirname, '..', 'email-templates'),
htmlToText = require('html-to-text'),
mail;

/**
Expand Down Expand Up @@ -54,24 +59,60 @@ mail = {
* @returns {Promise}
*/
sendTest: function (object, options) {
var html = '<p><strong>Hello there!</strong></p>' +
'<p>Excellent!' +
' You\'ve successfully setup your email config for your Ghost blog over on ' + config.url + '</p>' +
'<p>If you hadn\'t, you wouldn\'t be reading this email, but you are, so it looks like all is well :)</p>' +
'<p>xoxo</p>' +
'<p>Team Ghost<br>' +
'<a href="https://ghost.org">https://ghost.org</a></p>',

payload = {mail: [{

return mail.generateContent({template: 'test'}).then(function (emailContent) {
var payload = {mail: [{
message: {
to: object.to,
subject: 'Test Ghost Email',
html: html
html: emailContent.html,
text: emailContent.text
}
}]};

return mail.send(payload, options);
return mail.send(payload, options);
});
},

/**
*
* @param {
* data: JSON object representing the data that will go into the email
* template: which email template to load (files are stored in /core/server/email-templates/)
* }
* @returns {*}
*/
generateContent: function (options) {

var defaultData = {
siteUrl: config.forceAdminSSL ? (config.urlSSL || config.url) : config.url
},
emailData = _.defaults(defaultData, options.data);

_.templateSettings.interpolate = /{{([\s\S]+?)}}/g;

//read the proper email body template
return when.promise(function (resolve, reject) {
fs.readFile(templatesDir + '/' + options.template + '.html', {encoding: 'utf8'}, function (err, fileContent) {
if (err) {
reject(err);
}

//insert user-specific data into the email
var htmlContent = _.template(fileContent, emailData),
textContent;

//generate a plain-text version of the same email
textContent = htmlToText.fromString(htmlContent);

resolve({ html: htmlContent,
text: textContent
});

});
});

}
};

module.exports = mail;
module.exports = mail;
56 changes: 34 additions & 22 deletions core/server/api/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,28 +182,40 @@ users = {
dbHash = response.settings[0].value;
return dataProvider.User.generateResetToken(user.email, expires, dbHash);
}).then(function (resetToken) {
var baseUrl = config.forceAdminSSL ? (config.urlSSL || config.url) : config.url,
siteLink = '<a href="' + baseUrl + '">' + baseUrl + '</a>',
resetUrl = baseUrl.replace(/\/$/, '') + '/ghost/signup/' + resetToken + '/',
resetLink = '<a href="' + resetUrl + '">' + resetUrl + '</a>',
payload = {
mail: [{
message: {
to: user.email,
subject: 'Invitation',
html: '<p><strong>Hello!</strong></p>' +
'<p>You have been invited to ' + siteLink + '.</p>' +
'<p>Please follow the link to sign up and publish your ideas:<br><br>' + resetLink + '</p>' +
'<p>Ghost</p>'
},
options: {}
}]
};
return mail.send(payload, {context: {internal: true}}).then(function () {
// If status was invited-pending and sending the invitation succeeded, set status to invited.
if (user.status === 'invited-pending') {
return dataProvider.User.edit({status: 'invited'}, {id: user.id});
}
when.join(users.read({'id': user.created_by}), settings.read({'key': 'title'})).then(function (values) {
var invitedBy = values[0].users[0],
blogTitle = values[1].settings[0].value,
baseUrl = config.forceAdminSSL ? (config.urlSSL || config.url) : config.url,
resetUrl = baseUrl.replace(/\/$/, '') + '/ghost/signup/' + resetToken + '/',
emailData = {
blogName: blogTitle,
invitedByName: invitedBy.name,
invitedByEmail: invitedBy.email,
resetLink: resetUrl
};

mail.generateContent({data: emailData, template: 'invite-user'}).then(function (emailContent) {

var payload = {
mail: [
{
message: {
to: user.email,
subject: emailData.invitedByName + ' has invited you to join ' + emailData.blogName,
html: emailContent.html,
text: emailContent.text
},
options: {}
}
]
};
return mail.send(payload, {context: {internal: true}}).then(function () {
// If status was invited-pending and sending the invitation succeeded, set status to invited.
if (user.status === 'invited-pending') {
return dataProvider.User.edit({status: 'invited'}, {id: user.id});
}
});
});
});
}).then(function () {
return when.resolve({users: [user]});
Expand Down
56 changes: 56 additions & 0 deletions core/server/email-templates/invite-user.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN " "http://www.w3.org/TR/html4/loose.dtd">

<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

</head>
<body bgcolor="#ffffff" topmargin="0" leftmargin="0" marginheight="0" marginwidth="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; background: #ffffff; color: #828282; font-family: sans-serif; font-size: 15px; line-height: 1.5; margin: 0; width: 100%;">

<table width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#ffffff">
<tr>
<td bgcolor="#ffffff" width="100%">

<table class="main-wrapper" width="600" cellpadding="0" cellspacing="0" border="0" align="center" bgcolor="#ffffff">
<tr>
<td class="cell" width="100%">

<div class="wrapper" style="-moz-border-radius: 3px; -webkit-border-radius: 3px; border: #e5e3d8 1px solid; border-radius: 3px; margin: 2%; padding: 5% 8%;">
<table class="content" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td class="content-cell" width="100%">

<!-- START OF EMAIL CONTENT -->
<h1 style="color: #212425; font-family: sans-serif; font-size: 28px; font-weight: 600; letter-spacing: -1px; line-height: 1.1; margin: 0.4em 0 0.8em 0; padding: 0;">Welcome</h1>
<p style="color: #828282; font-family: sans-serif; font-size: 15px; font-weight: normal; line-height: 1.5em; margin: 0; padding: 0 0 1.5em 0;"><strong>{{blogName}}</strong> is using Ghost to publish things on the internet! {{invitedByName}} has invited you to join. Please click on the link below to activate your account.</p>
<p style="color: #828282; font-family: sans-serif; font-size: 15px; font-weight: normal; line-height: 1.5em; margin: 0; padding: 0 0 1.5em 0;"><a href="{{resetLink}}" style="color: #5ba4e5;">{{resetLink}}</a></p>
<p style="color: #828282; font-family: sans-serif; font-size: 15px; font-weight: normal; line-height: 1.5em; margin: 0; padding: 0 0 1.5em 0;"><strong>No idea what Ghost is?</strong> It's a simple, beautiful platform for running an online blog or publication. Writers, businesses and individuals from all over the world use Ghost to publish their stories and ideas. <a href="http://ghost.org" style="color: #5ba4e5;">Find out more</a>.</p>
<p style="color: #828282; font-family: sans-serif; font-size: 15px; font-weight: normal; line-height: 1.5em; margin: 0; padding: 0 0 1.5em 0;">If you have trouble activating your {{blogName}} account, you can reach out to {{invitedByName}} on {{invitedByEmail}} for assistance.</p>
<p style="color: #828282; font-family: sans-serif; font-size: 15px; font-weight: normal; line-height: 1.5em; margin: 0; padding: 0 0 1.5em 0;">Have fun, and good luck!</p>
<!-- END OF EMAIL CONTENT -->

</td>
</tr>
</table>
</div>
<div class="container" style="padding: 0 4%;">
<table class="footer" width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td class="footer-cell" align="right" style="color: #888888; font-family: sans-serif; font-size: 11px; line-height: 1.3; padding: 0 0 20px 0;">
Sent by <a href="{{siteUrl}}" style="color: #5ba4e5;">{{siteUrl}}</a>
</td>
</tr>
</table>
</div>

</td>
</tr>
</table>

</td>
</tr>
</table>

</body>
</html>

Loading

0 comments on commit e30e29b

Please sign in to comment.