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

mozilla / blurts-server / f3f44638-1a2f-4ab0-962d-3f21eca73642

pending completion
f3f44638-1a2f-4ab0-962d-3f21eca73642

push

circleci

GitHub
MNTOR-1004 backend work for v2 settings (#2773)

282 of 1245 branches covered (22.65%)

Branch coverage included in aggregate %.

77 of 77 new or added lines in 4 files covered. (100.0%)

959 of 3372 relevant lines covered (28.44%)

4.62 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
  const res = await knex('email_addresses')
×
58
    .update({
59
      verification_token: newVerificationToken,
60
      updated_at: knex.fn.now()
61
    })
62
    .where('id', emailAddressId)
63
    .returning('*')
64
  return res[0]
×
65
}
66

67
async function verifyEmailHash (token) {
68
  const unverifiedEmail = await getEmailByToken(token)
×
69
  if (!unverifiedEmail) {
×
70
    throw fluentError('Error message for this verification email timed out or something went wrong.')
×
71
  }
72
  const verifiedEmail = await _verifyNewEmail(unverifiedEmail)
×
73
  return verifiedEmail[0]
×
74
}
75

76
// TODO: refactor into an upsert? https://jaketrent.com/post/upsert-knexjs/
77
// Used internally, ideally should not be called by consumers.
78
async function _getSha1EntryAndDo (sha1, aFoundCallback, aNotFoundCallback) {
79
  const existingEntries = await knex('subscribers')
×
80
    .where('primary_sha1', sha1)
81

82
  if (existingEntries.length && aFoundCallback) {
×
83
    return await aFoundCallback(existingEntries[0])
×
84
  }
85

86
  if (!existingEntries.length && aNotFoundCallback) {
×
87
    return await aNotFoundCallback()
×
88
  }
89
}
90

91
// Used internally.
92
async function _addEmailHash (sha1, email, signupLanguage, verified = false) {
×
93
  log.debug('_addEmailHash', { sha1, email, signupLanguage, verified })
×
94
  try {
×
95
    return await _getSha1EntryAndDo(sha1, async aEntry => {
×
96
      // Entry existed, patch the email value if supplied.
97
      if (email) {
×
98
        const res = await knex('subscribers')
×
99
          .update({
100
            primary_email: email,
101
            primary_sha1: getSha1(email.toLowerCase()),
102
            primary_verified: verified,
103
            updated_at: knex.fn.now()
104
          })
105
          .where('id', '=', aEntry.id)
106
          .returning('*')
107
        return res[0]
×
108
      }
109

110
      return aEntry
×
111
    }, async () => {
112
      // Always add a verification_token value
113
      const verificationToken = uuidv4()
×
114
      const res = await knex('subscribers')
×
115
        .insert({ primary_sha1: getSha1(email.toLowerCase()), primary_email: email, signup_language: signupLanguage, primary_verification_token: verificationToken, primary_verified: verified })
116
        .returning('*')
117
      return res[0]
×
118
    })
119
  } catch (e) {
120
    log.error(e)
×
121
    throw fluentError('error-could-not-add-email')
×
122
  }
123
}
124

125
/**
126
     * Add a subscriber:
127
     * 1. Add a record to subscribers
128
     * 2. Immediately call _verifySubscriber
129
     * 3. For FxA subscriber, add refresh token and profile data
130
     *
131
     * @param {string} email to add
132
     * @param {string} signupLanguage from Accept-Language
133
     * @param {string} fxaAccessToken from Firefox Account Oauth
134
     * @param {string} fxaRefreshToken from Firefox Account Oauth
135
     * @param {string} fxaProfileData from Firefox Account
136
     * @returns {object} subscriber knex object added to DB
137
     */
138
async function addSubscriber (email, signupLanguage, fxaAccessToken = null, fxaRefreshToken = null, fxaProfileData = null) {
×
139
  console.log({ email })
×
140
  console.log({ signupLanguage })
×
141
  const emailHash = await _addEmailHash(getSha1(email), email, signupLanguage, true)
×
142
  const verified = await _verifySubscriber(emailHash)
×
143
  const verifiedSubscriber = Array.isArray(verified) ? verified[0] : null
×
144
  if (fxaRefreshToken || fxaProfileData) {
×
145
    return updateFxAData(verifiedSubscriber, fxaAccessToken, fxaRefreshToken, fxaProfileData)
×
146
  }
147
  return verifiedSubscriber
×
148
}
149

150
/**
151
     * When an email is verified, convert it into a subscriber:
152
     * 1. Subscribe the hash to HIBP
153
     * 2. Update our subscribers record to verified
154
     * 3. (if opted in) Subscribe the email to Fx newsletter
155
     *
156
     * @param {object} emailHash knex object in DB
157
     * @returns {object} verified subscriber knex object in DB
158
     */
159
async function _verifySubscriber (emailHash) {
160
  await subscribeHash(emailHash.primary_sha1)
×
161

162
  const verifiedSubscriber = await knex('subscribers')
×
163
    .where('primary_email', '=', emailHash.primary_email)
164
    .update({
165
      primary_verified: true,
166
      updated_at: knex.fn.now()
167
    })
168
    .returning('*')
169

170
  return verifiedSubscriber
×
171
}
172

173
// Verifies new emails added by existing users
174
async function _verifyNewEmail (emailHash) {
175
  await subscribeHash(emailHash.sha1)
×
176

177
  const verifiedEmail = await knex('email_addresses')
×
178
    .where('id', '=', emailHash.id)
179
    .update({
180
      verified: true
181
    })
182
    .returning('*')
183

184
  return verifiedEmail
×
185
}
186

187
async function getUserEmails (userId) {
188
  const userEmails = await knex('email_addresses')
×
189
    .where('subscriber_id', '=', userId)
190
    .returning('*')
191

192
  return userEmails
×
193
}
194

195
// This is used by SES callbacks to remove email addresses when recipients
196
// perma-bounce or mark our emails as spam
197
// Removes from either subscribers or email_addresses as necessary
198
async function removeEmail (email) {
199
  const subscriber = await getSubscriberByEmail(email)
×
200
  if (!subscriber) {
×
201
    const emailAddress = await getEmailAddressRecordByEmail(email)
×
202
    if (!emailAddress) {
×
203
      log.warn('removed-subscriber-not-found')
×
204
      return
×
205
    }
206
    await knex('email_addresses')
×
207
      .where({
208
        email,
209
        verified: true
210
      })
211
      .del()
212
    return
×
213
  }
214
  // This can fail if a subscriber has more email_addresses and marks
215
  // a primary email as spam, but we should let it fail so we can see it
216
  // in the logs
217
  await knex('subscribers')
×
218
    .where({
219
      primary_verification_token: subscriber.primary_verification_token,
220
      primary_sha1: subscriber.primary_sha1
221
    })
222
    .del()
223
}
224

225
async function removeOneSecondaryEmail (emailId) {
226
  await knex('email_addresses')
×
227
    .where({
228
      id: emailId
229
    })
230
    .del()
231
}
232

233
async function getEmailAddressesByHashes (hashes) {
234
  return await knex('email_addresses')
×
235
    .join('subscribers', 'email_addresses.subscriber_id', '=', 'subscribers.id')
236
    .whereIn('email_addresses.sha1', hashes)
237
    .andWhere('email_addresses.verified', '=', true)
238
}
239

240
async function deleteEmailAddressesByUid (uid) {
241
  await knex('email_addresses').where({ subscriber_id: uid }).del()
×
242
}
243

244
export {
245
  getEmailByToken,
246
  getEmailById,
247
  getEmailAddressRecordByEmail,
248
  addSubscriberUnverifiedEmailHash,
249
  resetUnverifiedEmailAddress,
250
  verifyEmailHash,
251
  addSubscriber,
252
  getUserEmails,
253
  removeEmail,
254
  removeOneSecondaryEmail,
255
  getEmailAddressesByHashes,
256
  deleteEmailAddressesByUid
257
}
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