diff --git a/.eslintrc b/.eslintrc index 69c945431e..a3bc6debda 100644 --- a/.eslintrc +++ b/.eslintrc @@ -28,7 +28,6 @@ "no-new": 2, "no-octal-escape": 2, "no-octal": 2, - "no-param-reassign": 2, "no-process-env": 2, "no-proto": 2, "no-implied-eval": 2, diff --git a/package.json b/package.json index 40c04bdfbb..8e0603e0db 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,10 @@ "babel": "^5.5.4", "babelify": "^7.2.0", "body-parser": "^1.14.1", - "compression": "^1.6.0", "bower": "~1.3.12", "browserify": "~12.0.1", "coffee-script": "1.6.x", + "compression": "^1.6.0", "connect-ratelimit": "0.0.7", "cookie-parser": "^1.4.0", "cookie-session": "^1.2.0", @@ -64,7 +64,7 @@ "nconf": "~0.8.2", "newrelic": "~1.23.0", "nib": "~1.0.1", - "nodemailer": "~0.5.2", + "nodemailer": "^1.9.0", "pageres": "^1.0.1", "passport": "~0.2.1", "passport-facebook": "2.0.0", diff --git a/tasks/gulp-console.js b/tasks/gulp-console.js index 46f4ea44ea..d5d318c28e 100644 --- a/tasks/gulp-console.js +++ b/tasks/gulp-console.js @@ -2,7 +2,7 @@ import mongoose from 'mongoose'; import autoinc from 'mongoose-id-autoinc'; import logger from '../website/src/libs/api-v3/logger'; import nconf from 'nconf'; -import utils from '../website/src/libs/utils'; +import utils from '../website/src/libs/api-v2/utils'; import repl from 'repl'; import gulp from 'gulp'; diff --git a/test/server_side/controllers/groups.test.js b/test/server_side/controllers/groups.test.js index 9e970c4afc..9574ed7640 100644 --- a/test/server_side/controllers/groups.test.js +++ b/test/server_side/controllers/groups.test.js @@ -8,7 +8,7 @@ var Group = require('../../../website/src/models/group').model; var groupsController = require('../../../website/src/controllers/api-v2/groups'); describe('Groups Controller', function() { - var utils = require('../../../website/src/libs/utils'); + var utils = require('../../../website/src/libs/api-v2/utils'); describe('#invite', function() { var res, req, user, group; diff --git a/website/src/controllers/api-v2/auth.js b/website/src/controllers/api-v2/auth.js index 11cab90624..dd379cc095 100644 --- a/website/src/controllers/api-v2/auth.js +++ b/website/src/controllers/api-v2/auth.js @@ -3,7 +3,7 @@ var validator = require('validator'); var passport = require('passport'); var shared = require('../../../../common'); var async = require('async'); -var utils = require('../../libs/utils'); +var utils = require('../../libs/api-v2/utils'); var nconf = require('nconf'); var request = require('request'); var FirebaseTokenGenerator = require('firebase-token-generator'); diff --git a/website/src/controllers/api-v2/challenges.js b/website/src/controllers/api-v2/challenges.js index 21104f2885..89d68495d6 100644 --- a/website/src/controllers/api-v2/challenges.js +++ b/website/src/controllers/api-v2/challenges.js @@ -9,7 +9,7 @@ var Group = require('./../../models/group').model; var Challenge = require('./../../models/challenge').model; var logging = require('./../../libs/api-v2/logging'); var csv = require('express-csv'); -var utils = require('../../libs/utils'); +var utils = require('../../libs/api-v2/utils'); var api = module.exports; var pushNotify = require('./../pushNotifications'); diff --git a/website/src/controllers/api-v2/groups.js b/website/src/controllers/api-v2/groups.js index 62ac3804b5..0355148856 100644 --- a/website/src/controllers/api-v2/groups.js +++ b/website/src/controllers/api-v2/groups.js @@ -9,7 +9,7 @@ var _ = require('lodash'); var nconf = require('nconf'); var async = require('async'); var Q = require('q'); -var utils = require('./../../libs/utils'); +var utils = require('./../../libs/api-v2/utils'); var shared = require('../../../../common'); var User = require('./../../models/user').model; var Group = require('./../../models/group').model; diff --git a/website/src/controllers/api-v2/members.js b/website/src/controllers/api-v2/members.js index c528b92cd7..1c3bb448ac 100644 --- a/website/src/controllers/api-v2/members.js +++ b/website/src/controllers/api-v2/members.js @@ -5,7 +5,7 @@ var api = module.exports; var async = require('async'); var _ = require('lodash'); var shared = require('../../../../common'); -var utils = require('../../libs/utils'); +var utils = require('../../libs/api-v2/utils'); var nconf = require('nconf'); var pushNotify = require('./../pushNotifications'); diff --git a/website/src/controllers/api-v2/unsubscription.js b/website/src/controllers/api-v2/unsubscription.js index 772d0580e9..65f37f6298 100644 --- a/website/src/controllers/api-v2/unsubscription.js +++ b/website/src/controllers/api-v2/unsubscription.js @@ -1,6 +1,6 @@ var User = require('../../models/user').model; var EmailUnsubscription = require('../../models/emailUnsubscription').model; -var utils = require('../../libs/utils'); +var utils = require('../../libs/api-v2/utils'); var i18n = require('../../../../common').i18n; var api = module.exports = {}; diff --git a/website/src/controllers/api-v2/user.js b/website/src/controllers/api-v2/user.js index 5c6d256044..519ae0c557 100644 --- a/website/src/controllers/api-v2/user.js +++ b/website/src/controllers/api-v2/user.js @@ -5,7 +5,7 @@ var nconf = require('nconf'); var async = require('async'); var shared = require('../../../../common'); var User = require('./../../models/user').model; -var utils = require('./../../libs/utils'); +var utils = require('./../../libs/api-v2/utils'); var analytics = utils.analytics; var Group = require('./../../models/group').model; var Challenge = require('./../../models/challenge').model; diff --git a/website/src/controllers/payments/index.js b/website/src/controllers/payments/index.js index 866c33381b..074fab2369 100644 --- a/website/src/controllers/payments/index.js +++ b/website/src/controllers/payments/index.js @@ -1,7 +1,7 @@ var _ = require('lodash'); var shared = require('../../../../common'); var nconf = require('nconf'); -var utils = require('./../../libs/utils'); +var utils = require('./../../libs/api-v2/utils'); var moment = require('moment'); var isProduction = nconf.get("NODE_ENV") === "production"; var stripe = require('./stripe'); diff --git a/website/src/index.js b/website/src/index.js index 134060bf45..1de0170cd1 100644 --- a/website/src/index.js +++ b/website/src/index.js @@ -9,13 +9,13 @@ var logger = require('./libs/api-v3/logger'); // Initialize configuration var setupNconf = require('./libs/api-v3/setupNconf'); setupNconf(); -var utils = require('./libs/utils'); -utils.setupConfig(); var IS_PROD = nconf.get('IS_PROD'); var IS_DEV = nconf.get('IS_DEV'); var cores = Number(nconf.get('WEB_CONCURRENCY')) || 0; +if (IS_DEV) Error.stackTraceLimit = Infinity; + // Setup the cluster module if (cores !== 0 && cluster.isMaster && (IS_DEV || IS_PROD)) { // Fork workers. If config.json has CORES=x, use that - otherwise, use all cpus-1 (production) diff --git a/website/src/libs/utils.js b/website/src/libs/api-v2/utils.js similarity index 100% rename from website/src/libs/utils.js rename to website/src/libs/api-v2/utils.js diff --git a/website/src/libs/api-v3/email.js b/website/src/libs/api-v3/email.js new file mode 100644 index 0000000000..8d0cff0a8c --- /dev/null +++ b/website/src/libs/api-v3/email.js @@ -0,0 +1,153 @@ +import { createTransport } from 'nodemailer'; +import nconf from 'nconf'; +import logger from './logger'; +import { encrypt } from './encryption'; +import request from 'request'; + +const IS_PROD = nconf.get('IS_PROD'); +const EMAIL_SERVER = { + url: nconf.get('EMAIL_SERVER:url'), + auth: { + user: nconf.get('EMAIL_SERVER:authUser'), + password: nconf.get('EMAIL_SERVER:authPassword'), + }, +}; +const BASE_URL = nconf.get('BASE_URL'); + +let smtpTransporter = createTransport({ + service: nconf.get('SMTP_SERVICE'), + auth: { + user: nconf.get('SMTP_USER'), + pass: nconf.get('SMTP_PASS'), + }, +}); + +// Send email directly from the server using the smtpTransporter, +// used only to send password reset emails because users unsubscribed on Mandrill wouldn't get them +export function send (mailData) { + return smtpTransporter + .sendMail(mailData) + .catch((error) => logger.error(error)); +} + +export function getUserInfo (user, fields) { + let info = {}; + + if (fields.indexOf('name') !== -1) { + if (user.auth.local) { + info.name = user.profile.name || user.auth.local.username; + } else if (user.auth.facebook) { + info.name = user.profile.name || user.auth.facebook.displayName || user.auth.facebook.username; + } + } + + if (fields.indexOf('email') !== -1) { + if (user.auth.local && user.auth.local.email) { + info.email = user.auth.local.email; + } else if (user.auth.facebook && user.auth.facebook.emails && user.auth.facebook.emails[0] && user.auth.facebook.emails[0].value) { + info.email = user.auth.facebook.emails[0].value; + } + } + + if (fields.indexOf('_id') !== -1) { + info._id = user._id; + } + + if (fields.indexOf('canSend') !== -1) { + info.canSend = user.preferences.emailNotifications.unsubscribeFromAll !== true; + } + + return info; +} + +// Send a transactional email using Mandrill through the external email server +export function txnEmail (mailingInfoArray, emailType, variables, personalVariables) { + mailingInfoArray = Array.isArray(mailingInfoArray) ? mailingInfoArray : [mailingInfoArray]; + + variables = [ + {name: 'BASE_URL', content: BASE_URL}, + ].concat(variables || []); + + // It's important to pass at least a user with its `preferences` as we need to check if he unsubscribed + mailingInfoArray = mailingInfoArray.map((mailingInfo) => { + return mailingInfo._id ? getUserInfo(mailingInfo, ['_id', 'email', 'name', 'canSend']) : mailingInfo; + }).filter((mailingInfo) => { + // Always send reset-password emails + // 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 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((mailingInfo) => { + return { + rcpt: mailingInfo.email, + vars: [ + { + name: 'RECIPIENT_NAME', + content: mailingInfo.name, + }, + { + name: 'RECIPIENT_UNSUB_URL', + content: `/unsubscribe?code=${encrypt(JSON.stringify({ + _id: mailingInfo._id, + email: mailingInfo.email, + }))}`, + }, + ], + }; + }); + } else { + let temporaryPersonalVariables = {}; + + mailingInfoArray.forEach((mailingInfo) => { + temporaryPersonalVariables[mailingInfo.email] = { + name: mailingInfo.name, + _id: mailingInfo._id, + }; + }); + + personalVariables.forEach((singlePersonalVariables) => { + singlePersonalVariables.vars.push( + { + name: 'RECIPIENT_NAME', + content: temporaryPersonalVariables[singlePersonalVariables.rcpt].name, + }, + { + name: 'RECIPIENT_UNSUB_URL', + content: `/unsubscribe?code=${encrypt(JSON.stringify({ + _id: temporaryPersonalVariables[singlePersonalVariables.rcpt]._id, + email: singlePersonalVariables.rcpt, + }))}`, + } + ); + }); + } + + if (IS_PROD && mailingInfoArray.length > 0) { + request({ + url: `${EMAIL_SERVER.url}/job`, + method: 'POST', + auth: { + user: EMAIL_SERVER.auth.user, + pass: EMAIL_SERVER.auth.password, + }, + json: { + type: 'email', + data: { + emailType, + to: mailingInfoArray, + variables, + personalVariables, + }, + options: { + priority: 'high', + attempts: 5, + backoff: {delay: 10 * 60 * 1000, type: 'fixed'}, + }, + }, + }, (err) => logger.error(err)); + } +} diff --git a/website/src/libs/api-v3/encryption.js b/website/src/libs/api-v3/encryption.js new file mode 100644 index 0000000000..4e370bb9a8 --- /dev/null +++ b/website/src/libs/api-v3/encryption.js @@ -0,0 +1,24 @@ +import { + createCipher, + createDecipher, +} from 'crypto'; +import nconf from 'nconf'; + +const algorithm = 'aes-256-ctr'; +const SESSION_SECRET = nconf.get('SESSION_SECRET'); + +export function encrypt (text) { + let cipher = createCipher(algorithm, SESSION_SECRET); + let crypted = cipher.update(text, 'utf8', 'hex'); + + crypted += cipher.final('hex'); + return crypted; +} + +export function decrypt (text) { + let decipher = createDecipher(algorithm, SESSION_SECRET); + let dec = decipher.update(text, 'hex', 'utf8'); + + dec += decipher.final('utf8'); + return dec; +} \ No newline at end of file diff --git a/website/src/middlewares/locals.js b/website/src/middlewares/locals.js index e6b2fa8818..e5caea707b 100644 --- a/website/src/middlewares/locals.js +++ b/website/src/middlewares/locals.js @@ -1,6 +1,6 @@ var nconf = require('nconf'); var _ = require('lodash'); -var utils = require('../libs/utils'); +var utils = require('../libs/api-v2/utils'); var shared = require('../../../common'); var i18n = require('../libs/api-v2/i18n'); var buildManifest = require('../libs/buildManifest'); diff --git a/website/src/server.js b/website/src/server.js index e9f7ddbbaa..e6a45aa743 100644 --- a/website/src/server.js +++ b/website/src/server.js @@ -2,7 +2,7 @@ import nconf from 'nconf'; import logger from './libs/api-v3/logger'; -import utils from './libs/utils'; +import utils from './libs/api-v2/utils'; import express from 'express'; import http from 'http'; // import path from 'path';