• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / blurts-server / 45ba77d7-fd3c-4064-90eb-157d6aa10611

pending completion
45ba77d7-fd3c-4064-90eb-157d6aa10611

push

circleci

Robert Helmer
update the queue adr

282 of 1631 branches covered (17.29%)

Branch coverage included in aggregate %.

959 of 4375 relevant lines covered (21.92%)

3.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/db/tables/email_addresses.js
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
import { v4 as uuidv4 } from 'uuid'
6
import Knex from 'knex'
7
import knexConfig from '../knexfile.js'
8
import mozlog from '../../utils/log.js'
9
import { fluentError } from '../../utils/fluent.js'
10
import { subscribeHash } from '../../utils/hibp.js'
11
import { getSha1 } from '../../utils/fxa.js'
12
import { getSubscriberByEmail, updateFxAData } from './subscribers.js'
13
const knex = Knex(knexConfig)
×
14
const log = mozlog('DB.email_addresses')
×
15

16
async function getEmailByToken (token) {
17
  const res = await knex('email_addresses')
×
18
    .where('verification_token', '=', token)
19

20
  return res[0]
×
21
}
22

23
async function getEmailById (emailAddressId) {
24
  const res = await knex('email_addresses')
×
25
    .where('id', '=', emailAddressId)
26

27
  return res[0]
×
28
}
29

30
async function getEmailAddressRecordByEmail (email) {
31
  const emailAddresses = await knex('email_addresses').where({
×
32
    email, verified: true
33
  })
34
  if (!emailAddresses) {
×
35
    return null
×
36
  }
37
  if (emailAddresses.length > 1) {
×
38
    // TODO: handle multiple emails in separate(?) subscriber accounts?
39
    log.warn('getEmailAddressRecordByEmail', { msg: 'found the same email multiple times' })
×
40
  }
41
  return emailAddresses[0]
×
42
}
43

44
async function addSubscriberUnverifiedEmailHash (user, email) {
45
  const res = await knex('email_addresses').insert({
×
46
    subscriber_id: user.id,
47
    email,
48
    sha1: getSha1(email),
49
    verification_token: uuidv4(),
50
    verified: false
51
  }).returning('*')
52
  return res[0]
×
53
}
54

55
async function resetUnverifiedEmailAddress (emailAddressId) {
56
  const newVerificationToken = uuidv4()
×
57

58
  // Time in ms to require between verification reset.
59
  const verificationWait = 5 * 60 * 1000 // 5 minutes
×
60

61
  const verificationRecentlyUpdated = await knex('email_addresses')
×
62
    .select('id')
63
    .whereRaw('"updated_at" > NOW() - INTERVAL \'1 MILLISECOND\' * ?', verificationWait)
64
    .andWhere('id', emailAddressId)
65
    .first()
66

67
  if (verificationRecentlyUpdated?.id === parseInt(emailAddressId)) {
×
68
    throw fluentError('error-email-validation-pending')
×
69
  }
70

71
  const res = await knex('email_addresses')
×
72
    .update({
73
      verification_token: newVerificationToken,
74
      updated_at: knex.fn.now()
75
    })
76
    .where('id', emailAddressId)
77
    .returning('*')
78
  return res[0]
×
79
}
80

81
async function verifyEmailHash (token) {
82
  const unverifiedEmail = await getEmailByToken(token)
×
83
  if (!unverifiedEmail) {
×
84
    throw fluentError('Error message for this verification email timed out or something went wrong.')
×
85
  }
86
  const verifiedEmail = await _verifyNewEmail(unverifiedEmail)
×
87
  return verifiedEmail[0]
×
88
}
89

90
// TODO: refactor into an upsert? https://jaketrent.com/post/upsert-knexjs/
91
// Used internally, ideally should not be called by consumers.
92
async function _getSha1EntryAndDo (sha1, aFoundCallback, aNotFoundCallback) {
93
  const existingEntries = await knex('subscribers')
×
94
    .where('primary_sha1', sha1)
95

96
  if (existingEntries.length && aFoundCallback) {
×
97
    return await aFoundCallback(existingEntries[0])
×
98
  }
99

100
  if (!existingEntries.length && aNotFoundCallback) {
×
101
    return await aNotFoundCallback()
×
102
  }
103
}
104

105
// Used internally.
106
async function _addEmailHash (sha1, email, signupLanguage, verified = false) {
×
107
  log.debug('_addEmailHash', { sha1, email, signupLanguage, verified })
×
108
  try {
×
109
    return await _getSha1EntryAndDo(sha1, async aEntry => {
×
110
      // Entry existed, patch the email value if supplied.
111
      if (email) {
×
112
        const res = await knex('subscribers')
×
113
          .update({
114
            primary_email: email,
115
            primary_sha1: getSha1(email.toLowerCase()),
116
            primary_verified: verified,
117
            updated_at: knex.fn.now()
118
          })
119
          .where('id', '=', aEntry.id)
120
          .returning('*')
121
        return res[0]
×
122
      }
123

124
      return aEntry
×
125
    }, async () => {
126
      // Always add a verification_token value
127
      const verificationToken = uuidv4()
×
128
      const res = await knex('subscribers')
×
129
        .insert({ primary_sha1: getSha1(email.toLowerCase()), primary_email: email, signup_language: signupLanguage, primary_verification_token: verificationToken, primary_verified: verified })
130
        .returning('*')
131
      return res[0]
×
132
    })
133
  } catch (e) {
134
    log.error(e)
×
135
    throw fluentError('error-could-not-add-email')
×
136
  }
137
}
138

139
/**
140
 * Add a subscriber:
141
 * 1. Add a record to subscribers
142
 * 2. Immediately call _verifySubscriber
143
 * 3. For FxA subscriber, add refresh token and profile data
144
 *
145
 * @param {string} email to add
146
 * @param {string} signupLanguage from Accept-Language
147
 * @param {string} fxaAccessToken from Firefox Account Oauth
148
 * @param {string} fxaRefreshToken from Firefox Account Oauth
149
 * @param {string} fxaProfileData from Firefox Account
150
 * @returns {object} subscriber knex object added to DB
151
 */
152
async function addSubscriber (email, signupLanguage, fxaAccessToken = null, fxaRefreshToken = null, fxaProfileData = null) {
×
153
  console.log({ email })
×
154
  console.log({ signupLanguage })
×
155
  const emailHash = await _addEmailHash(getSha1(email), email, signupLanguage, true)
×
156
  const verified = await _verifySubscriber(emailHash)
×
157
  const verifiedSubscriber = Array.isArray(verified) ? verified[0] : null
×
158
  if (fxaRefreshToken || fxaProfileData) {
×
159
    return updateFxAData(verifiedSubscriber, fxaAccessToken, fxaRefreshToken, fxaProfileData)
×
160
  }
161
  return verifiedSubscriber
×
162
}
163

164
/**
165
 * When an email is verified, convert it into a subscriber:
166
 * 1. Subscribe the hash to HIBP
167
 * 2. Update our subscribers record to verified
168
 * 3. (if opted in) Subscribe the email to Fx newsletter
169
 *
170
 * @param {object} emailHash knex object in DB
171
 * @returns {object} verified subscriber knex object in DB
172
 */
173
async function _verifySubscriber (emailHash) {
174
  await subscribeHash(emailHash.primary_sha1)
×
175

176
  const verifiedSubscriber = await knex('subscribers')
×
177
    .where('primary_email', '=', emailHash.primary_email)
178
    .update({
179
      primary_verified: true,
180
      updated_at: knex.fn.now()
181
    })
182
    .returning('*')
183

184
  return verifiedSubscriber
×
185
}
186

187
// Verifies new emails added by existing users
188
async function _verifyNewEmail (emailHash) {
189
  await subscribeHash(emailHash.sha1)
×
190

191
  const verifiedEmail = await knex('email_addresses')
×
192
    .where('id', '=', emailHash.id)
193
    .update({
194
      verified: true
195
    })
196
    .returning('*')
197

198
  return verifiedEmail
×
199
}
200

201
/**
202
 * @typedef {object} EmailRow Email data, as returned from the database table `email_addresses`
203
 * @property {string} email
204
 * @property {string} sha1
205
 * @property {boolean} verified
206
 */
207

208
/**
209
 * @param {number} userId
210
 * @returns {Promise<EmailRow[]>}
211
 */
212
async function getUserEmails (userId) {
213
  const userEmails = await knex('email_addresses')
×
214
    .where('subscriber_id', '=', userId)
215
    .returning('*')
216

217
  return userEmails
×
218
}
219

220
// This is used by SES callbacks to remove email addresses when recipients
221
// perma-bounce or mark our emails as spam
222
// Removes from either subscribers or email_addresses as necessary
223
async function removeEmail (email) {
224
  const subscriber = await getSubscriberByEmail(email)
×
225
  if (!subscriber) {
×
226
    const emailAddress = await getEmailAddressRecordByEmail(email)
×
227
    if (!emailAddress) {
×
228
      log.warn('removed-subscriber-not-found')
×
229
      return
×
230
    }
231
    await knex('email_addresses')
×
232
      .where({
233
        email,
234
        verified: true
235
      })
236
      .del()
237
    return
×
238
  }
239
  // This can fail if a subscriber has more email_addresses and marks
240
  // a primary email as spam, but we should let it fail so we can see it
241
  // in the logs
242
  await knex('subscribers')
×
243
    .where({
244
      primary_verification_token: subscriber.primary_verification_token,
245
      primary_sha1: subscriber.primary_sha1
246
    })
247
    .del()
248
}
249

250
async function removeOneSecondaryEmail (emailId) {
251
  await knex('email_addresses')
×
252
    .where({
253
      id: emailId
254
    })
255
    .del()
256
}
257

258
async function getEmailAddressesByHashes (hashes) {
259
  return await knex('email_addresses')
×
260
    .join('subscribers', 'email_addresses.subscriber_id', '=', 'subscribers.id')
261
    .whereIn('email_addresses.sha1', hashes)
262
    .andWhere('email_addresses.verified', '=', true)
263
}
264

265
async function deleteEmailAddressesByUid (uid) {
266
  await knex('email_addresses').where({ subscriber_id: uid }).del()
×
267
}
268

269
export {
270
  getEmailByToken,
271
  getEmailById,
272
  getEmailAddressRecordByEmail,
273
  addSubscriberUnverifiedEmailHash,
274
  resetUnverifiedEmailAddress,
275
  verifyEmailHash,
276
  addSubscriber,
277
  getUserEmails,
278
  removeEmail,
279
  removeOneSecondaryEmail,
280
  getEmailAddressesByHashes,
281
  deleteEmailAddressesByUid
282
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc