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

Commit 51f85ce

Browse files
committed
feat(mailer): check for hard bounced or complaints before sending emails
1 parent c2dc6fc commit 51f85ce

File tree

16 files changed

+878
-278
lines changed

16 files changed

+878
-278
lines changed

bin/key_server.js

Lines changed: 55 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require('../lib/newrelic')()
1010

1111
var config = require('../config').getProperties()
1212
var jwtool = require('fxa-jwtool')
13+
var P = require('../lib/promise')
1314

1415
var log = require('../lib/log')(config.log.level)
1516
var getGeoData = require('../lib/geodb')(log)
@@ -76,66 +77,61 @@ function main() {
7677
log.stat(Password.stat())
7778
}
7879

79-
let translator
80-
require('../lib/senders/translator')(config.i18n.supportedLanguages, config.i18n.defaultLanguage)
81-
.then(result => {
82-
translator = result
83-
return require('../lib/senders')(log, config, translator)
84-
})
85-
.then(
86-
function(result) {
87-
senders = result
88-
89-
var DB = require('../lib/db')(
90-
config,
91-
log,
92-
error,
93-
Token.SessionToken,
94-
Token.KeyFetchToken,
95-
Token.AccountResetToken,
96-
Token.PasswordForgotToken,
97-
Token.PasswordChangeToken,
98-
UnblockCode
99-
)
100-
101-
DB.connect(config[config.db.backend])
102-
.then(
103-
function (db) {
104-
database = db
105-
customs = new Customs(config.customsUrl)
106-
var routes = require('../lib/routes')(
107-
log,
108-
error,
109-
serverPublicKeys,
110-
signer,
111-
db,
112-
senders.email,
113-
senders.sms,
114-
Password,
115-
config,
116-
customs
117-
)
118-
server = Server.create(log, error, config, routes, db, translator)
119-
120-
server.start(
121-
function (err) {
122-
if (err) {
123-
log.error({ op: 'server.start.1', msg: 'failed startup with error',
124-
err: { message: err.message } })
125-
process.exit(1)
126-
} else {
127-
log.info({ op: 'server.start.1', msg: 'running on ' + server.info.uri })
128-
}
129-
}
130-
)
131-
statsInterval = setInterval(logStatInfo, 15000)
132-
},
133-
function (err) {
134-
log.error({ op: 'DB.connect', err: { message: err.message } })
135-
process.exit(1)
136-
}
137-
)
80+
var DB = require('../lib/db')(
81+
config,
82+
log,
83+
error,
84+
Token.SessionToken,
85+
Token.KeyFetchToken,
86+
Token.AccountResetToken,
87+
Token.PasswordForgotToken,
88+
Token.PasswordChangeToken,
89+
UnblockCode
90+
)
13891

92+
P.all([
93+
DB.connect(config[config.db.backend]),
94+
require('../lib/senders/translator')(config.i18n.supportedLanguages, config.i18n.defaultLanguage)
95+
])
96+
.spread(
97+
(db, translator) => {
98+
database = db
99+
100+
require('../lib/senders')(log, config, error, db, translator)
101+
.then(result => {
102+
senders = result
103+
customs = new Customs(config.customsUrl)
104+
var routes = require('../lib/routes')(
105+
log,
106+
error,
107+
serverPublicKeys,
108+
signer,
109+
db,
110+
senders.email,
111+
senders.sms,
112+
Password,
113+
config,
114+
customs
115+
)
116+
server = Server.create(log, error, config, routes, db, translator)
117+
118+
server.start(
119+
function (err) {
120+
if (err) {
121+
log.error({ op: 'server.start.1', msg: 'failed startup with error',
122+
err: { message: err.message } })
123+
process.exit(1)
124+
} else {
125+
log.info({ op: 'server.start.1', msg: 'running on ' + server.info.uri })
126+
}
127+
}
128+
)
129+
statsInterval = setInterval(logStatInfo, 15000)
130+
})
131+
},
132+
function (err) {
133+
log.error({ op: 'DB.connect', err: { message: err.message } })
134+
process.exit(1)
139135
}
140136
)
141137

config/index.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,52 @@ var conf = convict({
251251
format: String,
252252
default: undefined,
253253
env: 'SES_CONFIGURATION_SET'
254+
},
255+
bounces: {
256+
enabled: {
257+
doc: 'Flag to enable checking for bounces before sending email',
258+
default: true,
259+
env: 'BOUNCES_ENABLED'
260+
},
261+
complaint: {
262+
duration: {
263+
doc: 'Time until a complaint is no longer counted',
264+
default: '1 year',
265+
format: 'duration',
266+
env: 'BOUNCES_COMPLAINT_DURATION'
267+
},
268+
max: {
269+
doc: 'Maximum number of complaints before blocking emails',
270+
default: 0,
271+
env: 'BOUNCES_COMPLAINT_MAX'
272+
}
273+
},
274+
hard: {
275+
duration: {
276+
doc: 'Time until a hard bounce is no longer counted',
277+
default: '1 year',
278+
format: 'duration',
279+
env: 'BOUNCES_HARD_DURATION'
280+
},
281+
max: {
282+
doc: 'Maximum number of hard bounces before blocking emails',
283+
default: 0,
284+
env: 'BOUNCES_HARD_MAX'
285+
}
286+
},
287+
soft: {
288+
duration: {
289+
doc: 'Time until a soft bounce is no longer counted',
290+
default: '5 mins',
291+
format: 'duration',
292+
env: 'BOUNCES_SOFT_DURATION'
293+
},
294+
max: {
295+
doc: 'Maximum number of soft bounces before blocking emails',
296+
default: 0,
297+
env: 'BOUNCES_SOFT_MAX'
298+
}
299+
}
254300
}
255301
},
256302
maxEventLoopDelay: {

docs/api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ The currently-defined error responses are:
9292
* status code 400, errno 130: invalid region
9393
* status code 400, errno 131: invalid message id
9494
* status code 400, errno 132: message rejected
95+
* status code 400, errno 133: email sent complaint
96+
* status code 400, errno 134: email hard bounced
97+
* status code 400, errno 135: email soft bounced
9598
* status code 503, errno 201: service temporarily unavailable to due high load (see [backoff protocol](#backoff-protocol))
9699
* status code 503, errno 202: feature has been disabled for operational reasons
97100
* any status code, errno 999: unknown error

lib/db.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,15 @@ module.exports = function (
847847
return this.pool.post('/emailBounces', bounceData)
848848
}
849849

850+
DB.prototype.emailBounces = function (email) {
851+
log.trace({
852+
op: 'DB.emailBounces',
853+
email: email
854+
})
855+
856+
return this.pool.get('/emailBounces/' + Buffer(email, 'utf8').toString('hex'))
857+
}
858+
850859
function wrapTokenNotFoundError (err) {
851860
if (isNotFoundError(err)) {
852861
err = error.invalidToken('The authentication token could not be found')

lib/error.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ var ERRNO = {
3636
INVALID_REGION: 130,
3737
INVALID_MESSAGE_ID: 131,
3838
MESSAGE_REJECTED: 132,
39+
BOUNCE_COMPLAINT: 133,
40+
BOUNCE_HARD: 134,
41+
BOUNCE_SOFT: 135,
3942
SERVER_BUSY: 201,
4043
FEATURE_NOT_ENABLED: 202,
4144
UNEXPECTED_ERROR: 999
@@ -509,6 +512,33 @@ AppError.invalidMessageId = () => {
509512
})
510513
}
511514

515+
AppError.emailComplaint = () => {
516+
return new AppError({
517+
code: 400,
518+
error: 'Bad Request',
519+
errno: ERRNO.BOUNCE_COMPLAINT,
520+
message: 'Email account sent complaint'
521+
})
522+
}
523+
524+
AppError.emailBouncedHard = () => {
525+
return new AppError({
526+
code: 400,
527+
error: 'Bad Request',
528+
errno: ERRNO.BOUNCE_HARD,
529+
message: 'Email account hard bounced'
530+
})
531+
}
532+
533+
AppError.emailBouncedSoft = () => {
534+
return new AppError({
535+
code: 400,
536+
error: 'Bad Request',
537+
errno: ERRNO.BOUNCE_SOFT,
538+
message: 'Email account soft bounced'
539+
})
540+
}
541+
512542
AppError.messageRejected = (reason, reasonCode) => {
513543
return new AppError({
514544
code: 500,

lib/routes/account.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,14 @@ module.exports = function (
926926
uaDeviceType: sessionToken.uaDeviceType
927927
}
928928
)
929+
.catch(e => {
930+
// If we couldn't email them, no big deal. Log
931+
// and pretend everything worked.
932+
log.trace({
933+
op: 'Account.login.sendNewDeviceLoginNotification.error',
934+
error: e
935+
})
936+
})
929937
}
930938
)
931939
}

lib/routes/password.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,14 @@ module.exports = function (
265265
timeZone: geoData.timeZone
266266
}, request.headers['user-agent'], log)
267267
)
268+
.catch(e => {
269+
// If we couldn't email them, no big deal. Log
270+
// and pretend everything worked.
271+
log.trace({
272+
op: 'Password.changeFinish.sendPasswordChangedNotification.error',
273+
error: e
274+
})
275+
})
268276
}
269277
)
270278
}

0 commit comments

Comments
 (0)