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

mozilla / blurts-server / c12b763b-3145-4de7-902f-e08dab211da6

pending completion
c12b763b-3145-4de7-902f-e08dab211da6

Pull #2889

circleci

Florian Zia
fix: Append utm params
Pull Request #2889: Opt-out mechanism for emails

282 of 1437 branches covered (19.62%)

Branch coverage included in aggregate %.

53 of 53 new or added lines in 7 files covered. (100.0%)

959 of 3954 relevant lines covered (24.25%)

4.03 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 { getMessage } from '../utils/fluent.js'
11

12
const log = mozlog('email-utils')
×
13

14
const { SERVER_URL } = AppConstants
×
15

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

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

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

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

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

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

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

78
function appendUtmParams (url, utmParams) {
79
  const defaultUtmParams = {
×
80
    utm_source: 'fx-monitor',
81
    utm_medium: 'email'
82
  }
83

84
  const allUtmParams = { ...defaultUtmParams, ...utmParams }
×
85

86
  for (const param in allUtmParams) {
×
87
    const paramValue = allUtmParams[param]
×
88

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

94
  return url
×
95
}
96

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

106
  return appendUtmParams(url, utmParams)
×
107
}
108

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

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

121
  return appendUtmParams(url, verificationUtmParams)
×
122
}
123

124
// TODO: Unsubscribing from emails is only implemented for
125
// the monthly unresolved breach report
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
  const url = new URL(path)
×
136
  const token = Object.hasOwn(subscriber, 'verification_token')
×
137
    ? subscriber.verification_token
138
    : subscriber.primary_verification_token
139
  const hash = Object.hasOwn(subscriber, 'sha1')
×
140
    ? subscriber.sha1
141
    : subscriber.primary_sha1
142

143
  // Mandatory params for unsubscribing a user
144
  const unsubscribeUtmParams = {
×
145
    hash: encodeURIComponent(hash),
146
    token: encodeURIComponent(token),
147
    utm_campaign: isMonthlyEmail
×
148
      ? 'monthly-unresolved'
149
      : 'unsubscribe',
150
    utm_content: 'unsubscribe-cta'
151
  }
152

153
  return appendUtmParams(url, unsubscribeUtmParams)
×
154
}
155

156
function postUnsubscribe (req, res) {
157
  console.log('postUnsubscribe')
×
158

159
  return res.json({
×
160
    success: true,
161
    status: 200,
162
    message: 'Unsubscribed'
163
  })
164
}
165

166
/**
167
 * Dummy data for populating the breach notification email preview
168
 *
169
 * @param {string} recipient
170
 * @returns {object} Breach dummy data
171
 */
172
const getNotifictionDummyData = (recipient) => ({
×
173
  breachData: {
174
    Id: 1,
175
    Name: 'Adobe',
176
    Title: 'Adobe',
177
    Domain: 'adobe.com',
178
    BreachDate: '2013-01-01T22:00:00.000Z',
179
    AddedDate: '2013-01-02T00:00:00.000Z',
180
    ModifiedDate: '2023-01-01T00:00:00.000Z',
181
    PwnCount: 123,
182
    Description: 'Example description',
183
    LogoPath: '/images/favicon-144.webp',
184
    DataClasses: [
185
      'email-addresses',
186
      'password-hints',
187
      'passwords',
188
      'usernames'
189
    ],
190
    IsVerified: true,
191
    IsFabricated: false,
192
    IsSensitive: false,
193
    IsRetired: false,
194
    IsSpamList: false,
195
    IsMalware: false
196
  },
197
  breachedEmail: recipient,
198
  ctaHref: getEmailCtaHref('email-test-notification', 'dashboard-cta'),
199
  heading: getMessage('email-spotted-new-breach'),
200
  recipientEmail: recipient,
201
  subscriberId: 123,
202
  supportedLocales: ['en'],
203
  utm_utmCampaign: ''
204
})
205

206
/**
207
 * Dummy data for populating the email verification preview
208
 *
209
 * @param {string} recipient
210
 * @returns {object} Email verification dummy data
211
 */
212
const getVerificationDummyData = (recipient) => ({
×
213
  ctaHref: SERVER_URL,
214
  heading: getMessage('email-verify-heading'),
215
  recipientEmail: recipient,
216
  subheading: getMessage('email-verify-subhead'),
217
  utm_utmCampaign: 'email_verify'
218
})
219

220
/**
221
 * Dummy data for populating the monthly unresolved breaches email
222
 *
223
 * @param {string} recipient
224
 * @returns {object} Monthly unresolved breaches dummy data
225
 */
226
const getMonthlyDummyData = (recipient) => ({
×
227
  breachedEmail: 'breached@email.com',
228
  ctaHref: SERVER_URL,
229
  heading: getMessage('email-unresolved-heading'),
230
  monitoredEmails: {
231
    count: 2
232
  },
233
  numBreaches: {
234
    count: 3,
235
    numResolved: 2,
236
    numUnresolved: 1
237
  },
238
  recipientEmail: recipient,
239
  subheading: getMessage('email-unresolved-subhead'),
240
  unsubscribeUrl: getUnsubscribeCtaHref({
241
    subscriber: null,
242
    isMonthlyEmail: true
243
  }),
244
  utm_utmCampaign: ''
245
})
246

247
/**
248
 * Dummy data for populating the signup report email
249
 *
250
 * @param {string} recipient
251
 * @returns {object} Signup report email dummy data
252
 */
253

254
const getSignupReportDummyData = (recipient) => {
×
255
  const unsafeBreachesForEmail = [
×
256
    getNotifictionDummyData(recipient).breachData
257
  ]
258
  const breachesCount = unsafeBreachesForEmail.length
×
259
  const numPasswordsExposed = 1
×
260

261
  const emailBreachStats = [
×
262
    {
263
      statNumber: breachesCount,
264
      statTitle: getMessage('known-data-breaches-exposed', {
265
        breaches: breachesCount
266
      })
267
    },
268
    {
269
      statNumber: numPasswordsExposed,
270
      statTitle: getMessage('passwords-exposed', {
271
        passwords: numPasswordsExposed
272
      })
273
    }
274
  ]
275

276
  return {
×
277
    breachedEmail: recipient,
278
    ctaHref: getEmailCtaHref('email-test-notification', 'dashboard-cta'),
279
    heading: unsafeBreachesForEmail.length
×
280
      ? getMessage('email-subject-found-breaches')
281
      : getMessage('email-subject-no-breaches'),
282
    emailBreachStats,
283
    recipientEmail: recipient,
284
    subscriberId: 123,
285
    unsafeBreachesForEmail,
286
    utm_utmCampaign: ''
287
  }
288
}
289

290
export {
291
  EmailTemplateType,
292
  getEmailCtaHref,
293
  getMonthlyDummyData,
294
  getNotifictionDummyData,
295
  getSignupReportDummyData,
296
  getUnsubscribeCtaHref,
297
  getVerificationDummyData,
298
  getVerificationUrl,
299
  initEmail,
300
  postUnsubscribe,
301
  sendEmail
302
}
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