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

Commit 4fe29fa

Browse files
committed
feat(db): add emailBounces table
Add a table to record all hard and soft bounces, and complaints, to a given email address. Also add `POST /emailBounces` and `GET /emailBounces/:email` to query for them.
1 parent 4b6a92d commit 4fe29fa

File tree

10 files changed

+227
-11
lines changed

10 files changed

+227
-11
lines changed

docs/API.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ There are a number of methods that a DB storage backend should implement:
4545
* .createVerificationReminder(body)
4646
* .fetchReminders(body, query)
4747
* .deleteReminder(body)
48+
* Email Bounces
4849
* General
4950
* .ping()
5051
* .close()
@@ -493,4 +494,16 @@ Parameters:
493494
* uid : user id
494495
* type : type of reminder
495496

497+
## .createEmailBounce(body) ##
498+
499+
Record when an email bounce has occurred.
500+
501+
Parameters:
502+
503+
* body: (object)
504+
* email: A string of the email address that bounced
505+
* bounceType: The bounce type ([`'Permanent'`, `'Transient'`, `'Complaint'`])
506+
* bounceSubType: The bounce sub type string
507+
496508
(Ends)
509+

fxa-auth-db-server/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ function createServer(db) {
107107
api.get('/securityEvents/:id/ip/:ipAddr', withParams(db.securityEvents))
108108
api.post('/securityEvents', withBodyAndQuery(db.createSecurityEvent))
109109

110+
api.get('/emailBounces/:email', op(req => db.fetchEmailBounces(req.params.email)))
111+
api.post('/emailBounces', withBodyAndQuery(db.createEmailBounce))
112+
110113
api.get('/emailRecord/:id', withIdAndBody(db.emailRecord))
111114
api.head('/emailRecord/:id', withIdAndBody(db.accountExists))
112115

fxa-auth-db-server/test/backend/db_tests.js

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ var crypto = require('crypto')
66
var base64url = require('base64url')
77
var P = require('bluebird')
88

9-
var zeroBuffer16 = Buffer('00000000000000000000000000000000', 'hex')
10-
var zeroBuffer32 = Buffer('0000000000000000000000000000000000000000000000000000000000000000', 'hex')
9+
var zeroBuffer16 = Buffer.from('00000000000000000000000000000000', 'hex')
10+
var zeroBuffer32 = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex')
1111
var now = Date.now()
1212

1313
function newUuid() {
@@ -1898,6 +1898,36 @@ module.exports = function(config, DB) {
18981898
}
18991899
)
19001900

1901+
test(
1902+
'emailBounces',
1903+
t => {
1904+
t.plan(4)
1905+
const data = {
1906+
email: ('' + Math.random()).substr(2) + '@email.bounces',
1907+
bounceType: 'Permanent',
1908+
bounceSubType: 'NoEmail'
1909+
}
1910+
db.createEmailBounce(data)
1911+
.then(() => {
1912+
return db.fetchEmailBounces(data.email)
1913+
})
1914+
.then(bounces => {
1915+
t.equal(bounces.length, 1)
1916+
t.equal(bounces[0].email, data.email)
1917+
t.equal(bounces[0].bounceType, 1)
1918+
t.equal(bounces[0].bounceSubType, 3)
1919+
1920+
})
1921+
.done(
1922+
() => {
1923+
t.end()
1924+
},
1925+
(err) => {
1926+
t.fail(err)
1927+
}
1928+
)
1929+
}
1930+
)
19011931

19021932
test(
19031933
'teardown',

fxa-auth-db-server/test/backend/remote.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,33 @@ module.exports = function(cfg, server) {
10901090
}
10911091
)
10921092

1093+
test(
1094+
'email bounces',
1095+
function (t) {
1096+
t.plan(8)
1097+
var email = Math.random() + '@email.bounces'
1098+
return client.postThen('/emailBounces', {
1099+
email: email,
1100+
bounceType: 'Permanent',
1101+
bounceSubType: 'NoEmail'
1102+
})
1103+
.then(
1104+
function (r) {
1105+
respOkEmpty(t, r)
1106+
return client.getThen('/emailBounces/' + email)
1107+
}
1108+
)
1109+
.then(
1110+
function (r) {
1111+
respOk(t, r)
1112+
t.equal(r.obj.length, 1)
1113+
t.equal(r.obj[0].email, email)
1114+
t.ok(r.obj[0].createdAt <= Date.now(), 'returns { createdAt: Number }')
1115+
}
1116+
)
1117+
}
1118+
)
1119+
10931120
test(
10941121
'GET an unknown path',
10951122
function (t) {

lib/db/mem.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
var P = require('bluebird')
6-
var extend = require('util')._extend
7-
var ip = require('ip')
5+
'use strict'
6+
7+
const P = require('bluebird')
8+
const extend = require('util')._extend
9+
const ip = require('ip')
10+
const dbUtil = require('./util')
811

912
// our data stores
1013
var accounts = {}
@@ -18,6 +21,7 @@ var passwordForgotTokens = {}
1821
var reminders = {}
1922
var securityEvents = {}
2023
var unblockCodes = {}
24+
var emailBounces = {}
2125

2226
var DEVICE_FIELDS = [
2327
'sessionTokenId',
@@ -891,6 +895,21 @@ module.exports = function (log, error) {
891895
return P.resolve({ createdAt: timestamp })
892896
}
893897

898+
899+
Memory.prototype.createEmailBounce = function (data) {
900+
let row = emailBounces[data.email] || (emailBounces[data.email] = [])
901+
let bounce = extend({}, data)
902+
bounce.createdAt = Date.now()
903+
bounce.bounceType = dbUtil.mapEmailBounceType(bounce.bounceType)
904+
bounce.bounceSubType = dbUtil.mapEmailBounceSubType(bounce.bounceSubType)
905+
row.push(bounce)
906+
return P.resolve({})
907+
}
908+
909+
Memory.prototype.fetchEmailBounces = function(email) {
910+
return P.resolve(emailBounces[email] || [])
911+
}
912+
894913
// UTILITY FUNCTIONS
895914

896915
Memory.prototype.ping = function () {

lib/db/mysql.js

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
var crypto = require('crypto')
6-
var ip = require('ip')
7-
var mysql = require('mysql')
8-
var P = require('../promise')
5+
'use strict'
96

10-
var patch = require('./patch')
7+
const crypto = require('crypto')
8+
const ip = require('ip')
9+
const mysql = require('mysql')
10+
const P = require('../promise')
11+
12+
const patch = require('./patch')
13+
const dbUtil = require('./util')
1114

1215
// http://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html
1316
const ER_TOO_MANY_CONNECTIONS = 1040
@@ -1072,5 +1075,22 @@ module.exports = function (log, error) {
10721075
)
10731076
}
10741077

1078+
const CREATE_EMAIL_BOUNCE = 'CALL createEmailBounce_1(?, ?, ?, ?)'
1079+
MySql.prototype.createEmailBounce = function (data) {
1080+
const args = [
1081+
data.email,
1082+
dbUtil.mapEmailBounceType(data.bounceType),
1083+
dbUtil.mapEmailBounceSubType(data.bounceSubType),
1084+
Date.now()
1085+
]
1086+
return this.write(CREATE_EMAIL_BOUNCE, args)
1087+
}
1088+
1089+
const FETCH_EMAIL_BOUNCES = 'CALL fetchEmailBounces_1(?)'
1090+
MySql.prototype.fetchEmailBounces = function (email) {
1091+
return this.read(FETCH_EMAIL_BOUNCES, [email])
1092+
.then(result => result[0])
1093+
}
1094+
10751095
return MySql
10761096
}

lib/db/patch.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
// The expected patch level of the database. Update if you add a new
66
// patch in the ./schema/ directory.
7-
module.exports.level = 40
7+
module.exports.level = 41

lib/db/schema/patch-040-041.sql

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
CREATE TABLE IF NOT EXISTS emailBounces (
2+
email VARCHAR(255) NOT NULL,
3+
bounceType TINYINT UNSIGNED NOT NULL,
4+
bounceSubType TINYINT UNSIGNED NOT NULL,
5+
createdAt BIGINT UNSIGNED NOT NULL,
6+
PRIMARY KEY(email, createdAt)
7+
) ENGINE=InnoDB;
8+
9+
CREATE PROCEDURE `createEmailBounce_1` (
10+
IN inEmail VARCHAR(255),
11+
IN inBounceType TINYINT UNSIGNED,
12+
IN inBounceSubType TINYINT UNSIGNED,
13+
IN inCreatedAt BIGINT UNSIGNED
14+
)
15+
BEGIN
16+
INSERT INTO emailBounces(
17+
email,
18+
bounceType,
19+
bounceSubType,
20+
createdAt
21+
)
22+
VALUES(
23+
inEmail,
24+
inBounceType,
25+
inBounceSubType,
26+
inCreatedAt
27+
);
28+
END;
29+
30+
CREATE PROCEDURE `fetchEmailBounces_1` (
31+
IN `inEmail` VARCHAR(255)
32+
)
33+
BEGIN
34+
SELECT
35+
email,
36+
bounceType,
37+
bounceSubType,
38+
createdAt
39+
FROM emailBounces
40+
WHERE email = inEmail
41+
ORDER BY createdAt DESC
42+
LIMIT 20;
43+
END;
44+
45+
UPDATE dbMetadata SET value = '41' WHERE name = 'schema-patch-level';

lib/db/schema/patch-041-040.sql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- Rollback; commented out to prevent accidental running
2+
-- in production.
3+
4+
-- DROP TABLE `emailBounces`;
5+
-- DROP PROCEDURE `createEmailBounce_1`;
6+
-- DROP PROCEDURE `fetchEmailBounces_1`;
7+
8+
-- UPDATE dbMetadata SET value = '40' WHERE name = 'schema-patch-level';

lib/db/util.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
'use strict'
6+
7+
8+
const BOUNCE_TYPES = new Map([
9+
['__fxa__unmapped', 0], // a bounce type we don't yet recognize
10+
['Permanent', 1], // Hard
11+
['Transient', 2], // Soft
12+
['Complaint', 3] // Complaint
13+
])
14+
15+
const BOUNCE_SUB_TYPES = new Map([
16+
['__fxa__unmapped', 0], // a bounce type we don't yet recognize
17+
['Undetermined', 1],
18+
['General', 2],
19+
['NoEmail', 3],
20+
['Suppressed', 4],
21+
['MailboxFull', 5],
22+
['MessageTooLarge', 6],
23+
['ContentRejected', 7],
24+
['AttachmentRejected', 8],
25+
['abuse', 9],
26+
['auth-failure', 10],
27+
['fraud', 11],
28+
['not-spam', 12],
29+
['other', 13],
30+
['virus', 14]
31+
])
32+
33+
module.exports = {
34+
35+
mapEmailBounceType(val) {
36+
if (typeof val === 'number') {
37+
return val
38+
} else {
39+
return BOUNCE_TYPES.get(val) || 0
40+
}
41+
},
42+
43+
mapEmailBounceSubType(val) {
44+
if (typeof val === 'number') {
45+
return val
46+
} else {
47+
return BOUNCE_SUB_TYPES.get(val) || 0
48+
}
49+
}
50+
51+
}

0 commit comments

Comments
 (0)