packages/fxa-auth-server/lib/senders/email.js (3,009 lines of code) (raw):

/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 'use strict'; const emailUtils = require('../email/utils/helpers'); const moment = require('moment-timezone'); const AWS = require('aws-sdk'); const nodemailer = require('nodemailer'); const safeUserAgent = require('fxa-shared/lib/user-agent').default; const url = require('url'); const { URL } = url; const { localizedPlanConfig, } = require('fxa-shared/subscriptions/configuration/utils'); const { productDetailsFromPlan } = require('fxa-shared').subscriptions.metadata; const Renderer = require('./renderer').default; const { NodeRendererBindings } = require('./renderer/bindings-node'); const { determineLocale } = require('../../../../libs/shared/l10n/src'); const TEMPLATE_VERSIONS = require('./emails/templates/_versions.json'); const DEFAULT_LOCALE = 'en'; const DEFAULT_TIMEZONE = 'Etc/UTC'; const UTM_PREFIX = 'fx-'; const X_SES_CONFIGURATION_SET = 'X-SES-CONFIGURATION-SET'; const X_SES_MESSAGE_TAGS = 'X-SES-MESSAGE-TAGS'; module.exports = function (log, config, bounces, statsd) { const oauthClientInfo = require('./oauth_client_info')(log, config); const verificationReminders = require('../verification-reminders')( log, config ); const cadReminders = require('../cad-reminders')(config, log); const subscriptionAccountReminders = require('../subscription-account-reminders')(log, config); const paymentsServerURL = new URL(config.subscriptions.paymentsServer.url); // Email template to UTM campaign map, each of these should be unique and // map to exactly one email template. const templateNameToCampaignMap = { subscriptionAccountFinishSetup: 'subscription-account-finish-setup', subscriptionReactivation: 'subscription-reactivation', subscriptionRenewalReminder: 'subscription-renewal-reminder', subscriptionReplaced: 'subscription-replaced', subscriptionUpgrade: 'subscription-upgrade', subscriptionDowngrade: 'subscription-downgrade', subscriptionPaymentExpired: 'subscription-payment-expired', subscriptionsPaymentExpired: 'subscriptions-payment-expired', subscriptionPaymentProviderCancelled: 'subscription-payment-provider-cancelled', subscriptionsPaymentProviderCancelled: 'subscriptions-payment-provider-cancelled', subscriptionPaymentFailed: 'subscription-payment-failed', subscriptionAccountDeletion: 'subscription-account-deletion', subscriptionCancellation: 'subscription-cancellation', subscriptionFailedPaymentsCancellation: 'subscription-failed-payments-cancellation', subscriptionSubsequentInvoice: 'subscription-subsequent-invoice', subscriptionFirstInvoice: 'subscription-first-invoice', downloadSubscription: 'new-subscription', fraudulentAccountDeletion: 'account-deletion', inactiveAccountFirstWarning: 'account-inactive-reminder-first', inactiveAccountSecondWarning: 'account-inactive-reminder-second', inactiveAccountFinalWarning: 'account-inactive-reminder-third', lowRecoveryCodes: 'low-recovery-codes', newDeviceLogin: 'new-device-signin', passwordChangeRequired: 'password-change-required', passwordChanged: 'password-changed-success', passwordForgotOtp: 'password-forgot-otp', passwordReset: 'password-reset-success', passwordResetAccountRecovery: 'password-reset-account-recovery-success', passwordResetWithRecoveryKeyPrompt: 'password-reset-w-recovery-key-prompt', postAddLinkedAccount: 'account-linked', postRemoveSecondary: 'account-email-removed', postVerify: 'account-verified', postChangePrimary: 'account-email-changed', postVerifySecondary: 'account-email-verified', postAddTwoStepAuthentication: 'account-two-step-enabled', postRemoveTwoStepAuthentication: 'account-two-step-disabled', postConsumeRecoveryCode: 'account-consume-recovery-code', postNewRecoveryCodes: 'account-replace-recovery-codes', postAddAccountRecovery: 'account-recovery-generated', postChangeAccountRecovery: 'account-recovery-changed', postRemoveAccountRecovery: 'account-recovery-removed', postAddRecoveryPhone: 'recovery-phone-added', postChangeRecoveryPhone: 'recovery-phone-changed', postRemoveRecoveryPhone: 'recovery-phone-removed', postSigninRecoveryPhone: 'signin-recovery-phone', postSigninRecoveryCode: 'signin-recovery-code', recovery: 'forgot-password', unblockCode: 'new-unblock', verify: 'welcome', verifyShortCode: 'welcome', verifyLogin: 'new-signin', verifyLoginCode: 'new-signin-verify-code', verifyPrimary: 'welcome-primary', verifySecondaryCode: 'welcome-secondary', }; // Email template to UTM content, this is typically the main call out link/button // in template. // Please create a DB migration to add the new templates into `emailTypes` // when you add new templates. const templateNameToContentMap = { subscriptionAccountFinishSetup: 'subscriptions', subscriptionReactivation: 'subscriptions', subscriptionRenewalReminder: 'subscriptions', subscriptionReplaced: 'subscriptions', subscriptionUpgrade: 'subscriptions', subscriptionDowngrade: 'subscriptions', subscriptionPaymentExpired: 'subscriptions', subscriptionsPaymentExpired: 'subscriptions', subscriptionPaymentProviderCancelled: 'subscriptions', subscriptionsPaymentProviderCancelled: 'subscriptions', subscriptionPaymentFailed: 'subscriptions', subscriptionAccountDeletion: 'subscriptions', subscriptionCancellation: 'subscriptions', subscriptionFailedPaymentsCancellation: 'subscriptions', subscriptionSubsequentInvoice: 'subscriptions', subscriptionFirstInvoice: 'subscriptions', downloadSubscription: 'subscriptions', fraudulentAccountDeletion: 'manage-account', inactiveAccountFirstWarning: 'account-deletion', inactiveAccountSecondWarning: 'account-deletion', inactiveAccountFinalWarning: 'account-deletion', lowRecoveryCodes: 'recovery-codes', newDeviceLogin: 'manage-account', passwordChanged: 'password-change', passwordChangeRequired: 'password-change', passwordForgotOtp: 'password-reset', passwordReset: 'password-reset', passwordResetAccountRecovery: 'manage-account', passwordResetWithRecoveryKeyPrompt: 'create-recovery-key', postAddLinkedAccount: 'manage-account', postRemoveSecondary: 'account-email-removed', postVerify: 'connect-device', postChangePrimary: 'account-email-changed', postVerifySecondary: 'manage-account', postAddTwoStepAuthentication: 'manage-account', postRemoveTwoStepAuthentication: 'manage-account', postConsumeRecoveryCode: 'manage-account', postNewRecoveryCodes: 'manage-account', postAddAccountRecovery: 'manage-account', postChangeAccountRecovery: 'manage-account', postRemoveAccountRecovery: 'manage-account', postAddRecoveryPhone: 'manage-account', postSigninRecoveryPhone: 'manage-account', postSigninRecoveryCode: 'manage-account', recovery: 'reset-password', unblockCode: 'unblock-code', verify: 'activate', verifyShortCode: 'activate', verifyLogin: 'confirm-signin', verifyLoginCode: 'new-signin-verify-code', verifyPrimary: 'activate', verifySecondaryCode: 'activate', }; function extend(target, source) { for (const key in source) { target[key] = source[key]; } return target; } // TODO: can this be modified/removed? FXA-4761 / #12259 function linkAttributes(url) { // Not very nice to have presentation code in here, but this is to help l10n // contributors not deal with extraneous noise in strings. return `href="${url}" style="color: #0a84ff; text-decoration: none; font-family: sans-serif;"`; } function constructLocalTimeString(timeZone, locale) { // if no timeZone is passed, use DEFAULT_TIMEZONE moment.tz.setDefault(DEFAULT_TIMEZONE); // if no locale is passed, use DEFAULT_LOCALE locale = locale || DEFAULT_LOCALE; moment.locale(locale); let timeMoment = moment(); if (timeZone) { timeMoment = timeMoment.tz(timeZone); } // return a locale-specific time // if date or time is passed, return it as the current date or time const timeNow = timeMoment.format('LTS (z)'); const dateNow = timeMoment.format('dddd, ll'); return [timeNow, dateNow]; } function constructLocalDateString( timeZone, locale, date, formatString = 'L' ) { // if no timeZone is passed, use DEFAULT_TIMEZONE moment.tz.setDefault(DEFAULT_TIMEZONE); // if no locale is passed, use DEFAULT_LOCALE locale = locale || DEFAULT_LOCALE; moment.locale(locale); let time = moment(date); if (timeZone) { time = time.tz(timeZone); } // return a locale-specific date return time.format(formatString); } // Borrowed from fxa-payments-server/src/lib/formats.ts // TODO: Would be nice to share this if/when TypeScript conversion reaches here. const baseCurrencyOptions = { style: 'currency', currencyDisplay: 'symbol', }; /** * This returns a string that is formatted according to the given locale. * * Borrowed from fxa-payments-server/src/lib/formats.ts * TODO: Would be nice to share this if/when TypeScript conversion reaches here. * * @param {number} amountInCents * @param {string} currency * @param {string} locale */ function getLocalizedCurrencyString( amountInCents, currency = 'usd', locale = 'en-US' ) { const decimal = amountInCents / 100; const options = { ...baseCurrencyOptions, currency }; try { return new Intl.NumberFormat(locale, options).format(decimal); } catch (e) { // The exception could be a verror wrapped one. const cause = e.cause ? e.cause() : e; // If the language tag is not something Intl can handle, use 'en-US'. if (cause.message.endsWith('Incorrect locale information provided')) { return getLocalizedCurrencyString(amountInCents, currency, 'en-US'); } throw e; } } function sesMessageTagsHeaderValue(templateName, serviceName) { return `messageType=fxa-${templateName}, app=fxa, service=${serviceName}, ses:feedback-id-a=fxa-${templateName}`; } // These are brand names, so they probably don't need l10n. const CARD_TYPE_TO_TEXT = { amex: 'American Express', diners: 'Diners Club', discover: 'Discover', jcb: 'JCB', mastercard: 'Mastercard', unionpay: 'UnionPay', visa: 'Visa', unknown: 'Unknown', }; function cardTypeToText(cardType) { if (typeof cardType !== 'string') { return null; } return ( CARD_TYPE_TO_TEXT[cardType.toLowerCase()] || CARD_TYPE_TO_TEXT.unknown ); } function Mailer(mailerConfig, sender) { let options = { host: mailerConfig.host, secure: mailerConfig.secure, ignoreTLS: !mailerConfig.secure, port: mailerConfig.port, pool: mailerConfig.pool, maxConnections: mailerConfig.maxConnections, maxMessages: mailerConfig.maxMessages, connectionTimeout: mailerConfig.connectionTimeout, greetingTimeout: mailerConfig.greetingTimeout, socketTimeout: mailerConfig.socketTimeout, dnsTimeout: mailerConfig.dnsTimeout, }; if (mailerConfig.user && mailerConfig.password) { options.auth = { user: mailerConfig.user, pass: mailerConfig.password, }; } else { const ses = new AWS.SES({ apiVersion: '2010-12-01' }); options = { SES: { ses }, sendingRate: 5, maxConnections: 10, }; } this.accountSettingsUrl = mailerConfig.accountSettingsUrl; this.accountRecoveryCodesUrl = mailerConfig.accountRecoveryCodesUrl; this.androidUrl = mailerConfig.androidUrl; this.createAccountRecoveryUrl = mailerConfig.createAccountRecoveryUrl; this.accountFinishSetupUrl = mailerConfig.accountFinishSetupUrl; this.initiatePasswordChangeUrl = mailerConfig.initiatePasswordChangeUrl; this.initiatePasswordResetUrl = mailerConfig.initiatePasswordResetUrl; this.iosUrl = mailerConfig.iosUrl; this.iosAdjustUrl = mailerConfig.iosAdjustUrl; this.mailer = sender || nodemailer.createTransport(options); this.passwordManagerInfoUrl = mailerConfig.passwordManagerInfoUrl; this.passwordResetUrl = mailerConfig.passwordResetUrl; this.prependVerificationSubdomain = mailerConfig.prependVerificationSubdomain; this.privacyUrl = mailerConfig.privacyUrl; this.reportSignInUrl = mailerConfig.reportSignInUrl; this.revokeAccountRecoveryUrl = mailerConfig.revokeAccountRecoveryUrl; this.sender = mailerConfig.sender; this.sesConfigurationSet = mailerConfig.sesConfigurationSet; this.subscriptionSettingsUrl = mailerConfig.subscriptionSettingsUrl; this.subscriptionSupportUrl = mailerConfig.subscriptionSupportUrl; this.subscriptionTermsUrl = mailerConfig.subscriptionTermsUrl; this.supportUrl = mailerConfig.supportUrl; this.syncUrl = mailerConfig.syncUrl; this.unsubscribeUrl = mailerConfig.unsubscribeUrl; this.verificationUrl = mailerConfig.verificationUrl; this.verifyLoginUrl = mailerConfig.verifyLoginUrl; this.verifyPrimaryEmailUrl = mailerConfig.verifyPrimaryEmailUrl; this.renderer = new Renderer(new NodeRendererBindings()); this.metricsEnabled = true; } Mailer.prototype.stop = function () { this.mailer.close(); }; Mailer.prototype._supportLinkAttributes = function (templateName) { return linkAttributes(this.createSupportLink(templateName)); }; Mailer.prototype._passwordResetLinkAttributes = function ( email, templateName, emailToHashWith ) { return linkAttributes( this.createPasswordResetLink(email, templateName, emailToHashWith) ); }; Mailer.prototype._passwordChangeLinkAttributes = function ( email, templateName ) { return linkAttributes(this.createPasswordChangeLink(email, templateName)); }; Mailer.prototype._formatUserAgentInfo = function (message) { const uaBrowser = safeUserAgent.safeName(message.uaBrowser); const uaOS = safeUserAgent.safeName(message.uaOS); const uaOSVersion = safeUserAgent.safeVersion(message.uaOSVersion); return !uaBrowser && !uaOS ? null : { uaBrowser, uaOS, uaOSVersion, }; }; Mailer.prototype._constructLocalTimeString = function ( timeZone, acceptLanguage ) { return constructLocalTimeString(timeZone, determineLocale(acceptLanguage)); }; Mailer.prototype._constructLocalDateString = function ( timeZone, acceptLanguage, date, formatString = 'L' ) { return constructLocalDateString( timeZone, determineLocale(acceptLanguage), date, formatString ); }; Mailer.prototype._getLocalizedCurrencyString = function ( amountInCents, currency, acceptLanguage ) { return getLocalizedCurrencyString( amountInCents, currency, determineLocale(acceptLanguage) ); }; Mailer.prototype.localize = async function (message) { message.layout = message.layout || 'fxa'; const { html, text, subject, preview } = await this.renderer.renderEmail(message); return { html, language: determineLocale(message.acceptLanguage), subject, preview, text, }; }; Mailer.prototype.send = async function (message) { // Make sure brandMessagingMode always reflects the current config state. if (message && message.templateValues) { message.templateValues.brandMessagingMode = config.smtp.brandMessagingMode; } log.trace(`mailer.${message.template}`, { email: message.email, uid: message.uid, }); const localized = await this.localize(message); const template = message.template; let templateVersion = TEMPLATE_VERSIONS[template]; if (!templateVersion) { log.error('emailTemplateVersion.missing', { template }); templateVersion = 1; } message.templateVersion = templateVersion; const headers = { 'Content-Language': localized.language, 'X-Template-Name': template, 'X-Template-Version': templateVersion, ...message.headers, ...optionalHeader('X-Device-Id', message.deviceId), ...optionalHeader('X-Flow-Id', message.flowId), ...optionalHeader('X-Flow-Begin-Time', message.flowBeginTime), ...optionalHeader('X-Service-Id', message.service), ...optionalHeader('X-Uid', message.uid), }; const to = message.email; try { await bounces.check(to, template); } catch (err) { const tags = { template, error: err.errno }; statsd.increment('email.bounce.limit', tags); log.error('email.bounce.limit', { err: err.message, errno: err.errno, to, template, }); return; } if (this.sesConfigurationSet) { // Note on SES Event Publishing: The X-SES-CONFIGURATION-SET and // X-SES-MESSAGE-TAGS email headers will be stripped by SES from the // actual outgoing email messages. headers[X_SES_CONFIGURATION_SET] = this.sesConfigurationSet; headers[X_SES_MESSAGE_TAGS] = sesMessageTagsHeaderValue( message.metricsTemplate || template, 'fxa-auth-server' ); } log.debug('mailer.send', { email: to, template, headers: Object.keys(headers).join(','), }); const emailConfig = { sender: this.sender, from: this.sender, to, subject: localized.subject, preview: localized.preview, text: localized.text, html: localized.html, xMailer: false, headers, }; if (message.ccEmails) { emailConfig.cc = message.ccEmails; } await new Promise((resolve, reject) => { this.mailer.sendMail(emailConfig, (err, status) => { if (err) { log.error('mailer.send.error', { err: err.message, code: err.code, errno: err.errno, message: status && status.message, to: emailConfig && emailConfig.to, template, }); return reject(err); } log.debug('mailer.send.1', { status: status && status.message, id: status && status.messageId, to: emailConfig && emailConfig.to, }); emailUtils.logEmailEventSent(log, { ...message, headers, }); emailUtils.logAccountEventFromMessage( { headers: { ...headers, }, }, 'emailSent' ); return resolve(status); }); }); }; Mailer.prototype.verifyEmail = async function (message) { log.trace('mailer.verifyEmail', { email: message.email, uid: message.uid }); const templateName = 'verify'; const query = { uid: message.uid, code: message.code, }; const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); if (message.service) { query.service = message.service; } if (message.redirectTo) { query.redirectTo = message.redirectTo; } if (message.resume) { query.resume = message.resume; } if (message.style) { query.style = message.style; } const links = this._generateLinks( this.verificationUrl, message, query, templateName ); const headers = { 'X-Link': links.link, 'X-Verify-Code': message.code, }; const { name: serviceName } = await oauthClientInfo.fetch(message.service); return this.send({ ...message, headers, template: templateName, templateValues: { device: this._formatUserAgentInfo(message), date: date, email: message.email, link: links.link, oneClickLink: links.oneClickLink, privacyUrl: links.privacyUrl, serviceName: serviceName, style: message.style, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, sync: message.service === 'sync', time: time, }, }); }; Mailer.prototype.verifyShortCodeEmail = async function (message) { log.trace('mailer.verifyShortCodeEmail', { email: message.email, uid: message.uid, }); const templateName = 'verifyShortCode'; const metricsTemplateName = 'verify'; const code = message.code; const links = this._generateLinks( this.verificationUrl, message, {}, templateName ); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Verify-Short-Code': code, }; return this.send({ ...message, headers, metricsTemplate: metricsTemplateName, template: templateName, templateValues: { code, date, device: this._formatUserAgentInfo(message), email: message.email, privacyUrl: links.privacyUrl, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; verificationReminders.keys.forEach((key, index) => { // Template names are generated in the form `verificationReminderFirstEmail`, // where `First` is the key derived from config, with an initial capital letter. const template = `verificationReminder${key[0].toUpperCase()}${key.substr( 1 )}`; templateNameToCampaignMap[template] = `${key}-verification-reminder`; templateNameToContentMap[template] = 'confirm-email'; Mailer.prototype[`${template}Email`] = async function (message) { const { code, email, uid } = message; log.trace(`mailer.${template}`, { code, email, uid }); const query = { code, reminder: key, uid }; const links = this._generateLinks( this.verificationUrl, message, query, template ); const headers = { 'X-Link': links.link, 'X-Verify-Code': code, }; return this.send({ ...message, headers, template, templateValues: { email, link: links.link, oneClickLink: links.oneClickLink, privacyUrl: links.privacyUrl, supportUrl: links.supportUrl, supportLinkAttributes: links.supportLinkAttributes, }, }); }; }); subscriptionAccountReminders.keys.forEach((key, index) => { // Template names are generated in the form `verificationReminderFirstEmail`, // where `First` is the key derived from config, with an initial capital letter. const template = `subscriptionAccountReminder${key[0].toUpperCase()}${key.substr( 1 )}`; templateNameToCampaignMap[template] = `${key}-subscription-account-reminder`; templateNameToContentMap[template] = 'subscription-account-create-email'; Mailer.prototype[`${template}Email`] = async function (message) { const { email, uid, productId, productName, token, flowId, flowBeginTime, deviceId, accountVerified, } = message; log.trace(`mailer.${template}`, { email, uid }); const query = { email, product_name: productName, token, product_id: productId, flowId, flowBeginTime, deviceId, }; const links = this._generateLinks( this.accountFinishSetupUrl, message, query, template ); const headers = { 'X-Link': links.link, }; if (!accountVerified) { return this.send({ ...message, headers, layout: 'subscription', template, templateValues: { email, ...links, oneClickLink: links.oneClickLink, privacyUrl: links.privacyUrl, termsOfServiceDownloadURL: links.termsOfServiceDownloadURL, supportUrl: links.supportUrl, supportLinkAttributes: links.supportLinkAttributes, reminderShortForm: true, }, }); } }; }); Mailer.prototype.unblockCodeEmail = function (message) { log.trace('mailer.unblockCodeEmail', { email: message.email, uid: message.uid, }); const templateName = 'unblockCode'; const query = { unblockCode: message.unblockCode, email: message.email, uid: message.uid, }; const links = this._generateLinks(null, message, query, templateName); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Unblock-Code': message.unblockCode, 'X-Report-SignIn-Link': links.reportSignInLink, }; return this.send({ ...message, headers, template: templateName, templateValues: { date, device: this._formatUserAgentInfo(message), email: message.email, privacyUrl: links.privacyUrl, reportSignInLink: links.reportSignInLink, reportSignInLinkAttributes: links.reportSignInLinkAttributes, time, unblockCode: message.unblockCode, }, }); }; Mailer.prototype.verifyLoginEmail = async function (message) { log.trace('mailer.verifyLoginEmail', { email: message.email, uid: message.uid, }); const templateName = 'verifyLogin'; const query = { code: message.code, uid: message.uid, }; if (message.service) { query.service = message.service; } if (message.redirectTo) { query.redirectTo = message.redirectTo; } if (message.resume) { query.resume = message.resume; } const links = this._generateLinks( this.verifyLoginUrl, message, query, templateName ); const headers = { 'X-Link': links.link, 'X-Verify-Code': message.code, }; const { name: clientName } = await oauthClientInfo.fetch(message.service); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); return this.send({ ...message, headers, template: templateName, templateValues: { clientName, date, device: this._formatUserAgentInfo(message), email: message.email, link: links.link, oneClickLink: links.oneClickLink, passwordChangeLink: links.passwordChangeLink, passwordChangeLinkAttributes: links.passwordChangeLinkAttributes, privacyUrl: links.privacyUrl, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.verifyLoginCodeEmail = async function (message) { log.trace('mailer.verifyLoginCodeEmail', { email: message.email, uid: message.uid, }); const templateName = 'verifyLoginCode'; const query = { code: message.code, uid: message.uid, }; if (message.service) { query.service = message.service; } if (message.redirectTo) { query.redirectTo = message.redirectTo; } if (message.resume) { query.resume = message.resume; } const links = this._generateLinks( this.verifyLoginUrl, message, query, templateName ); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Signin-Verify-Code': message.code, }; const { name: serviceName } = await oauthClientInfo.fetch(message.service); return this.send({ ...message, headers, template: templateName, templateValues: { code: message.code, date, device: this._formatUserAgentInfo(message), email: message.email, passwordChangeLink: links.passwordChangeLink, passwordChangeLinkAttributes: links.passwordChangeLinkAttributes, privacyUrl: links.privacyUrl, serviceName, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, tokenCode: message.code, }, }); }; Mailer.prototype.verifyPrimaryEmail = function (message) { log.trace('mailer.verifyPrimaryEmail', { email: message.email, uid: message.uid, }); const templateName = 'verifyPrimary'; const query = { code: message.code, uid: message.uid, type: 'primary', primary_email_verified: message.email, }; if (message.service) { query.service = message.service; } if (message.redirectTo) { query.redirectTo = message.redirectTo; } if (message.resume) { query.resume = message.resume; } const links = this._generateLinks( this.verifyPrimaryEmailUrl, message, query, templateName ); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Link': links.link, 'X-Verify-Code': message.code, }; return this.send({ ...message, headers, template: templateName, templateValues: { date, device: this._formatUserAgentInfo(message), email: message.primaryEmail, link: links.link, oneClickLink: links.oneClickLink, passwordChangeLink: links.passwordChangeLink, passwordChangeLinkAttributes: links.passwordChangeLinkAttributes, privacyUrl: links.privacyUrl, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.verifySecondaryCodeEmail = function (message) { log.trace('mailer.verifySecondaryCodeEmail', { email: message.email, uid: message.uid, }); const templateName = 'verifySecondaryCode'; const links = this._generateLinks(undefined, message, {}, templateName); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Verify-Code': message.code, }; return this.send({ ...message, headers, template: templateName, templateValues: { code: message.code, date, device: this._formatUserAgentInfo(message), email: message.email, passwordChangeLink: links.passwordChangeLink, passwordChangeLinkAttributes: links.passwordChangeLinkAttributes, primaryEmail: message.primaryEmail, privacyUrl: links.privacyUrl, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.recoveryEmail = function (message) { const templateName = 'recovery'; const query = { uid: message.uid, token: message.token, code: message.code, email: message.email, }; if (message.service) { query.service = message.service; } if (message.redirectTo) { query.redirectTo = message.redirectTo; } if (message.resume) { query.resume = message.resume; } if (message.emailToHashWith) { query.emailToHashWith = message.emailToHashWith; } const links = this._generateLinks( this.passwordResetUrl, message, query, templateName ); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Link': links.link, 'X-Recovery-Code': message.code, }; return this.send({ ...message, headers, template: templateName, templateValues: { code: message.code, date, device: this._formatUserAgentInfo(message), email: message.email, link: links.link, privacyUrl: links.privacyUrl, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.passwordChangedEmail = function (message) { const templateName = 'passwordChanged'; const links = this._generateLinks( this.initiatePasswordResetUrl, message, {}, templateName ); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Link': links.resetLink, }; return this.send({ ...message, headers, template: templateName, templateValues: { date, device: this._formatUserAgentInfo(message), privacyUrl: links.privacyUrl, resetLink: links.resetLink, resetLinkAttributes: links.resetLinkAttributes, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.passwordChangeRequiredEmail = function (message) { const templateName = 'passwordChangeRequired'; const links = this._generateLinks( this.initiatePasswordChangeUrl, message, {}, templateName ); const headers = { 'X-Link': links.passwordChangeLink, }; return this.send({ ...message, headers, template: templateName, templateValues: { passwordManagerInfoUrl: links.passwordManagerInfoUrl, privacyUrl: links.privacyUrl, resetLink: links.resetLink, supportUrl: links.supportUrl, }, }); }; Mailer.prototype.passwordForgotOtpEmail = async function (message) { log.trace('mailer.passwordForgotOtpEmail', { email: message.email, uid: message.uid, }); const templateName = 'passwordForgotOtp'; const code = message.code; const links = this._generateLinks(undefined, message, {}, templateName); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Password-Forgot-Otp': code, }; return this.send({ ...message, headers, template: templateName, templateValues: { code, date, device: this._formatUserAgentInfo(message), email: message.email, privacyUrl: links.privacyUrl, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.passwordResetEmail = function (message) { const templateName = 'passwordReset'; const links = this._generateLinks( this.initiatePasswordResetUrl, message, {}, templateName ); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Link': links.resetLink, }; return this.send({ ...message, headers, template: templateName, templateValues: { date, device: this._formatUserAgentInfo(message), privacyUrl: links.privacyUrl, resetLink: links.resetLink, resetLinkAttributes: links.resetLinkAttributes, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.postAddLinkedAccountEmail = function (message) { log.trace('mailer.postAddLinkedAccountEmail', { email: message.email, uid: message.uid, }); const templateName = 'postAddLinkedAccount'; const links = this._generateSettingLinks(message, templateName); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Link': links.passwordChangeLink, 'X-Linked-Account-Provider-Id': message.providerName, }; return this.send({ ...message, headers, template: templateName, templateValues: { date, device: this._formatUserAgentInfo(message), link: links.link, passwordChangeLink: links.passwordChangeLink, passwordChangeLinkAttributes: links.passwordChangeLinkAttributes, privacyUrl: links.privacyUrl, providerName: message.providerName, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.newDeviceLoginEmail = async function (message) { log.trace('mailer.newDeviceLoginEmail', { email: message.email, uid: message.uid, }); const templateName = 'newDeviceLogin'; const links = this._generateSettingLinks(message, templateName); const headers = { 'X-Link': links.passwordChangeLink, }; const { name: clientName } = await oauthClientInfo.fetch(message.service); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); return this.send({ ...message, headers, template: templateName, templateValues: { clientName, date, device: this._formatUserAgentInfo(message), link: links.link, passwordChangeLink: links.passwordChangeLink, passwordChangeLinkAttributes: links.passwordChangeLinkAttributes, privacyUrl: links.privacyUrl, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.postVerifyEmail = function (message) { log.trace('mailer.postVerifyEmail', { email: message.email, uid: message.uid, }); const onDesktopOrTabletDevice = !message.onMobileDevice; const templateName = 'postVerify'; const query = {}; const links = this._generateLinks( this.syncUrl, message, query, templateName ); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, template: templateName, templateValues: { onDesktopOrTabletDevice, androidLinkAttributes: linkAttributes(links.androidLink), androidUrl: links.androidLink, cadLinkAttributes: linkAttributes(links.link), desktopLink: config.smtp.firefoxDesktopUrl, desktopLinkAttributes: linkAttributes(config.smtp.firefoxDesktopUrl), iosLinkAttributes: linkAttributes(links.iosLink), iosUrl: links.iosLink, link: links.link, privacyUrl: links.privacyUrl, style: message.style, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, productName: 'Firefox', }, }); }; Mailer.prototype.postVerifySecondaryEmail = function (message) { log.trace('mailer.postVerifySecondaryEmail', { email: message.email, uid: message.uid, }); const templateName = 'postVerifySecondary'; const links = this._generateSettingLinks(message, templateName); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, template: templateName, templateValues: { androidLink: links.androidLink, iosLink: links.iosLink, link: links.link, passwordChangeLink: links.passwordChangeLink, passwordChangeLinkAttributes: links.passwordChangeLinkAttributes, privacyUrl: links.privacyUrl, secondaryEmail: message.secondaryEmail, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, }, }); }; Mailer.prototype.postChangePrimaryEmail = function (message) { log.trace('mailer.postChangePrimaryEmail', { email: message.email, uid: message.uid, }); const templateName = 'postChangePrimary'; const links = this._generateSettingLinks(message, templateName); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, template: templateName, templateValues: { androidLink: links.androidLink, email: message.email, iosLink: links.iosLink, link: links.link, passwordChangeLink: links.passwordChangeLink, passwordChangeLinkAttributes: links.passwordChangeLinkAttributes, privacyUrl: links.privacyUrl, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, }, }); }; Mailer.prototype.postRemoveSecondaryEmail = function (message) { log.trace('mailer.postRemoveSecondaryEmail', { email: message.email, uid: message.uid, }); const templateName = 'postRemoveSecondary'; const links = this._generateSettingLinks(message, templateName); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, template: templateName, templateValues: { androidLink: links.androidLink, iosLink: links.iosLink, link: links.link, privacyUrl: links.privacyUrl, secondaryEmail: message.secondaryEmail, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, }, }); }; Mailer.prototype.postAddTwoStepAuthenticationEmail = function (message) { log.trace('mailer.postAddTwoStepAuthenticationEmail', { email: message.email, uid: message.uid, }); const templateName = 'postAddTwoStepAuthentication'; const links = this._generateSettingLinks(message, templateName); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, template: templateName, templateValues: { androidLink: links.androidLink, date, device: this._formatUserAgentInfo(message), email: message.email, iosLink: links.iosLink, link: links.link, passwordChangeLink: links.passwordChangeLink, passwordChangeLinkAttributes: links.passwordChangeLinkAttributes, privacyUrl: links.privacyUrl, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.postRemoveTwoStepAuthenticationEmail = function (message) { log.trace('mailer.postRemoveTwoStepAuthenticationEmail', { email: message.email, uid: message.uid, }); const templateName = 'postRemoveTwoStepAuthentication'; const links = this._generateSettingLinks(message, templateName); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, template: templateName, templateValues: { androidLink: links.androidLink, date, device: this._formatUserAgentInfo(message), email: message.email, iosLink: links.iosLink, link: links.link, passwordChangeLink: links.passwordChangeLink, passwordChangeLinkAttributes: links.passwordChangeLinkAttributes, privacyUrl: links.privacyUrl, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.postNewRecoveryCodesEmail = function (message) { log.trace('mailer.postNewRecoveryCodesEmail', { email: message.email, uid: message.uid, }); const templateName = 'postNewRecoveryCodes'; const links = this._generateSettingLinks(message, templateName); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, template: templateName, templateValues: { androidLink: links.androidLink, date, device: this._formatUserAgentInfo(message), email: message.email, iosLink: links.iosLink, link: links.link, passwordChangeLink: links.passwordChangeLink, passwordChangeLinkAttributes: links.passwordChangeLinkAttributes, privacyUrl: links.privacyUrl, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.postConsumeRecoveryCodeEmail = function (message) { log.trace('mailer.postConsumeRecoveryCodeEmail', { email: message.email, uid: message.uid, }); const templateName = 'postConsumeRecoveryCode'; const links = this._generateSettingLinks(message, templateName); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, template: templateName, templateValues: { androidLink: links.androidLink, date, device: this._formatUserAgentInfo(message), email: message.email, iosLink: links.iosLink, link: links.link, numberRemaining: message.numberRemaining, passwordChangeLink: links.passwordChangeLink, passwordChangeLinkAttributes: links.passwordChangeLinkAttributes, privacyUrl: links.privacyUrl, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.lowRecoveryCodesEmail = function (message) { const { numberRemaining } = message; log.trace('mailer.lowRecoveryCodesEmail', { email: message.email, uid: message.uid, numberRemaining, }); const templateName = 'lowRecoveryCodes'; const links = this._generateLowRecoveryCodesLinks(message, templateName); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, template: templateName, templateValues: { androidLink: links.androidLink, email: message.email, iosLink: links.iosLink, link: links.link, numberRemaining, passwordChangeLink: links.passwordChangeLink, passwordChangeLinkAttributes: links.passwordChangeLinkAttributes, privacyUrl: links.privacyUrl, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, }, }); }; Mailer.prototype.postAddRecoveryPhoneEmail = function (message) { const { maskedLastFourPhoneNumber } = message; log.trace('mailer.postAddRecoveryPhoneEmail', { email: message.email, uid: message.uid, maskedLastFourPhoneNumber, }); const templateName = 'postAddRecoveryPhone'; const links = this._generateSettingLinks(message, templateName); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, template: templateName, templateValues: { date, device: this._formatUserAgentInfo(message), privacyUrl: links.privacyUrl, link: links.link, // TODO, get actual last 4 when functionality is implemented (FXA-10370) /* Note that currently, we cannot define new EJS variables in our plain * text files without extra overhead of parsing these out before EJS * rendering, and adding them to our templateValues for Fluent. Because * of this, for now, we'll pass the variable with the bulleted mask * instead of handling the bulleted mask in the template itself. */ maskedLastFourPhoneNumber, resetLink: links.resetLink, resetLinkAttributes: links.resetLinkAttributes, supportLinkAttributes: links.supportLinkAttributes, // TODO, update this with proper #heading once it's written and add to links obj w/ // UTM parms, tests, & ensure Storybook is updated as well, FXA-10918 twoFactorSupportLink: 'https://support.mozilla.org/kb/secure-mozilla-account-two-step-authentication', supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.postChangeRecoveryPhoneEmail = function (message) { const templateName = 'postChangeRecoveryPhone'; const links = this._generateLinks( this.initiatePasswordResetUrl, message, {}, templateName ); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Link': links.resetLink, }; return this.send({ ...message, headers, template: templateName, templateValues: { date, device: this._formatUserAgentInfo(message), privacyUrl: links.privacyUrl, resetLink: links.resetLink, resetLinkAttributes: links.resetLinkAttributes, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.postRemoveRecoveryPhoneEmail = function (message) { const templateName = 'postRemoveRecoveryPhone'; const links = this._generateLinks( this.initiatePasswordResetUrl, message, {}, templateName ); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, template: templateName, templateValues: { date, device: this._formatUserAgentInfo(message), privacyUrl: links.privacyUrl, resetLink: links.resetLink, resetLinkAttributes: links.resetLinkAttributes, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.postSigninRecoveryPhoneEmail = function (message) { const templateName = 'postSigninRecoveryPhone'; const links = this._generateSettingLinks(message, templateName); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, template: templateName, templateValues: { date, device: this._formatUserAgentInfo(message), privacyUrl: links.privacyUrl, link: links.link, resetLink: links.resetLink, resetLinkAttributes: links.resetLinkAttributes, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.postSigninRecoveryCodeEmail = function (message) { const templateName = 'postSigninRecoveryCode'; const links = this._generateSettingLinks(message, templateName); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, template: templateName, templateValues: { date, device: this._formatUserAgentInfo(message), privacyUrl: links.privacyUrl, link: links.link, resetLink: links.resetLink, resetLinkAttributes: links.resetLinkAttributes, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.postAddAccountRecoveryEmail = function (message) { log.trace('mailer.postAddAccountRecoveryEmail', { email: message.email, uid: message.uid, }); const templateName = 'postAddAccountRecovery'; const links = this._generateSettingLinks(message, templateName); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, template: templateName, templateValues: { androidLink: links.androidLink, date, device: this._formatUserAgentInfo(message), email: message.email, iosLink: links.iosLink, link: links.link, passwordChangeLink: links.passwordChangeLink, passwordChangeLinkAttributes: links.passwordChangeLinkAttributes, privacyUrl: links.privacyUrl, revokeAccountRecoveryLink: links.revokeAccountRecoveryLink, revokeAccountRecoveryLinkAttributes: links.revokeAccountRecoveryLinkAttributes, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.postChangeAccountRecoveryEmail = function (message) { log.trace('mailer.postChangeAccountRecoveryEmail', { email: message.email, uid: message.uid, }); const templateName = 'postChangeAccountRecovery'; const links = this._generateSettingLinks(message, templateName); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, template: templateName, templateValues: { androidLink: links.androidLink, date, device: this._formatUserAgentInfo(message), email: message.email, iosLink: links.iosLink, link: links.link, passwordChangeLink: links.passwordChangeLink, passwordChangeLinkAttributes: links.passwordChangeLinkAttributes, privacyUrl: links.privacyUrl, revokeAccountRecoveryLink: links.revokeAccountRecoveryLink, revokeAccountRecoveryLinkAttributes: links.revokeAccountRecoveryLinkAttributes, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.postRemoveAccountRecoveryEmail = function (message) { log.trace('mailer.postRemoveAccountRecoveryEmail', { email: message.email, uid: message.uid, }); const templateName = 'postRemoveAccountRecovery'; const links = this._generateSettingLinks(message, templateName); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, template: templateName, templateValues: { androidLink: links.androidLink, date, device: this._formatUserAgentInfo(message), email: message.email, iosLink: links.iosLink, link: links.link, passwordChangeLink: links.passwordChangeLink, passwordChangeLinkAttributes: links.passwordChangeLinkAttributes, privacyUrl: links.privacyUrl, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.passwordResetAccountRecoveryEmail = function (message) { log.trace('mailer.passwordResetAccountRecoveryEmail', { email: message.email, uid: message.uid, }); const templateName = 'passwordResetAccountRecovery'; const links = this._generateSettingLinks(message, templateName); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, template: templateName, templateValues: { androidUrl: links.androidLink, date, device: this._formatUserAgentInfo(message), email: message.email, iosUrl: links.iosLink, link: links.link, privacyUrl: links.privacyUrl, productName: 'Firefox', passwordChangeLink: links.passwordChangeLink, passwordChangeLinkAttributes: links.passwordChangeLinkAttributes, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.passwordResetWithRecoveryKeyPromptEmail = function ( message ) { const templateName = 'passwordResetWithRecoveryKeyPrompt'; const links = this._generateCreateAccountRecoveryLinks( message, templateName ); const [time, date] = this._constructLocalTimeString( message.timeZone, message.acceptLanguage ); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, template: templateName, templateValues: { date, device: this._formatUserAgentInfo(message), email: message.email, link: links.link, privacyUrl: links.privacyUrl, productName: 'Firefox', passwordChangeLink: links.passwordChangeLink, passwordChangeLinkAttributes: links.passwordChangeLinkAttributes, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, time, }, }); }; Mailer.prototype.subscriptionAccountFinishSetupEmail = async function ( message ) { const { email, uid, productId, productName, invoiceNumber, invoiceTotalInCents, invoiceTotalCurrency, planEmailIconURL, invoiceDate, nextInvoiceDate, token, flowId, flowBeginTime, deviceId, } = message; const enabled = config.subscriptions.transactionalEmails.enabled; log.trace('mailer.subscriptionAccountFinishSetupEmail', { enabled, email, productId, uid, }); if (!enabled) { return; } const query = { email, product_name: productName, token, product_id: productId, flowId, flowBeginTime, deviceId, }; const template = 'subscriptionAccountFinishSetup'; const links = this._generateLinks( this.accountFinishSetupUrl, message, query, template ); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, layout: 'subscription', template, templateValues: { ...links, uid, email, productName, invoiceNumber, invoiceTotal: this._getLocalizedCurrencyString( invoiceTotalInCents, invoiceTotalCurrency, message.acceptLanguage ), invoiceDateOnly: this._constructLocalDateString( message.timeZone, message.acceptLanguage, invoiceDate ), isFinishSetup: true, nextInvoiceDateOnly: this._constructLocalDateString( message.timeZone, message.acceptLanguage, nextInvoiceDate ), icon: planEmailIconURL, product: productName, }, }); }; Mailer.prototype.subscriptionReplacedEmail = async function (message) { const { email, uid, productId, planId, productName } = message; const enabled = config.subscriptions.transactionalEmails.enabled; log.trace('mailer.subscriptionReplaced', { enabled, email, productId, uid, }); if (!enabled) { return; } const query = { plan_id: planId, product_id: productId, uid }; const template = 'subscriptionReplaced'; const links = this._generateLinks(null, message, query, template); const headers = {}; return this.send({ ...message, headers, layout: 'subscription', template, templateValues: { ...links, uid, email, productName, }, }); }; Mailer.prototype.subscriptionUpgradeEmail = async function (message) { const { email, uid, productId, planId, productIconURLNew, productNameOld, productNameNew, paymentAmountOldInCents, paymentAmountOldCurrency, paymentAmountNewInCents, paymentAmountNewCurrency, paymentProratedInCents, paymentProratedCurrency, productPaymentCycleNew, productPaymentCycleOld, } = message; const enabled = config.subscriptions.transactionalEmails.enabled; log.trace('mailer.subscriptionUpgrade', { enabled, email, productId, uid }); if (!enabled) { return; } const query = { plan_id: planId, product_id: productId, uid }; const template = 'subscriptionUpgrade'; const links = this._generateLinks(null, message, query, template); const headers = {}; return this.send({ ...message, headers, layout: 'subscription', template, templateValues: { ...links, uid, email, productIconURLNew, productName: productNameNew, productNameOld, paymentAmountOld: this._getLocalizedCurrencyString( paymentAmountOldInCents, paymentAmountOldCurrency, message.acceptLanguage ), paymentAmountNew: this._getLocalizedCurrencyString( paymentAmountNewInCents, paymentAmountNewCurrency, message.acceptLanguage ), paymentProrated: this._getLocalizedCurrencyString( paymentProratedInCents, paymentProratedCurrency, message.acceptLanguage ), productPaymentCycleNew, productPaymentCycleOld, icon: productIconURLNew, }, }); }; Mailer.prototype.subscriptionDowngradeEmail = async function (message) { const { email, uid, productId, planId, productIconURLNew, productIconURLOld, productNameOld, productNameNew, paymentAmountOldInCents, paymentAmountOldCurrency, paymentAmountNewInCents, paymentAmountNewCurrency, paymentProratedInCents, paymentProratedCurrency, productPaymentCycleNew, productPaymentCycleOld, } = message; const enabled = config.subscriptions.transactionalEmails.enabled; log.trace('mailer.subscriptionDowngrade', { enabled, email, productId, uid, }); if (!enabled) { return; } const query = { plan_id: planId, product_id: productId, uid }; const template = 'subscriptionDowngrade'; const links = this._generateLinks(null, message, query, template); const headers = {}; return this.send({ ...message, headers, layout: 'subscription', template, templateValues: { ...links, uid, email, productIconURLNew, productIconURLOld, productName: productNameNew, productNameOld, paymentAmountOld: this._getLocalizedCurrencyString( paymentAmountOldInCents, paymentAmountOldCurrency, message.acceptLanguage ), paymentAmountNew: this._getLocalizedCurrencyString( paymentAmountNewInCents, paymentAmountNewCurrency, message.acceptLanguage ), paymentProrated: this._getLocalizedCurrencyString( paymentProratedInCents, paymentProratedCurrency, message.acceptLanguage ), productPaymentCycleNew, productPaymentCycleOld, icon: productIconURLNew, }, }); }; Mailer.prototype.subscriptionPaymentExpiredEmail = async function (message) { const { email, uid, subscriptions } = message; const enabled = config.subscriptions.transactionalEmails.enabled; log.trace('mailer.subscriptionPaymentExpired', { enabled, email, uid, }); if (!enabled) { return; } const headers = {}; let productName; let template = 'subscriptionPaymentExpired'; let links = {}; if (subscriptions.length === 1) { productName = subscriptions[0].productName; links = this._generateLinks( null, message, { plan_id: subscriptions[0].planId, product_id: subscriptions[0].productId, uid, }, template ); } else { template = 'subscriptionsPaymentExpired'; links = this._generateLinks(null, message, {}, template); } return this.send({ ...message, headers, layout: 'subscription', template, templateValues: { ...links, uid, email, subscriptions, productName, }, }); }; Mailer.prototype.subscriptionPaymentProviderCancelledEmail = async function ( message ) { const { email, uid, subscriptions } = message; const enabled = config.subscriptions.transactionalEmails.enabled; log.trace('mailer.subscriptionPaymentProviderCancelled', { enabled, email, uid, }); if (!enabled) { return; } const headers = {}; let productName; let template = 'subscriptionPaymentProviderCancelled'; let links = {}; if (subscriptions.length === 1) { productName = subscriptions[0].productName; links = this._generateLinks( null, message, { plan_id: subscriptions[0].planId, product_id: subscriptions[0].productId, uid, }, template ); } else { template = 'subscriptionsPaymentProviderCancelled'; links = this._generateLinks(null, message, {}, template); } return this.send({ ...message, headers, layout: 'subscription', template, templateValues: { ...links, uid, email, subscriptions, productName, }, }); }; Mailer.prototype.subscriptionPaymentFailedEmail = async function (message) { const { email, uid, productId, planId, planEmailIconURL, productName } = message; const enabled = config.subscriptions.transactionalEmails.enabled; log.trace('mailer.subscriptionPaymentFailed', { enabled, email, productId, uid, }); if (!enabled) { return; } const query = { plan_id: planId, product_id: productId, uid }; const template = 'subscriptionPaymentFailed'; const links = this._generateLinks(null, message, query, template); const headers = {}; return this.send({ ...message, headers, layout: 'subscription', template, templateValues: { ...links, productName, uid, email, icon: planEmailIconURL, product: productName, }, }); }; Mailer.prototype.subscriptionAccountDeletionEmail = async function (message) { const { email, uid, productId, planId, planEmailIconURL, productName, invoiceDate, invoiceTotalInCents, invoiceTotalCurrency, } = message; const enabled = config.subscriptions.transactionalEmails.enabled; log.trace('mailer.subscriptionAccountDeletion', { enabled, email, productId, uid, }); if (!enabled) { return; } const query = { plan_id: planId, product_id: productId, uid }; const template = 'subscriptionAccountDeletion'; const links = this._generateLinks(null, message, query, template); const headers = {}; return this.send({ ...message, headers, layout: 'subscription', template, templateValues: { ...links, productName, uid, email, isCancellationEmail: true, invoiceDateOnly: this._constructLocalDateString( message.timeZone, message.acceptLanguage, invoiceDate ), icon: planEmailIconURL, invoiceTotal: this._getLocalizedCurrencyString( invoiceTotalInCents, invoiceTotalCurrency, message.acceptLanguage ), }, }); }; Mailer.prototype.subscriptionCancellationEmail = async function (message) { const { email, uid, productId, planId, planEmailIconURL, productName, invoiceDate, invoiceTotalInCents, invoiceTotalCurrency, serviceLastActiveDate, showOutstandingBalance, cancelAtEnd = true, } = message; const enabled = config.subscriptions.transactionalEmails.enabled; log.trace('mailer.subscriptionCancellation', { enabled, email, productId, uid, }); if (!enabled) { return; } const query = { plan_id: planId, product_id: productId, uid }; const template = 'subscriptionCancellation'; const links = this._generateLinks(null, message, query, template); const headers = {}; return this.send({ ...message, headers, layout: 'subscription', template, templateValues: { ...links, uid, email, isCancellationEmail: true, invoiceDateOnly: this._constructLocalDateString( message.timeZone, message.acceptLanguage, invoiceDate ), serviceLastActiveDateOnly: this._constructLocalDateString( message.timeZone, message.acceptLanguage, serviceLastActiveDate ), icon: planEmailIconURL, productName, invoiceTotal: this._getLocalizedCurrencyString( invoiceTotalInCents, invoiceTotalCurrency, message.acceptLanguage ), showOutstandingBalance, cancelAtEnd, }, }); }; Mailer.prototype.subscriptionFailedPaymentsCancellationEmail = async function (message) { const { email, uid, productId, planId, planEmailIconURL, productName } = message; const enabled = config.subscriptions.transactionalEmails.enabled; log.trace('mailer.subscriptionFailedPaymentsCancellation', { enabled, email, productId, uid, }); if (!enabled) { return; } const query = { plan_id: planId, product_id: productId, uid }; const template = 'subscriptionFailedPaymentsCancellation'; const links = this._generateLinks(null, message, query, template); const headers = {}; return this.send({ ...message, headers, layout: 'subscription', template, templateValues: { ...links, uid, email, isCancellationEmail: true, icon: planEmailIconURL, productName, }, }); }; Mailer.prototype.subscriptionReactivationEmail = async function (message) { const { email, uid, productId, planId, planEmailIconURL, productName, invoiceTotalInCents, invoiceTotalCurrency, cardType, lastFour, nextInvoiceDate, payment_provider, } = message; const enabled = config.subscriptions.transactionalEmails.enabled; log.trace('mailer.subscriptionReactivation', { enabled, email, productId, uid, }); if (!enabled) { return; } const query = { plan_id: planId, product_id: productId, uid }; const template = 'subscriptionReactivation'; const links = this._generateLinks(null, message, query, template); const headers = {}; return this.send({ ...message, headers, layout: 'subscription', template, templateValues: { ...links, uid, email, nextInvoiceDateOnly: this._constructLocalDateString( message.timeZone, message.acceptLanguage, nextInvoiceDate ), icon: planEmailIconURL, productName, invoiceTotal: this._getLocalizedCurrencyString( invoiceTotalInCents, invoiceTotalCurrency, message.acceptLanguage ), payment_provider, cardType, cardName: cardTypeToText(cardType), lastFour, nextInvoiceDate, }, }); }; Mailer.prototype.subscriptionRenewalReminderEmail = async function (message) { const { email, uid, subscription } = message; const enabled = config.subscriptions.transactionalEmails.enabled; log.trace('mailer.subscriptionRenewalReminderEmail', { enabled, email, uid, }); if (!enabled) { return; } const headers = {}; const template = 'subscriptionRenewalReminder'; const productName = subscription.productName; const links = this._generateLinks( null, message, { plan_id: subscription.planId, product_id: subscription.productId, uid, }, template ); return this.send({ ...message, headers, layout: 'subscription', template, templateValues: { ...links, uid, email, productName, reminderLength: message.reminderLength, planIntervalCount: message.planIntervalCount, planInterval: message.planInterval, invoiceTotal: this._getLocalizedCurrencyString( message.invoiceTotalInCents, message.invoiceTotalCurrency, message.acceptLanguage ), }, }); }; Mailer.prototype.subscriptionSubsequentInvoiceEmail = async function ( message ) { const { email, uid, productId, planId, planEmailIconURL, productName, invoiceLink, invoiceNumber, invoiceDate, invoiceTotalInCents, invoiceTotalCurrency, invoiceSubtotalInCents, invoiceDiscountAmountInCents, invoiceTaxAmountInCents, cardType, lastFour, nextInvoiceDate, payment_provider, paymentProratedInCents, paymentProratedCurrency, showPaymentMethod, showTaxAmount, discountType, discountDuration, } = message; const enabled = config.subscriptions.transactionalEmails.enabled; log.trace('mailer.subscriptionSubsequentInvoice', { enabled, email, productId, uid, }); if (!enabled) { return; } const query = { plan_id: planId, product_id: productId, uid }; const template = 'subscriptionSubsequentInvoice'; const links = this._generateLinks(null, message, query, template); const headers = {}; let paymentProrated; let showProratedAmount = false; if (typeof paymentProratedInCents !== 'undefined') { showProratedAmount = true; paymentProrated = this._getLocalizedCurrencyString( paymentProratedInCents, paymentProratedCurrency, message.acceptLanguage ); } return this.send({ ...message, headers, layout: 'subscription', template, templateValues: { ...links, uid, email, invoiceDateOnly: this._constructLocalDateString( message.timeZone, message.acceptLanguage, invoiceDate ), nextInvoiceDateOnly: this._constructLocalDateString( message.timeZone, message.acceptLanguage, nextInvoiceDate ), icon: planEmailIconURL, productName, invoiceLink, invoiceNumber, invoiceDate, invoiceTotal: this._getLocalizedCurrencyString( invoiceTotalInCents, invoiceTotalCurrency, message.acceptLanguage ), invoiceSubtotal: invoiceSubtotalInCents && this._getLocalizedCurrencyString( invoiceSubtotalInCents, invoiceTotalCurrency, message.acceptLanguage ), invoiceTaxAmount: invoiceTaxAmountInCents && this._getLocalizedCurrencyString( invoiceTaxAmountInCents, invoiceTotalCurrency, message.acceptLanguage ), invoiceDiscountAmount: invoiceDiscountAmountInCents && this._getLocalizedCurrencyString( invoiceDiscountAmountInCents, invoiceTotalCurrency, message.acceptLanguage ), payment_provider, cardType, cardName: cardTypeToText(cardType), lastFour, nextInvoiceDate, paymentProrated, showProratedAmount, showPaymentMethod, showTaxAmount, discountType, discountDuration, }, }); }; Mailer.prototype.subscriptionFirstInvoiceEmail = async function (message) { const { email, uid, productId, planId, planEmailIconURL, productName, invoiceNumber, invoiceDate, invoiceLink, invoiceTotalInCents, invoiceTotalCurrency, invoiceSubtotalInCents, invoiceDiscountAmountInCents, invoiceTaxAmountInCents, payment_provider, cardType, lastFour, nextInvoiceDate, showPaymentMethod, showTaxAmount, discountType, discountDuration, } = message; const enabled = config.subscriptions.transactionalEmails.enabled; log.trace('mailer.subscriptionFirstInvoice', { enabled, email, productId, uid, }); if (!enabled) { return; } const query = { plan_id: planId, product_id: productId, uid }; const template = 'subscriptionFirstInvoice'; const links = this._generateLinks(null, message, query, template); const headers = {}; return this.send({ ...message, headers, layout: 'subscription', template, templateValues: { ...links, uid, email, invoiceDateOnly: this._constructLocalDateString( message.timeZone, message.acceptLanguage, invoiceDate ), nextInvoiceDateOnly: this._constructLocalDateString( message.timeZone, message.acceptLanguage, nextInvoiceDate ), icon: planEmailIconURL, productName, invoiceLink, invoiceNumber, invoiceDate, invoiceTotal: this._getLocalizedCurrencyString( invoiceTotalInCents, invoiceTotalCurrency, message.acceptLanguage ), invoiceSubtotal: invoiceSubtotalInCents && this._getLocalizedCurrencyString( invoiceSubtotalInCents, invoiceTotalCurrency, message.acceptLanguage ), invoiceTaxAmount: invoiceTaxAmountInCents && this._getLocalizedCurrencyString( invoiceTaxAmountInCents, invoiceTotalCurrency, message.acceptLanguage ), invoiceDiscountAmount: invoiceDiscountAmountInCents && this._getLocalizedCurrencyString( invoiceDiscountAmountInCents, invoiceTotalCurrency, message.acceptLanguage ), payment_provider, cardType, cardName: cardTypeToText(cardType), lastFour, nextInvoiceDate, showPaymentMethod, showTaxAmount, showProratedAmount: false, discountType, discountDuration, }, }); }; Mailer.prototype.downloadSubscriptionEmail = async function (message) { const { email, productId, planId, productName, planEmailIconURL, planSuccessActionButtonURL, uid, appStoreLink, playStoreLink, } = message; log.trace('mailer.downloadSubscription', { email, productId, uid }); const query = { plan_id: planId, product_id: productId, uid }; const template = 'downloadSubscription'; const links = this._generateLinks( planSuccessActionButtonURL, message, query, template, appStoreLink, playStoreLink ); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, layout: 'subscription', template, templateValues: { ...links, productName, uid, email, icon: planEmailIconURL, }, }); }; Mailer.prototype.fraudulentAccountDeletionEmail = async function (message) { const { email, uid } = message; const enabled = config.subscriptions.transactionalEmails.enabled; log.trace('mailer.fraudulentAccountDeletion', { enabled, email, uid, }); if (!enabled) { return; } const query = { uid }; const template = 'fraudulentAccountDeletion'; const links = this._generateLinks(null, message, query, template); const headers = {}; return this.send({ ...message, headers, layout: 'subscription', template, templateValues: { ...links, mozillaSupportUrl: 'https://support.mozilla.org', uid, email, wasDeleted: true, }, }); }; Mailer.prototype._inactiveAccountWarningEmail = async function ( message, templateName ) { const deletionDate = this._constructLocalDateString( message.timezone, message.acceptLanguage, message.inactiveDeletionEta, 'dddd, ll' ); const links = this._generateLinks( this.accountSettingsUrl, message, {}, templateName ); const headers = { 'X-Link': links.link, }; return this.send({ ...message, ...links, headers, template: templateName, templateValues: { deletionDate, }, }); }; Mailer.prototype.inactiveAccountFirstWarningEmail = async function (message) { const templateName = 'inactiveAccountFirstWarning'; this._inactiveAccountWarningEmail(message, templateName); }; Mailer.prototype.inactiveAccountSecondWarningEmail = async function ( message ) { const templateName = 'inactiveAccountSecondWarning'; this._inactiveAccountWarningEmail(message, templateName); }; Mailer.prototype.inactiveAccountFinalWarningEmail = async function (message) { const templateName = 'inactiveAccountFinalWarning'; this._inactiveAccountWarningEmail(message, templateName); }; cadReminders.keys.forEach((key, index) => { // Template names are generated in the form `cadReminderFirstEmail`, // where `First` is the key derived from config, with an initial capital letter. const template = `cadReminder${key[0].toUpperCase()}${key.substr(1)}`; const query = {}; templateNameToCampaignMap[template] = `cad-reminder-${key}`; templateNameToContentMap[template] = 'connect-device'; Mailer.prototype[`${template}Email`] = async function (message) { const { code, email, uid } = message; log.trace(`mailer.${template}`, { code, email, uid }); const links = this._generateLinks(this.syncUrl, message, query, template); const headers = { 'X-Link': links.link, }; return this.send({ ...message, headers, template, templateValues: { androidLinkAttributes: linkAttributes(links.androidLink), androidUrl: links.androidLink, cadLinkAttributes: linkAttributes(links.link), iosLinkAttributes: linkAttributes(links.iosLink), iosUrl: links.iosLink, link: links.link, privacyUrl: links.privacyUrl, productName: 'Firefox', style: message.style, supportLinkAttributes: links.supportLinkAttributes, supportUrl: links.supportUrl, }, }); }; }); Mailer.prototype._legalDocsRedirectUrl = function (url) { return `${paymentsServerURL.origin}/legal-docs?url=${encodeURIComponent( url )}`; }; Mailer.prototype._generateUTMLink = function ( link, query, templateName, content ) { const parsedLink = new URL(link); Object.keys(query).forEach((key) => { const value = typeof query[key] !== 'undefined' ? query[key] : ''; parsedLink.searchParams.set(key, value); }); if (this.metricsEnabled) { parsedLink.searchParams.set('utm_medium', 'email'); const campaign = templateNameToCampaignMap[templateName]; if (campaign && !parsedLink.searchParams.has('utm_campaign')) { parsedLink.searchParams.set('utm_campaign', UTM_PREFIX + campaign); } if (content) { parsedLink.searchParams.set('utm_content', UTM_PREFIX + content); } } const isAccountOrEmailVerification = link === this.verificationUrl || link === this.verifyLoginUrl; if ( this.prependVerificationSubdomain.enabled && isAccountOrEmailVerification ) { parsedLink.host = `${this.prependVerificationSubdomain.subdomain}.${parsedLink.host}`; } return parsedLink.toString(); }; Mailer.prototype._generateLinks = function ( primaryLink, message, query, templateName, appStoreLink, playStoreLink ) { const { email, uid, metricsEnabled } = message; // set this to avoid passing `metricsEnabled` around to all link functions this.metricsEnabled = metricsEnabled; const localizedUrls = (message) => { const defaultSureyUrl = 'https://survey.alchemer.com/s3/6534408/Privacy-Security-Product-Cancellation-of-Service-Q4-21'; const urls = {}; if ( config.subscriptions.productConfigsFirestore.enabled && message.planConfig ) { // we are not using `determineLocale` because the product config might support more locales than the FxA supported locales list const locales = message.acceptLanguage ? [message.acceptLanguage] : []; const localizedConfigs = localizedPlanConfig( message.planConfig, locales ); // the ToS and Privacy Notice URLs are actually not localized in the product config; the redirect endpoint on the payments server does that. but we do need it in the urls object so we can overwrite the metadata ones with the Firestore ones. // eslint did not like ??= localizedConfigs['urls'] ?? (localizedConfigs['urls'] = {}); const urlKeys = { termsOfServiceDownloadURL: 'termsOfServiceDownload', privacyNoticeDownloadURL: 'privacyNoticeDownload', cancellationSurveyUrl: 'cancellationSurvey', }; Object.entries(urlKeys).forEach(([urlKey, configKey]) => { if (localizedConfigs.urls[configKey]) { urls[urlKey] = localizedConfigs.urls[configKey]; } }); } const cancellationSurveyUrl = (message.productMetadata && message.productMetadata['product:cancellationSurveyURL']) || defaultSureyUrl; return { ...productDetailsFromPlan( { product_metadata: message.productMetadata || message.subscription?.productMetadata, }, determineLocale(message.acceptLanguage) ), cancellationSurveyUrl, ...urls, }; }; const { termsOfServiceDownloadURL = this.subscriptionTermsUrl, privacyNoticeDownloadURL = this.privacyUrl, cancellationSurveyUrl, } = localizedUrls(message); // Generate all possible links. The option to use a specific link // is left up to the template. const links = {}; const utmContent = templateNameToContentMap[templateName]; if (primaryLink && utmContent) { links['link'] = this._generateUTMLink( primaryLink, query, templateName, utmContent ); } if (appStoreLink && utmContent) { links['appStoreLink'] = this._generateUTMLink( appStoreLink, query, templateName, utmContent ); } if (playStoreLink && utmContent) { links['playStoreLink'] = this._generateUTMLink( playStoreLink, query, templateName, utmContent ); } links['privacyUrl'] = this.createPrivacyLink(templateName); links['supportLinkAttributes'] = this._supportLinkAttributes(templateName); links['supportUrl'] = this.createSupportLink(templateName); links['subscriptionSupportUrl'] = this._generateUTMLink( this.subscriptionSupportUrl, {}, templateName, 'subscription-support' ); links['passwordChangeLink'] = this.createPasswordChangeLink( email, templateName ); links['passwordChangeLinkAttributes'] = this._passwordChangeLinkAttributes( email, templateName ); links['resetLink'] = this.createPasswordResetLink( email, templateName, query.emailToHashWith ); links['resetLinkAttributes'] = this._passwordResetLinkAttributes( email, templateName, query.emailToHashWith ); links['androidLink'] = this._generateUTMLink( this.androidUrl, query, templateName, 'connect-android' ); links['iosLink'] = this._generateUTMLink( this.iosUrl, query, templateName, 'connect-ios' ); links['passwordManagerInfoUrl'] = this._generateUTMLink( this.passwordManagerInfoUrl, query, templateName, 'password-info' ); links['reportSignInLink'] = this.createReportSignInLink( templateName, query ); links['reportSignInLinkAttributes'] = this._reportSignInLinkAttributes( email, templateName, query ); links['revokeAccountRecoveryLink'] = this.createRevokeAccountRecoveryLink(templateName); links['revokeAccountRecoveryLinkAttributes'] = this._revokeAccountRecoveryLinkAttributes(templateName); links['createAccountRecoveryLink'] = this.createAccountRecoveryLink(templateName); links.accountSettingsUrl = this._generateUTMLink( this.accountSettingsUrl, { ...query, email, uid }, templateName, 'account-settings' ); links.accountSettingsLinkAttributes = `href="${links.accountSettingsUrl}" target="_blank" rel="noopener noreferrer" style="color:#ffffff;font-weight:500;"`; links.cancellationSurveyUrl = cancellationSurveyUrl; links.cancellationSurveyLinkAttributes = `href="${links.cancellationSurveyUrl}" style="text-decoration: none; color: #0060DF;"`; links.subscriptionTermsUrl = this._legalDocsRedirectUrl( this._generateUTMLink( termsOfServiceDownloadURL, {}, templateName, 'subscription-terms' ) ); links.subscriptionPrivacyUrl = this._legalDocsRedirectUrl( this._generateUTMLink( privacyNoticeDownloadURL, {}, templateName, 'subscription-privacy' ) ); links.cancelSubscriptionUrl = this._generateUTMLink( this.subscriptionSettingsUrl, { ...query, email, uid }, templateName, 'cancel-subscription' ); links.reactivateSubscriptionUrl = this._generateUTMLink( this.subscriptionSettingsUrl, { ...query, email, uid }, templateName, 'reactivate-subscription' ); links.updateBillingUrl = this._generateUTMLink( this.subscriptionSettingsUrl, { ...query, email, uid }, templateName, 'update-billing' ); links.unsubscribeUrl = this.unsubscribeUrl; const queryOneClick = extend(query, { one_click: true }); if (primaryLink && utmContent) { links['oneClickLink'] = this._generateUTMLink( primaryLink, queryOneClick, templateName, `${utmContent}-oneclick` ); } return links; }; Mailer.prototype._generateSettingLinks = function ( message, templateName, link = this.accountSettingsUrl ) { // Generate all possible links where the primary link is `accountSettingsUrl`. const query = {}; if (message.email) { query.email = message.email; } if (message.uid) { query.uid = message.uid; } return this._generateLinks(link, message, query, templateName); }; Mailer.prototype._generateLowRecoveryCodesLinks = function ( message, templateName ) { // Generate all possible links where the primary link is `accountRecoveryCodesUrl`. const query = { low_recovery_codes: true }; if (message.email) { query.email = message.email; } if (message.uid) { query.uid = message.uid; } return this._generateLinks( this.accountRecoveryCodesUrl, message, query, templateName ); }; Mailer.prototype._generateCreateAccountRecoveryLinks = function ( message, templateName ) { // Generate all possible links where the primary link is `createAccountRecoveryUrl`. const query = {}; if (message.email) { query.email = message.email; } if (message.uid) { query.uid = message.uid; } return this._generateLinks( this.createAccountRecoveryUrl, message, query, templateName ); }; Mailer.prototype.createPasswordResetLink = function ( email, templateName, emailToHashWith ) { // Default `reset_password_confirm` to false, to show warnings about // resetting password and sync data const query = { email: email, reset_password_confirm: false, email_to_hash_with: emailToHashWith, }; return this._generateUTMLink( this.initiatePasswordResetUrl, query, templateName, 'reset-password' ); }; Mailer.prototype.createPasswordChangeLink = function (email, templateName) { const query = { email: email }; return this._generateUTMLink( this.initiatePasswordChangeUrl, query, templateName, 'change-password' ); }; Mailer.prototype.createReportSignInLink = function (templateName, data) { const query = { uid: data.uid, unblockCode: data.unblockCode, }; return this._generateUTMLink( this.reportSignInUrl, query, templateName, 'report' ); }; Mailer.prototype._reportSignInLinkAttributes = function ( email, templateName, query ) { return linkAttributes(this.createReportSignInLink(templateName, query)); }; Mailer.prototype.createSupportLink = function (templateName) { return this._generateUTMLink(this.supportUrl, {}, templateName, 'support'); }; Mailer.prototype.createPrivacyLink = function (templateName) { return this._generateUTMLink(this.privacyUrl, {}, templateName, 'privacy'); }; Mailer.prototype.createRevokeAccountRecoveryLink = function (templateName) { return this._generateUTMLink( this.revokeAccountRecoveryUrl, {}, templateName, 'report' ); }; Mailer.prototype._revokeAccountRecoveryLinkAttributes = function ( templateName ) { return linkAttributes(this.createRevokeAccountRecoveryLink(templateName)); }; Mailer.prototype.createAccountRecoveryLink = function (templateName) { return this._generateUTMLink( this.createAccountRecoveryUrl, {}, templateName ); }; return Mailer; }; function optionalHeader(key, value) { if (value) { return { [key]: value }; } }