2019-08-28 08:59:41 -04:00
|
|
|
const { callbackify } = require('util')
|
2019-05-29 05:21:06 -04:00
|
|
|
const logger = require('logger-sharelatex')
|
|
|
|
const Settings = require('settings-sharelatex')
|
|
|
|
const crypto = require('crypto')
|
|
|
|
const Mailchimp = require('mailchimp-api-v3')
|
2019-08-28 08:59:41 -04:00
|
|
|
const OError = require('@overleaf/o-error')
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2019-08-28 08:59:41 -04:00
|
|
|
const provider = getProvider()
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
subscribe: callbackify(provider.subscribe),
|
|
|
|
unsubscribe: callbackify(provider.unsubscribe),
|
|
|
|
changeEmail: callbackify(provider.changeEmail),
|
|
|
|
promises: provider
|
|
|
|
}
|
|
|
|
|
2020-08-11 05:28:29 -04:00
|
|
|
class NonFatalEmailUpdateError extends OError {
|
|
|
|
constructor(message, oldEmail, newEmail) {
|
|
|
|
super(message, { oldEmail, newEmail })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-28 08:59:41 -04:00
|
|
|
function getProvider() {
|
|
|
|
if (mailchimpIsConfigured()) {
|
|
|
|
logger.info('Using newsletter provider: mailchimp')
|
|
|
|
return makeMailchimpProvider()
|
|
|
|
} else {
|
|
|
|
logger.info('Using newsletter provider: none')
|
|
|
|
return makeNullProvider()
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-28 08:59:41 -04:00
|
|
|
function mailchimpIsConfigured() {
|
|
|
|
return Settings.mailchimp != null && Settings.mailchimp.api_key != null
|
|
|
|
}
|
|
|
|
|
|
|
|
function makeMailchimpProvider() {
|
|
|
|
const mailchimp = new Mailchimp(Settings.mailchimp.api_key)
|
|
|
|
const MAILCHIMP_LIST_ID = Settings.mailchimp.list_id
|
|
|
|
|
|
|
|
return {
|
|
|
|
subscribe,
|
|
|
|
unsubscribe,
|
|
|
|
changeEmail
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2019-08-28 08:59:41 -04:00
|
|
|
async function subscribe(user) {
|
|
|
|
try {
|
|
|
|
const path = getSubscriberPath(user.email)
|
|
|
|
await mailchimp.put(path, {
|
|
|
|
email_address: user.email,
|
|
|
|
status: 'subscribed',
|
|
|
|
status_if_new: 'subscribed',
|
|
|
|
merge_fields: getMergeFields(user)
|
|
|
|
})
|
|
|
|
logger.info({ user }, 'finished subscribing user to newsletter')
|
|
|
|
} catch (err) {
|
2020-08-11 05:28:29 -04:00
|
|
|
throw OError.tag(err, 'error subscribing user to newsletter', {
|
|
|
|
userId: user._id
|
|
|
|
})
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-08-28 08:59:41 -04:00
|
|
|
}
|
|
|
|
|
2019-09-25 10:27:38 -04:00
|
|
|
async function unsubscribe(user, options = {}) {
|
2019-08-28 08:59:41 -04:00
|
|
|
try {
|
|
|
|
const path = getSubscriberPath(user.email)
|
2019-09-25 10:27:38 -04:00
|
|
|
if (options.delete) {
|
|
|
|
await mailchimp.delete(path)
|
|
|
|
} else {
|
|
|
|
await mailchimp.patch(path, {
|
|
|
|
status: 'unsubscribed',
|
|
|
|
merge_fields: getMergeFields(user)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
logger.info(
|
|
|
|
{ user, options },
|
|
|
|
'finished unsubscribing user from newsletter'
|
|
|
|
)
|
2019-08-28 08:59:41 -04:00
|
|
|
} catch (err) {
|
2019-09-25 10:27:38 -04:00
|
|
|
if (err.status === 404 || err.status === 405) {
|
|
|
|
// silently ignore users who were never subscribed (404) or previously deleted (405)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-08-28 08:59:41 -04:00
|
|
|
if (err.message.includes('looks fake or invalid')) {
|
|
|
|
logger.info(
|
2019-09-25 10:27:38 -04:00
|
|
|
{ err, user, options },
|
2019-08-28 08:59:41 -04:00
|
|
|
'Mailchimp declined to unsubscribe user because it finds the email looks fake'
|
|
|
|
)
|
2019-09-25 10:27:38 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-08-11 05:28:29 -04:00
|
|
|
throw OError.tag(err, 'error unsubscribing user from newsletter', {
|
|
|
|
userId: user._id
|
|
|
|
})
|
2019-09-25 10:27:38 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function changeEmail(user, newEmail) {
|
|
|
|
const oldEmail = user.email
|
|
|
|
|
|
|
|
try {
|
|
|
|
await updateEmailInMailchimp(user, newEmail)
|
|
|
|
} catch (updateError) {
|
|
|
|
// if we failed to update the user, delete their old email address so that
|
|
|
|
// we don't leave it stuck in mailchimp
|
|
|
|
logger.info(
|
|
|
|
{ oldEmail, newEmail, updateError },
|
|
|
|
'unable to change email in newsletter, removing old mail'
|
|
|
|
)
|
|
|
|
|
|
|
|
try {
|
|
|
|
await unsubscribe(user, { delete: true })
|
|
|
|
} catch (unsubscribeError) {
|
|
|
|
// something went wrong removing the user's address
|
2020-08-11 05:28:29 -04:00
|
|
|
throw OError.tag(
|
|
|
|
unsubscribeError,
|
|
|
|
'error unsubscribing old email in response to email change failure',
|
|
|
|
{ oldEmail, newEmail, updateError }
|
|
|
|
)
|
2019-09-25 10:27:38 -04:00
|
|
|
}
|
|
|
|
|
2020-08-11 05:28:29 -04:00
|
|
|
if (!(updateError instanceof NonFatalEmailUpdateError)) {
|
2019-09-25 10:27:38 -04:00
|
|
|
throw updateError
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
}
|
2019-08-28 08:59:41 -04:00
|
|
|
}
|
|
|
|
|
2019-09-25 10:27:38 -04:00
|
|
|
async function updateEmailInMailchimp(user, newEmail) {
|
|
|
|
const oldEmail = user.email
|
|
|
|
|
|
|
|
// mailchimp doesn't give us error codes, so we have to parse the message :'(
|
|
|
|
const errors = {
|
|
|
|
'merge fields were invalid': 'user has never subscribed',
|
|
|
|
'could not be validated':
|
|
|
|
'user has previously unsubscribed or new email already exist on list',
|
|
|
|
'is already a list member': 'new email is already on mailing list',
|
|
|
|
'looks fake or invalid': 'mail looks fake to mailchimp'
|
|
|
|
}
|
|
|
|
|
2019-08-28 08:59:41 -04:00
|
|
|
try {
|
|
|
|
const path = getSubscriberPath(oldEmail)
|
2019-09-25 10:27:38 -04:00
|
|
|
await mailchimp.patch(path, {
|
2019-08-28 08:59:41 -04:00
|
|
|
email_address: newEmail,
|
2019-09-25 10:27:38 -04:00
|
|
|
merge_fields: getMergeFields(user)
|
2019-08-28 08:59:41 -04:00
|
|
|
})
|
|
|
|
logger.info('finished changing email in the newsletter')
|
|
|
|
} catch (err) {
|
2019-09-25 10:27:38 -04:00
|
|
|
// silently ignore users who were never subscribed
|
|
|
|
if (err.status === 404) {
|
|
|
|
return
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-09-25 10:27:38 -04:00
|
|
|
|
|
|
|
// look through expected mailchimp errors and log if we find one
|
|
|
|
Object.keys(errors).forEach(key => {
|
|
|
|
if (err.message.includes(key)) {
|
|
|
|
const message = `unable to change email in newsletter, ${errors[key]}`
|
|
|
|
|
|
|
|
logger.info({ oldEmail, newEmail }, message)
|
|
|
|
|
2020-08-11 05:28:29 -04:00
|
|
|
throw new NonFatalEmailUpdateError(
|
|
|
|
message,
|
|
|
|
oldEmail,
|
|
|
|
newEmail
|
|
|
|
).withCause(err)
|
2019-09-25 10:27:38 -04:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// if we didn't find an expected error, generate something to throw
|
2020-08-11 05:28:29 -04:00
|
|
|
throw OError.tag(err, 'error changing email in newsletter', {
|
|
|
|
oldEmail,
|
|
|
|
newEmail
|
|
|
|
})
|
2019-08-28 08:59:41 -04:00
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
|
2019-08-28 08:59:41 -04:00
|
|
|
function getSubscriberPath(email) {
|
|
|
|
const emailHash = hashEmail(email)
|
|
|
|
return `/lists/${MAILCHIMP_LIST_ID}/members/${emailHash}`
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
|
2019-08-28 08:59:41 -04:00
|
|
|
function hashEmail(email) {
|
|
|
|
return crypto
|
|
|
|
.createHash('md5')
|
|
|
|
.update(email.toLowerCase())
|
|
|
|
.digest('hex')
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
|
2019-08-28 08:59:41 -04:00
|
|
|
function getMergeFields(user) {
|
|
|
|
return {
|
2019-05-29 05:21:06 -04:00
|
|
|
FNAME: user.first_name,
|
|
|
|
LNAME: user.last_name,
|
2019-08-28 08:59:41 -04:00
|
|
|
MONGO_ID: user._id.toString()
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-28 08:59:41 -04:00
|
|
|
function makeNullProvider() {
|
|
|
|
return {
|
|
|
|
subscribe,
|
|
|
|
unsubscribe,
|
|
|
|
changeEmail
|
|
|
|
}
|
|
|
|
|
|
|
|
async function subscribe(user) {
|
|
|
|
logger.info(
|
|
|
|
{ user },
|
|
|
|
'Not subscribing user to newsletter because no newsletter provider is configured'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
async function unsubscribe(user) {
|
|
|
|
logger.info(
|
|
|
|
{ user },
|
|
|
|
'Not unsubscribing user from newsletter because no newsletter provider is configured'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
async function changeEmail(oldEmail, newEmail) {
|
|
|
|
logger.info(
|
|
|
|
{ oldEmail, newEmail },
|
|
|
|
'Not changing email in newsletter for user because no newsletter provider is configured'
|
|
|
|
)
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|