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

mozilla / blurts-server / #13117

pending completion
#13117

push

circleci

Vinnl
Pretend HTMLElement.shadowRoot is always set

We generally only tend to access it after having called
this.attachShadow({ mode: 'open' }), and the error message about it
potentially being undefined at every location we access
this.shadowRoot is more noisy than helpful.

See also
https://github.com/mozilla/blurts-server/pull/2959#discussion_r1154023113
and
https://github.com/mozilla/blurts-server/pull/2959#discussion_r1154960095

282 of 1629 branches covered (17.31%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

959 of 4374 relevant lines covered (21.93%)

1.83 hits per line

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

0.0
/src/utils/email.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 { createTransport } from 'nodemailer'
6
import { URL } from 'url'
7

8
import mozlog from './log.js'
9
import AppConstants from '../app-constants.js'
10
import { MethodNotAllowedError } from '../utils/error.js'
11
import { getMessage, fluentError } from '../utils/fluent.js'
12
import { updateMonthlyEmailOptout } from '../db/tables/subscribers.js'
13

14
const log = mozlog('email-utils')
×
15

16
const { SERVER_URL } = AppConstants
×
17

18
// The SMTP transport object. This is initialized to a nodemailer transport
19
// object while reading SMTP credentials, or to a dummy function in debug mode.
20
let gTransporter
21

22
const EmailTemplateType = {
×
23
  Notification: 'notification',
24
  Verification: 'verification',
25
  Monthly: 'monthly',
26
  SignupReport: 'signup-report'
27
}
28

29
async function initEmail (smtpUrl = AppConstants.SMTP_URL) {
×
30
  // Allow a debug mode that will log JSON instead of sending emails.
31
  if (!smtpUrl) {
×
32
    log.info('smtpUrl-empty', { message: 'EmailUtils will log a JSON response instead of sending emails.' })
×
33
    gTransporter = createTransport({ jsonTransport: true })
×
34
    return true
×
35
  }
36

37
  gTransporter = createTransport(smtpUrl)
×
38
  const gTransporterVerification = await gTransporter.verify()
×
39
  return gTransporterVerification
×
40
}
41

42
/**
43
 * Send Email
44
 *
45
 * @param {string} recipient
46
 * @param {string} subject
47
 * @param {string} html
48
 * @returns {Promise} <Promise>
49
 */
50
function sendEmail (recipient, subject, html) {
51
  if (!gTransporter) {
×
52
    return Promise.reject(new Error('SMTP transport not initialized'))
×
53
  }
54

55
  return new Promise((resolve, reject) => {
×
56
    const emailFrom = AppConstants.EMAIL_FROM
×
57
    const mailOptions = {
×
58
      from: emailFrom,
59
      to: recipient,
60
      subject,
61
      html,
62
      headers: {
63
        'x-ses-configuration-set': AppConstants.SES_CONFIG_SET
64
      }
65
    }
66

67
    gTransporter.sendMail(mailOptions, (error, info) => {
×
68
      if (error) {
×
69
        reject(new Error(error))
×
70
        return
×
71
      }
72
      if (gTransporter.transporter.name === 'JSONTransport') {
×
73
        log.info('JSONTransport', { message: info.message.toString() })
×
74
      }
75
      resolve(info)
×
76
    })
77
  })
78
}
79

80
function appendUrlParams (url, urlParams) {
81
  const defaultUtmParams = {
×
82
    utm_source: 'fx-monitor',
83
    utm_medium: 'email'
84
  }
85

86
  const allUtmParams = { ...defaultUtmParams, ...urlParams }
×
87

88
  for (const param in allUtmParams) {
×
89
    const paramValue = allUtmParams[param]
×
90

91
    if (typeof paramValue !== 'undefined' && paramValue) {
×
92
      url.searchParams.set(param, encodeURIComponent(paramValue))
×
93
    }
94
  }
95

96
  return url
×
97
}
98

99
function getEmailCtaHref (emailType, content, subscriberId = null) {
×
100
  const dashboardUrl = `${SERVER_URL}/user/breaches`
×
101
  const url = new URL(dashboardUrl)
×
102
  const utmParams = {
×
103
    subscriber_id: subscriberId,
104
    utm_campaign: emailType,
105
    utm_content: content
106
  }
107

108
  return appendUrlParams(url, utmParams)
×
109
}
110

111
function getVerificationUrl (subscriber) {
112
  if (!subscriber.verification_token) {
×
113
    throw new Error('subscriber has no verification_token')
×
114
  }
115

116
  const url = new URL(`${SERVER_URL}/api/v1/user/verify-email`)
×
117
  const verificationUtmParams = {
×
118
    token: subscriber.verification_token,
119
    utm_campaign: 'verified-subscribers',
120
    utm_content: 'account-verification-email'
121
  }
122

123
  return appendUrlParams(url, verificationUtmParams)
×
124
}
125

126
function getUnsubscribeCtaHref ({ subscriber, isMonthlyEmail }) {
127
  const path = isMonthlyEmail
×
128
    ? `${SERVER_URL}/user/unsubscribe-monthly`
129
    : `${SERVER_URL}/user/unsubscribe`
130

131
  if (!subscriber) {
×
132
    return path
×
133
  }
134

135
  if (isMonthlyEmail && !subscriber.primary_verification_token) {
×
136
    throw new Error('subscriber has no primary verification_token')
×
137
  }
138

139
  const url = new URL(path)
×
140
  const token = Object.hasOwn(subscriber, 'verification_token')
×
141
    ? subscriber.verification_token
142
    : subscriber.primary_verification_token
143
  const hash = Object.hasOwn(subscriber, 'sha1')
×
144
    ? subscriber.sha1
145
    : subscriber.primary_sha1
146

147
  // Mandatory params for unsubscribing a user
148
  const unsubscribeUtmParams = {
×
149
    hash,
150
    token,
151
    utm_campaign: isMonthlyEmail
×
152
      ? 'monthly-unresolved'
153
      : 'unsubscribe',
154
    utm_content: 'unsubscribe-cta'
155
  }
156

157
  return appendUrlParams(url, unsubscribeUtmParams)
×
158
}
159

160
/**
161
 * Check if params contain all mandatory params.
162
 *
163
 * @param {object} params Params that we like to have checked
164
 * @param {string} mandatoryParams A comma separated list of mandatory params
165
 * @returns {boolean} True if all mandatory params are present in params
166
 */
167
function hasMandatoryParams (params, mandatoryParams) {
168
  return mandatoryParams.split(',').every(paramKey => {
×
169
    const paramKeyParsed = paramKey.trim()
×
170
    return (
×
171
      Object.hasOwn(params, paramKeyParsed) &&
×
172
      params[paramKeyParsed] !== ''
173
    )
174
  })
175
}
176

177
/**
178
 * TODO: Unsubscribing from emails is currently only implemented for the
179
 * monthly unresolved breach report.
180
 *
181
 * Unsubscribes a user from receiving emails other than the monthly reports.
182
 *
183
 * @param {object} req Request should contain a `token` and `hash` query param.
184
 */
185
async function unsubscribeFromEmails (req) {
186
  const urlQuery = req.query
×
187

188
  // For unsubscribing from emails we need a hash and token
189
  if (!hasMandatoryParams(urlQuery, 'hash,token')) {
×
190
    throw fluentError('user-unsubscribe-token-email-error')
×
191
  }
192

193
  throw new MethodNotAllowedError()
×
194
}
195

196
/**
197
 * Unsubscribe the user from receiving the monthly unresolved breach reports.
198
 *
199
 * @param {object} req Request that should contain a `token` query param
200
 */
201
async function unsubscribeFromMonthlyReport (req) {
202
  const urlQuery = req.query
×
203

204
  // For unsubscribing from the monthly emails we need a token
205
  if (!hasMandatoryParams(urlQuery, 'token')) {
×
206
    throw fluentError('user-unsubscribe-token-error')
×
207
  }
208

209
  // Unsubscribe user from the monthly unresolved breach emails
210
  await updateMonthlyEmailOptout(urlQuery.token)
×
211
}
212

213
const breachDummyLogo = new Map([
×
214
  ['adobe.com', '/images/logo_cache/adobe.com.ico']
215
])
216

217
/**
218
 * Dummy data for populating the breach notification email preview.
219
 *
220
 * @param {string} recipient
221
 * @returns {object} Breach dummy data
222
 */
223
const getNotificationDummyData = (recipient) => ({
×
224
  breachData: {
225
    Id: 1,
226
    Name: 'Adobe',
227
    Title: 'Adobe',
228
    Domain: 'adobe.com',
229
    BreachDate: '2013-01-01T22:00:00.000Z',
230
    AddedDate: '2013-01-02T00:00:00.000Z',
231
    ModifiedDate: '2023-01-01T00:00:00.000Z',
232
    PwnCount: 123,
233
    Description: 'Example description',
234
    DataClasses: [
235
      'email-addresses',
236
      'password-hints',
237
      'passwords',
238
      'usernames'
239
    ],
240
    IsVerified: true,
241
    IsFabricated: false,
242
    IsSensitive: false,
243
    IsRetired: false,
244
    IsSpamList: false,
245
    IsMalware: false
246
  },
247
  breachedEmail: recipient,
248
  breachLogos: breachDummyLogo,
249
  ctaHref: getEmailCtaHref('email-test-notification', 'dashboard-cta'),
250
  heading: getMessage('email-spotted-new-breach'),
251
  recipientEmail: recipient,
252
  subscriberId: 123,
253
  supportedLocales: ['en']
254
})
255

256
/**
257
 * Dummy data for populating the email verification preview
258
 *
259
 * @param {string} recipient
260
 * @returns {object} Email verification dummy data
261
 */
262
const getVerificationDummyData = (recipient) => ({
×
263
  ctaHref: SERVER_URL,
264
  heading: getMessage('email-verify-heading'),
265
  recipientEmail: recipient,
266
  subheading: getMessage('email-verify-subhead')
267
})
268

269
/**
270
 * Dummy data for populating the monthly unresolved breaches email
271
 *
272
 * @param {string} recipient
273
 * @returns {object} Monthly unresolved breaches dummy data
274
 */
275
const getMonthlyDummyData = (recipient) => ({
×
276
  breachedEmail: 'breached@email.com',
277
  breachLogos: breachDummyLogo,
278
  ctaHref: `${SERVER_URL}/user/breaches`,
279
  heading: getMessage('email-unresolved-heading'),
280
  monitoredEmails: {
281
    count: 2
282
  },
283
  numBreaches: {
284
    count: 3,
285
    numResolved: 2,
286
    numUnresolved: 1
287
  },
288
  recipientEmail: recipient,
289
  subheading: getMessage('email-unresolved-subhead'),
290
  unsubscribeUrl: `${SERVER_URL}/user/unsubscribe-monthly?token=token_123`
291
})
292

293
/**
294
 * Dummy data for populating the signup report email
295
 *
296
 * @param {string} recipient
297
 * @returns {object} Signup report email dummy data
298
 */
299

300
const getSignupReportDummyData = (recipient) => {
×
301
  const unsafeBreachesForEmail = [
×
302
    getNotificationDummyData(recipient).breachData
303
  ]
304
  const breachesCount = unsafeBreachesForEmail.length
×
305
  const numPasswordsExposed = 1
×
306

307
  const emailBreachStats = [
×
308
    {
309
      statNumber: breachesCount,
310
      statTitle: getMessage('known-data-breaches-exposed', {
311
        breaches: breachesCount
312
      })
313
    },
314
    {
315
      statNumber: numPasswordsExposed,
316
      statTitle: getMessage('passwords-exposed', {
317
        passwords: numPasswordsExposed
318
      })
319
    }
320
  ]
321

322
  return {
×
323
    breachedEmail: recipient,
324
    breachLogos: breachDummyLogo,
325
    ctaHref: getEmailCtaHref('email-test-notification', 'dashboard-cta'),
326
    heading: unsafeBreachesForEmail.length
×
327
      ? getMessage('email-subject-found-breaches')
328
      : getMessage('email-subject-no-breaches'),
329
    emailBreachStats,
330
    recipientEmail: recipient,
331
    subscriberId: 123,
332
    unsafeBreachesForEmail
333
  }
334
}
335

336
export {
337
  EmailTemplateType,
338
  getEmailCtaHref,
339
  getMonthlyDummyData,
340
  getNotificationDummyData,
341
  getSignupReportDummyData,
342
  getUnsubscribeCtaHref,
343
  getVerificationDummyData,
344
  getVerificationUrl,
345
  initEmail,
346
  unsubscribeFromEmails,
347
  unsubscribeFromMonthlyReport,
348
  sendEmail
349
}
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