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

Commit

Permalink
feat(mailer): check for hard bounced or complaints before sending emails
Browse files Browse the repository at this point in the history
  • Loading branch information
seanmonstar committed Mar 9, 2017
1 parent c2dc6fc commit 51f85ce
Show file tree
Hide file tree
Showing 16 changed files with 878 additions and 278 deletions.
114 changes: 55 additions & 59 deletions bin/key_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require('../lib/newrelic')()

var config = require('../config').getProperties()
var jwtool = require('fxa-jwtool')
var P = require('../lib/promise')

var log = require('../lib/log')(config.log.level)
var getGeoData = require('../lib/geodb')(log)
Expand Down Expand Up @@ -76,66 +77,61 @@ function main() {
log.stat(Password.stat())
}

let translator
require('../lib/senders/translator')(config.i18n.supportedLanguages, config.i18n.defaultLanguage)
.then(result => {
translator = result
return require('../lib/senders')(log, config, translator)
})
.then(
function(result) {
senders = result

var DB = require('../lib/db')(
config,
log,
error,
Token.SessionToken,
Token.KeyFetchToken,
Token.AccountResetToken,
Token.PasswordForgotToken,
Token.PasswordChangeToken,
UnblockCode
)

DB.connect(config[config.db.backend])
.then(
function (db) {
database = db
customs = new Customs(config.customsUrl)
var routes = require('../lib/routes')(
log,
error,
serverPublicKeys,
signer,
db,
senders.email,
senders.sms,
Password,
config,
customs
)
server = Server.create(log, error, config, routes, db, translator)

server.start(
function (err) {
if (err) {
log.error({ op: 'server.start.1', msg: 'failed startup with error',
err: { message: err.message } })
process.exit(1)
} else {
log.info({ op: 'server.start.1', msg: 'running on ' + server.info.uri })
}
}
)
statsInterval = setInterval(logStatInfo, 15000)
},
function (err) {
log.error({ op: 'DB.connect', err: { message: err.message } })
process.exit(1)
}
)
var DB = require('../lib/db')(
config,
log,
error,
Token.SessionToken,
Token.KeyFetchToken,
Token.AccountResetToken,
Token.PasswordForgotToken,
Token.PasswordChangeToken,
UnblockCode
)

P.all([
DB.connect(config[config.db.backend]),
require('../lib/senders/translator')(config.i18n.supportedLanguages, config.i18n.defaultLanguage)
])
.spread(
(db, translator) => {
database = db

require('../lib/senders')(log, config, error, db, translator)
.then(result => {
senders = result
customs = new Customs(config.customsUrl)
var routes = require('../lib/routes')(
log,
error,
serverPublicKeys,
signer,
db,
senders.email,
senders.sms,
Password,
config,
customs
)
server = Server.create(log, error, config, routes, db, translator)

server.start(
function (err) {
if (err) {
log.error({ op: 'server.start.1', msg: 'failed startup with error',
err: { message: err.message } })
process.exit(1)
} else {
log.info({ op: 'server.start.1', msg: 'running on ' + server.info.uri })
}
}
)
statsInterval = setInterval(logStatInfo, 15000)
})
},
function (err) {
log.error({ op: 'DB.connect', err: { message: err.message } })
process.exit(1)
}
)

Expand Down
46 changes: 46 additions & 0 deletions config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,52 @@ var conf = convict({
format: String,
default: undefined,
env: 'SES_CONFIGURATION_SET'
},
bounces: {
enabled: {
doc: 'Flag to enable checking for bounces before sending email',
default: true,
env: 'BOUNCES_ENABLED'
},
complaint: {
duration: {
doc: 'Time until a complaint is no longer counted',
default: '1 year',
format: 'duration',
env: 'BOUNCES_COMPLAINT_DURATION'
},
max: {
doc: 'Maximum number of complaints before blocking emails',
default: 0,
env: 'BOUNCES_COMPLAINT_MAX'
}
},
hard: {
duration: {
doc: 'Time until a hard bounce is no longer counted',
default: '1 year',
format: 'duration',
env: 'BOUNCES_HARD_DURATION'
},
max: {
doc: 'Maximum number of hard bounces before blocking emails',
default: 0,
env: 'BOUNCES_HARD_MAX'
}
},
soft: {
duration: {
doc: 'Time until a soft bounce is no longer counted',
default: '5 mins',
format: 'duration',
env: 'BOUNCES_SOFT_DURATION'
},
max: {
doc: 'Maximum number of soft bounces before blocking emails',
default: 0,
env: 'BOUNCES_SOFT_MAX'
}
}
}
},
maxEventLoopDelay: {
Expand Down
3 changes: 3 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ The currently-defined error responses are:
* status code 400, errno 130: invalid region
* status code 400, errno 131: invalid message id
* status code 400, errno 132: message rejected
* status code 400, errno 133: email sent complaint
* status code 400, errno 134: email hard bounced
* status code 400, errno 135: email soft bounced
* status code 503, errno 201: service temporarily unavailable to due high load (see [backoff protocol](#backoff-protocol))
* status code 503, errno 202: feature has been disabled for operational reasons
* any status code, errno 999: unknown error
Expand Down
9 changes: 9 additions & 0 deletions lib/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,15 @@ module.exports = function (
return this.pool.post('/emailBounces', bounceData)
}

DB.prototype.emailBounces = function (email) {
log.trace({
op: 'DB.emailBounces',
email: email
})

return this.pool.get('/emailBounces/' + Buffer(email, 'utf8').toString('hex'))
}

function wrapTokenNotFoundError (err) {
if (isNotFoundError(err)) {
err = error.invalidToken('The authentication token could not be found')
Expand Down
30 changes: 30 additions & 0 deletions lib/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ var ERRNO = {
INVALID_REGION: 130,
INVALID_MESSAGE_ID: 131,
MESSAGE_REJECTED: 132,
BOUNCE_COMPLAINT: 133,
BOUNCE_HARD: 134,
BOUNCE_SOFT: 135,
SERVER_BUSY: 201,
FEATURE_NOT_ENABLED: 202,
UNEXPECTED_ERROR: 999
Expand Down Expand Up @@ -509,6 +512,33 @@ AppError.invalidMessageId = () => {
})
}

AppError.emailComplaint = () => {
return new AppError({
code: 400,
error: 'Bad Request',
errno: ERRNO.BOUNCE_COMPLAINT,
message: 'Email account sent complaint'
})
}

AppError.emailBouncedHard = () => {
return new AppError({
code: 400,
error: 'Bad Request',
errno: ERRNO.BOUNCE_HARD,
message: 'Email account hard bounced'
})
}

AppError.emailBouncedSoft = () => {
return new AppError({
code: 400,
error: 'Bad Request',
errno: ERRNO.BOUNCE_SOFT,
message: 'Email account soft bounced'
})
}

AppError.messageRejected = (reason, reasonCode) => {
return new AppError({
code: 500,
Expand Down
8 changes: 8 additions & 0 deletions lib/routes/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,14 @@ module.exports = function (
uaDeviceType: sessionToken.uaDeviceType
}
)
.catch(e => {
// If we couldn't email them, no big deal. Log
// and pretend everything worked.
log.trace({
op: 'Account.login.sendNewDeviceLoginNotification.error',
error: e
})
})
}
)
}
Expand Down
8 changes: 8 additions & 0 deletions lib/routes/password.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,14 @@ module.exports = function (
timeZone: geoData.timeZone
}, request.headers['user-agent'], log)
)
.catch(e => {
// If we couldn't email them, no big deal. Log
// and pretend everything worked.
log.trace({
op: 'Password.changeFinish.sendPasswordChangedNotification.error',
error: e
})
})
}
)
}
Expand Down
Loading

0 comments on commit 51f85ce

Please sign in to comment.