Skip to content

Use Mailgun as default implementation for reset password and email verification. #250

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
1 change: 1 addition & 0 deletions Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function Config(applicationId, mount) {
this.restAPIKey = cacheInfo.restAPIKey;
this.fileKey = cacheInfo.fileKey;
this.facebookAppIds = cacheInfo.facebookAppIds;
this.verifyEmails = cacheInfo.verifyEmails;
this.mount = mount;
}

Expand Down
43 changes: 32 additions & 11 deletions RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ var rack = require('hat').rack();

var Auth = require('./Auth');
var cache = require('./cache');
var Config = require('./Config');
var passwordCrypto = require('./password');
var facebook = require('./facebook');
var Parse = require('parse/node');
var triggers = require('./triggers');
var MailAdapterStore = require('./mail/MailAdapterStore')

// query and data are both provided in REST API format. So data
// types are encoded by plain old objects.
Expand Down Expand Up @@ -350,13 +350,16 @@ RestWrite.prototype.transformUser = function() {
email: this.data.email,
objectId: {'$ne': this.objectId()}
}, {limit: 1}).then((results) => {
if (results.length > 0) {
throw new Parse.Error(Parse.Error.EMAIL_TAKEN,
'Account already exists for this email ' +
'address');
}
return Promise.resolve();
});
if (results.length > 0) {
throw new Parse.Error(Parse.Error.EMAIL_TAKEN,
'Account already exists for this email ' +
'address');
}
this.data.emailVerified = false;
this.data.perishableToken = rack();
this.data.emailVerifyToken = rack();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you add those in the default _User schema? to prevent accidental overrides.

return Promise.resolve();
});
});
};

Expand Down Expand Up @@ -654,16 +657,34 @@ RestWrite.prototype.runDatabaseOperation = function() {
}
}

function sendEmailVerification () {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the right place to define the sendEmailVerification function?
What if you want to call this manually on a parse job?
What if you want to disable email verification?

Maybe we abstract this to something like validatePayload(className, payload) and add validation rules for the _User model that are configurable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the sendEmailVerification function is only used in that local scope and is defined to avoid code repetition. I guess it just depends on personal preference whether you define this within the function or in the global scope of the file.

That's a good call, I didn't even consider a user not wanting to add email verification, I'll update that.

var emailSender = MailAdapterStore.getMailService(this.config.applicationId);
if (!emailSender && this.config.verifyEmails) {
throw new Error("Verify emails option was set, but not email sending configuration was sent to parse");
}
var hasUserEmail = typeof this.data.email !== 'undefined' && this.className === "_User"
var canSendEmail = emailSender && this.config.verifyEmails;
if ( hasUserEmail && canSendEmail) {
var link = this.config.mount + "/verify_email?token=" + encodeURIComponent(this.data.emailVerifyToken) + "&username=" + encodeURIComponent(this.data.email);
var email = emailSender.getVerificationEmail(this.data.email, link);
emailSender.sendMail(this.data.email, email.subject, email.text, email.html);
}
}

if (this.query) {
// Run an update
return this.config.database.update(
this.className, this.query, this.data, options).then((resp) => {
this.response = resp;
this.response.updatedAt = this.updatedAt;
});
sendEmailVerification.call(this);
this.response = resp;
this.response.updatedAt = this.updatedAt;
});
} else {
// Run a create
return this.config.database.create(this.className, this.data, options)
.then(()=> {
sendEmailVerification.call(this);
})
.then(() => {
var resp = {
objectId: this.data.objectId,
Expand Down
Binary file added img/404-sprite.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/parse-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 32 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ var batch = require('./batch'),
express = require('express'),
FilesAdapter = require('./FilesAdapter'),
S3Adapter = require('./S3Adapter'),
MailAdapterStore = require('./mail/MailAdapterStore'),
MailAdapter = require('./mail/MailAdapter'),
MailgunAdapter = require('./mail/MailgunAdapter'),
middlewares = require('./middlewares'),
multer = require('multer'),
Parse = require('parse/node').Parse,
PromiseRouter = require('./PromiseRouter'),
path = require('path'),
httpRequest = require('./httpRequest');

// Mutate the Parse object to add the Cloud Code handlers
Expand All @@ -27,6 +31,16 @@ addParseCloud();
// "cloud": relative location to cloud code to require, or a function
// that is given an instance of Parse as a parameter. Use this instance of Parse
// to register your cloud code hooks and functions.
// "mailConfig": a dictionary of 3rd party mail service settings (such as API keys etc)
// currently only Mailgun is supported. So service, apiKey, domain and fromAddress
// are all required. Setup like mailgun: { service: 'mailgun', apiKey: 'MG_APIKEY',
// domain:'MG_DOMAIN', fromAddress:'Your App <yourapp@domain.com>' }. If you do not
// define mailConfig, no mail service will be setup.
// "mailAdapter": A replacement mail adapter for sending custom emails, or emails
// from a service other than MailGun. See the implementation of MailgunAdapter for
// expected prototype.
// "verifyEmails": A boolean value indicating that parse-server should send a verification
// email whenever a users email is changed.
// "appId": the application id to host
// "masterKey": the master key for requests to this app
// "facebookAppIds": an array of valid Facebook Application IDs, required
Expand All @@ -38,6 +52,7 @@ addParseCloud();
// "dotNetKey": optional key from Parse dashboard
// "restAPIKey": optional key from Parse dashboard
// "javascriptKey": optional key from Parse dashboard

function ParseServer(args) {
if (!args.appId || !args.masterKey) {
throw 'You must provide an appId and masterKey!';
Expand All @@ -52,6 +67,12 @@ function ParseServer(args) {
if (args.databaseURI) {
DatabaseAdapter.setAppDatabaseURI(args.appId, args.databaseURI);
}
if(args.mailAdapter) {
MailAdapterStore.setAdapter(args.appId, args.mailAdapter);
} else if (args.mailConfig) {
MailAdapterStore.configureDefaultAdapter(args.appId, args.mailConfig)
}

if (args.cloud) {
addParseCloud();
if (typeof args.cloud === 'function') {
Expand All @@ -72,7 +93,8 @@ function ParseServer(args) {
dotNetKey: args.dotNetKey || '',
restAPIKey: args.restAPIKey || '',
fileKey: args.fileKey || 'invalid-file-key',
facebookAppIds: args.facebookAppIds || []
facebookAppIds: args.facebookAppIds || [],
verifyEmails: args.verifyEmails || false
};

// To maintain compatibility. TODO: Remove in v2.1
Expand All @@ -92,6 +114,11 @@ function ParseServer(args) {

// File handling needs to be before default middlewares are applied
api.use('/', require('./files').router);
api.set('views', path.join(__dirname, 'views'));
api.use('/img', express.static(path.resolve(__dirname, 'img')));
api.use("/request_password_reset", require('./passwordReset').reset(args.appName, args.appId));
api.get("/password_reset_success", require('./passwordReset').success);
api.get("/verify_email", require('./verifyEmail')(args.appId));

// TODO: separate this from the regular ParseServer object
if (process.env.TESTING == 1) {
Expand All @@ -103,6 +130,7 @@ function ParseServer(args) {
api.use(middlewares.allowCrossDomain);
api.use(middlewares.allowMethodOverride);
api.use(middlewares.handleParseHeaders);
api.set('view engine', 'jade');

var router = new PromiseRouter();

Expand Down Expand Up @@ -165,5 +193,7 @@ function getClassName(parseClass) {

module.exports = {
ParseServer: ParseServer,
S3Adapter: S3Adapter
S3Adapter: S3Adapter,
MailAdapter: MailAdapter,
MailgunAdapter: MailgunAdapter
};
38 changes: 38 additions & 0 deletions mail/MailAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
var defaultResetEmail =
"Hi,\n\n" +
"You requested a password reset.\n\n" +
"" +
"Click here to reset it:\n" +
"<%LINK_GOES_HERE%>";

var defaultVerify =
"Hi,\n\n" +
"You are being asked to confirm the e-mail address <%EMAIL_GOES_HERE%>\n\n" +
"" +
"Click here to confirm it:\n" +
"<%LINK_GOES_HERE%>";


function MailAdapter() {
}

MailAdapter.prototype.sendMail = function(to, subject, text) {
throw new Error("Send mail must be overridden")
};

MailAdapter.prototype.getResetPasswordEmail = function(to, resetLink) {
return {
subject: 'Password Reset Request',
text: defaultResetEmail.replace("<%LINK_GOES_HERE%>", resetLink)
}
};

MailAdapter.prototype.getVerificationEmail = function(to, verifyLink) {
return {
subject: 'Please verify your e-mail',
text: defaultVerify.replace("<%EMAIL_GOES_HERE%>", to).replace("<%LINK_GOES_HERE%>", verifyLink)
}
};


module.exports = MailAdapter;
77 changes: 77 additions & 0 deletions mail/MailAdapterStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Mail Adapter
//
// Allows you to send email using a third party API such as Mailgun.
//
// To send messages:
// var service = MailAdapter.getMailService(appId);
// if(service !== null) service.sendMail('user@domain.com', 'Hello User!', 'Thanks for signing up!');
//
// Each adapter requires:-
// * validateConfig(config) -> returns a set of configuration values for each service. Different services have different config requirements
// * sendMail(to, subject, text) -> sends a message using the configured service

var MailgunAdapter = require('./MailgunAdapter');

var adapter = MailgunAdapter;
var mailConfigs = {};
var mailServices = {};

function configureDefaultAdapter(appId, mailApiConfig) {

// Perform a type check on mailApiConfig to ensure it's a dictionary/object
if(typeof mailApiConfig === 'object') {

// Ensure mailApiConfig has a least a service defined, if not — default to mailgun
if(typeof mailApiConfig.service === 'undefined' || mailApiConfig.service === '') {
mailApiConfig.service = 'mailgun'; // use mailgun as the default adapter
}

// Set the mail service as configured
if(mailApiConfig.service === '' || mailApiConfig.service === 'mailgun') {
adapter = MailgunAdapter;
mailApiConfig = MailgunAdapter.validateConfig(mailApiConfig);
} else {
// Handle other mail adapters here... (such as mandrill, postmark, etc
}

} else {
// Unexpected type, should be an object/dictionary.
console.log('Error: Unexpected `mailApiConfig` in MailAdapter.');
mailApiConfig = MailgunAdapter.validateConfig({}); // Just get some empty values
return false;
}

mailConfigs[appId] = mailApiConfig;
return true;
}

function setAdapter(appId, mailAdapter) {
mailServices[appId] = mailAdapter;
}

function clearMailService(appId) {
delete mailConfigs[appId];
delete mailServices[appId];
}

function getMailService(appId) {
if (mailServices[appId]) {
return mailServices[appId];
}

if(mailConfigs[appId]) {
mailServices[appId] = new adapter(appId, mailConfigs[appId]);
return mailServices[appId];
} else {
return null;
}
}

module.exports = {
mailConfigs: mailConfigs,
mailServices: mailServices,
configureDefaultAdapter: configureDefaultAdapter,
setAdapter: setAdapter,
getMailService: getMailService,
clearMailService: clearMailService
};
60 changes: 60 additions & 0 deletions mail/MailgunAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
var MailAdapter = require('./MailAdapter');
// options can contain:
function MailgunAdapter(appId, mailApiConfig) {
this.appId = appId;
this.apiConfig = mailApiConfig;
MailAdapter.call(this);
}

MailgunAdapter.prototype = Object.create(MailAdapter);

// Connects to the database. Returns a promise that resolves when the
// connection is successful.
// this.db will be populated with a Mongo "Db" object when the
// promise resolves successfully.
MailgunAdapter.sendMail = function(to, subject, text, html) {

var mailgun = require('mailgun-js')({apiKey: this.apiConfig.apiKey, domain: this.apiConfig.domain});

var data = {
from: this.apiConfig.fromAddress,
to: to,
subject: subject,
text: text,
html: html
};

return new Promise((resolve, reject) => {
mailgun.messages().send(data, (err, body) => {
if (typeof err !== 'undefined') {
// console.log("Mailgun Error", err);
return reject(err);
}
// console.log(body);
resolve(body);
});
});
};

MailgunAdapter.validateConfig = function(config) {
var cfg = {apiKey:'', domain:'', fromAddress:''};
var helperMessage = "When creating an instance of ParseServer, you should have a mailConfig section like this: mailConfig: { service:'mailgun', apiKey:'MAILGUN_KEY_HERE', domain:'MAILGUN_DOMAIN_HERE', fromAddress:'MAILGUN_FROM_ADDRESS_HERE' }";
if(typeof config.apiKey === 'undefined' || config.apiKey === '') {
console.error('Error: You need to define a MailGun `apiKey` when configuring ParseServer. ' + helperMessage);
} else {
cfg.apiKey = config.apiKey;
}
if(typeof config.domain === 'undefined' || config.domain === '') {
console.error('Error: You need to define a MailGun `domain` when configuring ParseServer. ' + helperMessage);
} else {
cfg.domain = config.domain;
}
if(typeof config.fromAddress === 'undefined' || config.fromAddress === '') {
console.error('Error: You need to define a MailGun `fromAddress` when configuring ParseServer. ' + helperMessage);
} else {
cfg.fromAddress = config.fromAddress;
}
return cfg;
};

module.exports = MailgunAdapter;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"deepcopy": "^0.6.1",
"express": "^4.13.4",
"hat": "~0.0.3",
"mailgun-js": "^0.7.7",
"mime": "^1.3.4",
"mongodb": "~2.1.0",
"multer": "^1.1.0",
Expand Down
Loading