diff --git a/common/dist/scripts/habitrpg-shared.js b/common/dist/scripts/habitrpg-shared.js index 5e76967282..9b1d44793f 100644 --- a/common/dist/scripts/habitrpg-shared.js +++ b/common/dist/scripts/habitrpg-shared.js @@ -7451,7 +7451,6 @@ process.browser = true; process.env = {}; process.argv = []; process.version = ''; // empty string to avoid regexp issues -process.versions = {}; function noop() {} diff --git a/common/locales/en/settings.json b/common/locales/en/settings.json index d182b5940b..db4bf0953e 100644 --- a/common/locales/en/settings.json +++ b/common/locales/en/settings.json @@ -98,6 +98,9 @@ "invitedQuest": "Invited to Quest", "kickedGroup": "Kicked from group", "remindersToLogin": "Reminders to check in to HabitRPG", + "unsubscribedSuccessfully": "Unsubscribed successfully!", + "unsubscribedTextUsers": "You have successfully unsubscribed from all HabitRPG emails. You can enable only the emails you want to receive from the settings (requires login).", + "unsubscribedTextOthers": "You won't receive any other email from HabitRPG.", "unsubscribeAllEmails": "Check to Unsubscribe from Emails", "unsubscribeAllEmailsText": "By checking this box, I certify that I understand that by unsubscribing from all emails, HabitRPG will never be able to notify me via email about important changes to the site or my account.", "correctlyUnsubscribedEmailType": "Correctly unsubscribed from \"<%= emailType %>\" emails.", diff --git a/website/src/controllers/auth.js b/website/src/controllers/auth.js index 25681d9474..a21d4ebc76 100644 --- a/website/src/controllers/auth.js +++ b/website/src/controllers/auth.js @@ -7,6 +7,7 @@ var utils = require('../utils'); var nconf = require('nconf'); var request = require('request'); var User = require('../models/user').model; +var EmailUnsubscription = require('../models/emailUnsubscription').model; var ga = require('./../utils').ga; var i18n = require('./../i18n'); @@ -109,9 +110,14 @@ api.registerUser = function(req, res, next) { newUser.preferences = newUser.preferences || {}; newUser.preferences.language = req.language; // User language detected from browser, not saved var user = new User(newUser); - utils.txnEmail(user, 'welcome'); ga.event('register', 'Local').send(); - user.save(cb); + user.save(function(err, savedUser){ + // Clean previous email preferences + EmailUnsubscription.remove({email: savedUser.auth.local.email}, function(){ + utils.txnEmail(savedUser, 'welcome'); + }); + cb.apply(cb, arguments); + }); } }] }, function(err, data) { @@ -178,9 +184,16 @@ api.loginSocial = function(req, res, next) { }; user.auth[network] = prof; user = new User(user); - user.save(cb); + user.save(function(err, savedUser){ + // Clean previous email preferences + if(savedUser.auth.facebook.emails && savedUser.auth.facebook.emails[0] && savedUser.auth.facebook.emails[0].value){ + EmailUnsubscription.remove({email: savedUser.auth.facebook.emails[0].value}, function(){ + utils.txnEmail(savedUser, 'welcome'); + }); + } + cb.apply(cb, arguments); + }); - utils.txnEmail(user, 'welcome'); ga.event('register', network).send(); }] }, function(err, results){ diff --git a/website/src/controllers/groups.js b/website/src/controllers/groups.js index 4e2a93d79f..f93bf9c47f 100644 --- a/website/src/controllers/groups.js +++ b/website/src/controllers/groups.js @@ -12,6 +12,7 @@ var shared = require('../../../common'); var User = require('./../models/user').model; var Group = require('./../models/group').model; var Challenge = require('./../models/challenge').model; +var EmailUnsubscription = require('./../models/emailUnsubscription').model; var isProd = nconf.get('NODE_ENV') === 'production'; var api = module.exports; @@ -623,10 +624,15 @@ var inviteByEmails = function(invites, group, req, res, next){ } // TODO implement "users can only be invited once" - invite.canSend = true; // Requested by utils.txnEmail - utils.txnEmail(invite, ('invite-friend' + (group.type == 'guild' ? '-guild' : '')), variables); + // Check for the email address not to be unsubscribed + EmailUnsubscription.findOne({email: invite.email}, function(err, unsubscribed){ + if(err) return cb(err); + if(unsubscribed) return cb(); - cb(); + utils.txnEmail(invite, ('invite-friend' + (group.type == 'guild' ? '-guild' : '')), variables); + + cb(); + }); }); }else{ cb(); diff --git a/website/src/controllers/unsubscription.js b/website/src/controllers/unsubscription.js new file mode 100644 index 0000000000..d0ef89f920 --- /dev/null +++ b/website/src/controllers/unsubscription.js @@ -0,0 +1,36 @@ +var User = require('../models/user').model; +var EmailUnsubscription = require('../models/emailUnsubscription').model; +var utils = require('../utils'); +var i18n = require('../../../common').i18n; + +var api = module.exports = {}; + +api.unsubscribe = function(req, res, next){ + if(!req.query.code) return res.json(500, {err: 'Missing unsubscription code.'}); + + var data = JSON.parse(utils.decrypt(req.query.code)); + + if(data._id){ + User.update({_id: data._id}, { + $set: {'preferences.emailNotifications.unsubscribeFromAll': true} + }, {multi: false}, function(err, nAffected){ + if(err) return next(err); + if(nAffected !== 1) return res.json(404, {err: 'User not found'}); + + res.send('

' + i18n.t('unsubscribedSuccessfully', null, req.language) + '

' + i18n.t('unsubscribedTextUsers', null, req.language)); + }); + }else{ + EmailUnsubscription.findOne({email: data.email}, function(err, doc){ + if(err) return next(err); + var okRes = '

' + i18n.t('unsubscribedSuccessfully', null, req.language) + '

' + i18n.t('unsubscribedTextOthers', null, req.language); + + if(doc) return res.send(okRes); + + EmailUnsubscription.create({email: data.email}, function(err, doc){ + if(err) return next(err); + + res.send(okRes); + }) + }); + } +}; \ No newline at end of file diff --git a/website/src/models/emailUnsubscription.js b/website/src/models/emailUnsubscription.js new file mode 100644 index 0000000000..144417f3fc --- /dev/null +++ b/website/src/models/emailUnsubscription.js @@ -0,0 +1,14 @@ +var mongoose = require("mongoose"); +var shared = require('../../../common'); + +// A collection used to store mailing list unsubscription for non registered email addresses +var EmailUnsubscriptionSchema = new mongoose.Schema({ + _id: { + type: String, + 'default': shared.uuid + }, + email: String +}); + +module.exports.schema = EmailUnsubscriptionSchema; +module.exports.model = mongoose.model('EmailUnsubscription', EmailUnsubscriptionSchema); \ No newline at end of file diff --git a/website/src/routes/unsubscription.js b/website/src/routes/unsubscription.js new file mode 100644 index 0000000000..efd06f9197 --- /dev/null +++ b/website/src/routes/unsubscription.js @@ -0,0 +1,8 @@ +var express = require('express'); +var router = new express.Router(); +var i18n = require('../i18n'); +var unsubscription = require('../controllers/unsubscription'); + +router.get('/unsubscribe', i18n.getUserLanguage, unsubscription.unsubscribe); + +module.exports = router; \ No newline at end of file diff --git a/website/src/server.js b/website/src/server.js index 1c3484d776..1447823a4d 100644 --- a/website/src/server.js +++ b/website/src/server.js @@ -127,6 +127,7 @@ if (cores!==0 && cluster.isMaster && (isDev || isProd)) { app.use(require('./routes/payments').middleware); app.use(require('./routes/auth').middleware); app.use(require('./routes/coupon').middleware); + app.use(require('./routes/unsubscription').middleware); var v2 = express(); app.use('/api/v2', v2); app.use('/api/v1', require('./routes/apiv1').middleware); diff --git a/website/src/utils.js b/website/src/utils.js index 6349ca5d3e..c2e0085087 100644 --- a/website/src/utils.js +++ b/website/src/utils.js @@ -44,6 +44,10 @@ function getUserInfo(user, fields) { } } + if(fields.indexOf('_id') != -1){ + info._id = user._id; + } + if(fields.indexOf('canSend') != -1){ info.canSend = user.preferences.emailNotifications.unsubscribeFromAll !== true; } @@ -62,37 +66,59 @@ module.exports.txnEmail = function(mailingInfoArray, emailType, variables, perso // It's important to pass at least a user with its `preferences` as we need to check if he unsubscribed mailingInfoArray = mailingInfoArray.map(function(mailingInfo){ - return mailingInfo._id ? getUserInfo(mailingInfo, ['email', 'name', 'canSend']) : mailingInfo; + return mailingInfo._id ? getUserInfo(mailingInfo, ['_id', 'email', 'name', 'canSend']) : mailingInfo; }).filter(function(mailingInfo){ // Always send reset-password emails - return (mailingInfo.email && (mailingInfo.canSend || emailType === 'reset-password')); + // Don't check canSend for non registered users as already checked before + return (mailingInfo.email && ((!mailingInfo._id || mailingInfo.canSend) || emailType === 'reset-password')); }); // Personal variables are personal to each email recipient, if they are missing - // we manually create a structure for them with RECIPIENT_NAME - // otherwise we just add RECIPIENT_NAME to the existing personal variables + // we manually create a structure for them with RECIPIENT_NAME and RECIPIENT_UNSUB_URL + // otherwise we just add RECIPIENT_NAME and RECIPIENT_UNSUB_URL to the existing personal variables if(!personalVariables || personalVariables.length === 0){ personalVariables = mailingInfoArray.map(function(mailingInfo){ return { rcpt: mailingInfo.email, - vars: [{ - name: 'RECIPIENT_NAME', - content: mailingInfo.name - }] + vars: [ + { + name: 'RECIPIENT_NAME', + content: mailingInfo.name + }, + { + name: 'RECIPIENT_UNSUB_URL', + content: baseUrl + '/unsubscribe?code=' + module.exports.encrypt(JSON.stringify({ + _id: mailingInfo._id, + email: mailingInfo.email + })) + } + ] } }); }else{ var temporaryPersonalVariables = {}; mailingInfoArray.forEach(function(mailingInfo){ - temporaryPersonalVariables[mailingInfo.email] = mailingInfo.name; + temporaryPersonalVariables[mailingInfo.email] = { + name: mailingInfo.name, + _id: mailingInfo._id + } }); personalVariables.forEach(function(singlePersonalVariables){ - singlePersonalVariables.vars.push({ - name: 'RECIPIENT_NAME', - content: temporaryPersonalVariables[singlePersonalVariables.rcpt] - }); + singlePersonalVariables.vars.push( + { + name: 'RECIPIENT_NAME', + content: temporaryPersonalVariables[singlePersonalVariables.rcpt].name + }, + { + name: 'RECIPIENT_UNSUB_URL', + content: baseUrl + '/unsubscribe?code=' + module.exports.encrypt(JSON.stringify({ + _id: temporaryPersonalVariables[singlePersonalVariables.rcpt]._id, + email: singlePersonalVariables.rcpt + })) + } + ) }); }