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

mozilla / blurts-server / 4040bc25-7f30-464b-958e-746a756a4e44

pending completion
4040bc25-7f30-464b-958e-746a756a4e44

push

circleci

GitHub
Merge pull request #2790 from mozilla/MNTOR-1056-Migrate-breach-alert-email

282 of 1375 branches covered (20.51%)

Branch coverage included in aggregate %.

174 of 174 new or added lines in 17 files covered. (100.0%)

959 of 3709 relevant lines covered (25.86%)

4.2 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
 * @param {string} recipient
43
 * @param {string} subject
44
 * @param {string} html
45
 * @returns <Promise>
46
 */
47
function sendEmail (recipient, subject, html) {
48
  if (!gTransporter) {
×
49
    return Promise.reject(new Error('SMTP transport not initialized'))
×
50
  }
51

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

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

77
function appendUtmParams (url, campaign, content) {
78
  const utmParameters = {
×
79
    utm_source: 'fx-monitor',
80
    utm_medium: 'email',
81
    utm_campaign: campaign,
82
    utm_content: content
83
  }
84
  for (const param in utmParameters) {
×
85
    url.searchParams.append(param, utmParameters[param])
×
86
  }
87
  return url
×
88
}
89

90
function getEmailCtaHref (emailType, content, subscriberId = null) {
×
91
  const subscriberParamPath = (subscriberId) ? `/?subscriber_id=${subscriberId}` : '/'
×
92
  const url = new URL(subscriberParamPath, SERVER_URL)
×
93
  return appendUtmParams(url, emailType, content)
×
94
}
95

96
function getVerificationUrl (subscriber) {
97
  if (!subscriber.verification_token) throw new Error('subscriber has no verification_token')
×
98
  let url = new URL(`${SERVER_URL}/api/v1/user/verify-email`)
×
99
  url = appendUtmParams(url, 'verified-subscribers', 'account-verification-email')
×
100
  url.searchParams.append('token', encodeURIComponent(subscriber.verification_token))
×
101
  return url
×
102
}
103

104
function getUnsubscribeUrl (subscriber, emailType) {
105
  // TODO: email unsubscribe is broken for most emails
106
  let url = new URL(`${SERVER_URL}/user/unsubscribe`)
×
107
  const token = (Object.prototype.hasOwnProperty.call(subscriber, 'verification_token')) ? subscriber.verification_token : subscriber.primary_verification_token
×
108
  const hash = (Object.prototype.hasOwnProperty.call(subscriber, 'sha1')) ? subscriber.sha1 : subscriber.primary_sha1
×
109
  url.searchParams.append('token', encodeURIComponent(token))
×
110
  url.searchParams.append('hash', encodeURIComponent(hash))
×
111
  url = appendUtmParams(url, 'unsubscribe', emailType)
×
112
  return url
×
113
}
114

115
function getMonthlyUnsubscribeUrl (subscriber, campaign, content) {
116
  // TODO: create new subscriptions section in settings to manage all emails and avoid one-off routes like this
117
  if (!subscriber.primary_verification_token) throw new Error('subscriber has no primary verification_token')
×
118
  let url = new URL('user/unsubscribe-monthly/', SERVER_URL)
×
119

120
  url = appendUtmParams(url, campaign, content)
×
121
  url.searchParams.append('token', encodeURIComponent(subscriber.primary_verification_token))
×
122

123
  return url
×
124
}
125

126
/**
127
 * Dummy data for populating the breach notification email preview
128
 *
129
 * @param {string} recipient
130
 * @returns {object} Breach dummy data
131
 */
132
const getNotifictionDummyData = (recipient) => ({
×
133
  breachData: {
134
    Id: 1,
135
    Name: 'Adobe',
136
    Title: 'Adobe',
137
    Domain: 'adobe.com',
138
    BreachDate: '2013-01-01T22:00:00.000Z',
139
    AddedDate: '2013-01-02T00:00:00.000Z',
140
    ModifiedDate: '2023-01-01T00:00:00.000Z',
141
    PwnCount: 123,
142
    Description: 'Example description',
143
    LogoPath: '/images/favicon-144.webp',
144
    DataClasses: [
145
      'email-addresses',
146
      'password-hints',
147
      'passwords',
148
      'usernames'
149
    ],
150
    IsVerified: true,
151
    IsFabricated: false,
152
    IsSensitive: false,
153
    IsRetired: false,
154
    IsSpamList: false,
155
    IsMalware: false
156
  },
157
  breachedEmail: recipient,
158
  ctaHref: SERVER_URL,
159
  heading: getMessage('email-spotted-new-breach'),
160
  recipientEmail: recipient,
161
  subscriberId: 123,
162
  supportedLocales: ['en'],
163
  unsubscribeUrl: SERVER_URL,
164
  utmCampaign: ''
165
})
166

167
/**
168
 * Dummy data for populating the email verification preview
169
 *
170
 * @param {string} recipient
171
 * @returns {object} Email verification dummy data
172
 */
173
const getVerificationDummyData = (recipient) => ({
×
174
  recipientEmail: recipient,
175
  ctaHref: SERVER_URL,
176
  utmCampaign: 'email_verify',
177
  unsubscribeUrl: SERVER_URL,
178
  heading: getMessage('email-verify-heading'),
179
  subheading: getMessage('email-verify-subhead')
180
})
181

182
/**
183
 * Dummy data for populating the monthly unresolved breaches email
184
 *
185
 * @param {string} recipient
186
 * @returns {object} Monthly unresolved breaches dummy data
187
 */
188
const getMonthlyDummyData = (recipient) => ({
×
189
  recipientEmail: recipient,
190
  ctaHref: SERVER_URL,
191
  utmCampaign: '',
192
  unsubscribeUrl: SERVER_URL,
193
  heading: getMessage('email-unresolved-heading'),
194
  subheading: getMessage('email-unresolved-subhead'),
195
  breachedEmail: 'breached@email.com',
196
  monitoredEmails: {
197
    count: 2
198
  },
199
  numBreaches: {
200
    count: 3,
201
    numResolved: 2,
202
    numUnresolved: 1
203
  }
204
})
205

206
/**
207
 * Dummy data for populating the signup report email
208
 *
209
 * @param {string} recipient
210
 * @returns {object} Signup report email dummy data
211
 */
212

213
const getSignupReportDummyData = (recipient) => {
×
214
  const unsafeBreachesForEmail = [
×
215
    getNotifictionDummyData(recipient)
216
  ]
217
  const breachesCount = unsafeBreachesForEmail.length
×
218
  const numPasswordsExposed = 1
×
219

220
  const emailBreachStats = [
×
221
    {
222
      statNumber: breachesCount,
223
      statTitle: getMessage('known-data-breaches-exposed', {
224
        breaches: breachesCount
225
      })
226
    },
227
    {
228
      statNumber: numPasswordsExposed,
229
      statTitle: getMessage('passwords-exposed', {
230
        passwords: numPasswordsExposed
231
      })
232
    }
233
  ]
234

235
  return {
×
236
    breachedEmail: recipient,
237
    ctaHref: SERVER_URL,
238
    heading: unsafeBreachesForEmail.length
×
239
      ? getMessage('email-subject-found-breaches')
240
      : getMessage('email-subject-no-breaches'),
241
    emailBreachStats,
242
    recipientEmail: recipient,
243
    subscriberId: 123,
244
    unsafeBreachesForEmail,
245
    unsubscribeUrl: SERVER_URL,
246
    utmCampaign: ''
247
  }
248
}
249

250
export {
251
  EmailTemplateType,
252
  getEmailCtaHref,
253
  getMonthlyDummyData,
254
  getMonthlyUnsubscribeUrl,
255
  getNotifictionDummyData,
256
  getSignupReportDummyData,
257
  getUnsubscribeUrl,
258
  getVerificationDummyData,
259
  getVerificationUrl,
260
  initEmail,
261
  sendEmail
262
}
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