Skip to content
This repository has been archived by the owner on Apr 3, 2019. It is now read-only.

Commit

Permalink
refactor(bounces): pull bounce logic into separate module
Browse files Browse the repository at this point in the history
  • Loading branch information
seanmonstar committed Mar 28, 2017
1 parent b06b0da commit 48d7625
Show file tree
Hide file tree
Showing 15 changed files with 270 additions and 203 deletions.
4 changes: 3 additions & 1 deletion bin/key_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,9 @@ function run(config) {
.spread(
(db, translator) => {
database = db
const bounces = require('../lib/bounces')(config, db)

return require('../lib/senders')(log, config, error, db, translator)
return require('../lib/senders')(log, config, error, bounces, translator)
.then(result => {
senders = result
customs = new Customs(config.customsUrl)
Expand All @@ -106,6 +107,7 @@ function run(config) {
serverPublicKeys,
signer,
db,
bounces,
senders.email,
senders.sms,
Password,
Expand Down
73 changes: 73 additions & 0 deletions lib/bounces.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

'use strict'

const error = require('./error')
const P = require('./promise')

module.exports = (config, db) => {
const configBounces = config.smtp && config.smtp.bounces || {}
const BOUNCES_ENABLED = !! configBounces.enabled
const MAX_HARD = configBounces.hard && configBounces.hard.max || 0
const MAX_SOFT = configBounces.soft && configBounces.soft.max || 0
const MAX_COMPLAINT = configBounces.complaint && configBounces.complaint.max || 0
const DURATION_HARD = configBounces.hard && configBounces.hard.duration || Infinity
const DURATION_SOFT = configBounces.soft && configBounces.soft.duration || Infinity
const DURATION_COMPLAINT = configBounces.complaint && configBounces.complaint.duration || Infinity
const BOUNCE_TYPE_HARD = 1
const BOUNCE_TYPE_SOFT = 2
const BOUNCE_TYPE_COMPLAINT = 3

const freeze = Object.freeze
const BOUNCE_RULES = freeze({
[BOUNCE_TYPE_HARD]: freeze({
duration: DURATION_HARD,
error: error.emailBouncedHard,
max: MAX_HARD
}),
[BOUNCE_TYPE_COMPLAINT]: freeze({
duration: DURATION_COMPLAINT,
error: error.emailComplaint,
max: MAX_COMPLAINT
}),
[BOUNCE_TYPE_SOFT]: freeze({
duration: DURATION_SOFT,
error: error.emailBouncedSoft,
max: MAX_SOFT
})
})

function checkBounces(email) {
return db.emailBounces(email)
.then(bounces => {
const counts = {
[BOUNCE_TYPE_HARD]: 0,
[BOUNCE_TYPE_COMPLAINT]: 0,
[BOUNCE_TYPE_SOFT]: 0
}
const now = Date.now()
bounces.forEach(bounce => {
const type = bounce.bounceType
const ruleSet = BOUNCE_RULES[type]
if (ruleSet) {
if (bounce.createdAt > now - ruleSet.duration) {
counts[type]++
if (counts[type] > ruleSet.max) {
throw ruleSet.error()
}
}
}
})
})
}

function disabled() {
return P.resolve()
}

return {
check: BOUNCES_ENABLED ? checkBounces : disabled
}
}
7 changes: 7 additions & 0 deletions lib/routes/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ module.exports = function (
isA,
error,
db,
bounces,
mailer,
Password,
config,
Expand Down Expand Up @@ -1619,6 +1620,12 @@ module.exports = function (
return P.resolve()
}

/*
function checkBounces() {
}
*/

function createResponse() {

var sessionVerified = sessionToken.tokenVerified
Expand Down
2 changes: 2 additions & 0 deletions lib/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module.exports = function (
serverPublicKeys,
signer,
db,
bounces,
mailer,
smsImpl,
Password,
Expand All @@ -35,6 +36,7 @@ module.exports = function (
isA,
error,
db,
bounces,
mailer,
Password,
config,
Expand Down
67 changes: 4 additions & 63 deletions lib/senders/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@
// there's nothing left that imports mailer/config, it is safe to merge
// legacy_index.js and this file into one.
var createSenders = require('./legacy_index')
var P = require('../promise')

module.exports = function (log, config, error, db, translator, sender) {
module.exports = function (log, config, error, bounces, translator, sender) {
var defaultLanguage = config.i18n.defaultLanguage

return createSenders(
Expand All @@ -25,62 +24,10 @@ module.exports = function (log, config, error, db, translator, sender) {
)
.then(function (senders) {
var ungatedMailer = senders.email
var configBounces = config.smtp && config.smtp.bounces || {}
var BOUNCES_ENABLED = !! configBounces.enabled
var MAX_HARD = configBounces.hard && configBounces.hard.max || 0
var MAX_SOFT = configBounces.soft && configBounces.soft.max || 0
var MAX_COMPLAINT = configBounces.complaint && configBounces.complaint.max || 0
var DURATION_HARD = configBounces.hard && configBounces.hard.duration || Infinity
var DURATION_SOFT = configBounces.soft && configBounces.soft.duration || Infinity
var DURATION_COMPLAINT = configBounces.complaint && configBounces.complaint.duration || Infinity
var BOUNCE_TYPE_HARD = 1
var BOUNCE_TYPE_SOFT = 2
var BOUNCE_TYPE_COMPLAINT = 3

// I really wanted to use Computer property names here, but thats
// an ES2015 feature, and this directory (senders) is stuck in
// ES5-land.
var freeze = Object.freeze
var BOUNCE_RULES = {}
BOUNCE_RULES[BOUNCE_TYPE_HARD] = freeze({
duration: DURATION_HARD,
error: error.emailBouncedHard,
max: MAX_HARD
})
BOUNCE_RULES[BOUNCE_TYPE_COMPLAINT] = freeze({
duration: DURATION_COMPLAINT,
error: error.emailComplaint,
max: MAX_COMPLAINT
})
BOUNCE_RULES[BOUNCE_TYPE_SOFT] = freeze({
duration: DURATION_SOFT,
error: error.emailBouncedSoft,
max: MAX_SOFT
})
BOUNCE_RULES = freeze(BOUNCE_RULES)

function bounceGatedMailer(email) {
return db.emailBounces(email)
.then(function (bounces) {
var counts = {}
counts[BOUNCE_TYPE_HARD] = 0
counts[BOUNCE_TYPE_COMPLAINT] = 0
counts[BOUNCE_TYPE_SOFT] = 0
var now = Date.now()
bounces.forEach(function (bounce) {
var type = bounce.bounceType
var ruleSet = BOUNCE_RULES[type]
if (ruleSet) {
if (bounce.createdAt > now - ruleSet.duration) {
counts[type]++
if (counts[type] > ruleSet.max) {
throw ruleSet.error()
}
}
}
})
return ungatedMailer
})
function getSafeMailer(email) {
return bounces.check(email)
.return(ungatedMailer)
.catch(function (err) {
log.info({
op: 'mailer.blocked',
Expand All @@ -90,12 +37,6 @@ module.exports = function (log, config, error, db, translator, sender) {
})
}

function noopMailer() {
return P.resolve(ungatedMailer)
}

var getSafeMailer = BOUNCES_ENABLED ? bounceGatedMailer : noopMailer

function getVerifiedSecondaryEmails(emais) {
return emais.reduce(function(list, email) {
if (! email.isPrimary && email.isVerified) {
Expand Down
7 changes: 4 additions & 3 deletions scripts/write-emails-to-disk.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,14 @@ var mailSender = {
close: function () {}
}

const db = {
emailBounces: () => P.resolve([])
const bounces = {
// this is for dev purposes, no need to check db
check: () => P.resolve()
}

require('../lib/senders/translator')(config.i18n.supportedLanguages, config.i18n.defaultLanguage)
.then(translator => {
return createSenders(log, config, error, db, translator, mailSender)
return createSenders(log, config, error, bounces, translator, mailSender)
})
.then((senders) => {
const mailer = senders.email._ungatedMailer
Expand Down
1 change: 1 addition & 0 deletions test/local/ip_profiling.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ var makeRoutes = function (options, requireMocks) {
isA,
error,
db,
mocks.mockBounces(),
options.mailer || {},
Password,
config,
Expand Down
139 changes: 139 additions & 0 deletions test/local/lib/bounces.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

'use strict'

const ROOT_DIR = '../../..'

const assert = require('insist')
const config = require(`${ROOT_DIR}/config`).getProperties()
const createBounces = require(`${ROOT_DIR}/lib/bounces`)
const error = require(`${ROOT_DIR}/lib/error`)
const P = require('bluebird')
const sinon = require('sinon')

const EMAIL = Math.random() + '@example.test'
const BOUNCE_TYPE_HARD = 1
const BOUNCE_TYPE_COMPLAINT = 3

const NOW = Date.now()

describe('bounces', () => {

it('succeeds if bounces not over limit', () => {
const db = {
emailBounces: sinon.spy(() => P.resolve([]))
}
return createBounces(config, db).check(EMAIL)
.then(() => {
assert.equal(db.emailBounces.callCount, 1)
})
})

it('error if complaints over limit', () => {
const conf = Object.assign({}, config)
conf.smtp = {
bounces: {
enabled: true,
complaint: {
max: 0
}
}
}
const db = {
emailBounces: sinon.spy(() => P.resolve([
{
bounceType: BOUNCE_TYPE_COMPLAINT,
createdAt: NOW
}
]))
}
return createBounces(conf, db).check(EMAIL)
.then(
() => assert(false),
e => {
assert.equal(db.emailBounces.callCount, 1)
assert.equal(e.errno, error.ERRNO.BOUNCE_COMPLAINT)
}
)
})

it('error if hard bounces over limit', () => {
const conf = Object.assign({}, config)
conf.smtp = {
bounces: {
enabled: true,
hard: {
max: 0
}
}
}
const db = {
emailBounces: sinon.spy(() => P.resolve([
{
bounceType: BOUNCE_TYPE_HARD,
createdAt: NOW
}
]))
}
return createBounces(conf, db).check(EMAIL)
.then(
() => assert(false),
e => {
assert.equal(db.emailBounces.callCount, 1)
assert.equal(e.errno, error.ERRNO.BOUNCE_HARD)
}
)
})

it('does not error if not enough bounces in duration', () => {
const conf = Object.assign({}, config)
conf.smtp = {
bounces: {
enabled: true,
hard: {
max: 0,
duration: 5000
}
}
}
const db = {
emailBounces: sinon.spy(() => P.resolve([
{
bounceType: BOUNCE_TYPE_HARD,
createdAt: Date.now() - 20000
}
]))
}
return createBounces(conf, db).check(EMAIL)
.then(() => {
assert.equal(db.emailBounces.callCount, 1)
})
})


describe('disabled', () => {
it('does not call bounces.check if disabled', () => {
const conf = Object.assign({}, config)
conf.smtp = {
bounces: {
enabled: false
}
}
const db = {
emailBounces: sinon.spy(() => P.resolve([
{
bounceType: BOUNCE_TYPE_HARD,
createdAt: Date.now() - 20000
}
]))
}
assert.equal(db.emailBounces.callCount, 0)
return createBounces(conf, db).check(EMAIL)
.then(() => {
assert.equal(db.emailBounces.callCount, 0)
})
})
})
})
Loading

0 comments on commit 48d7625

Please sign in to comment.