Merge pull request #18021 from overleaf/rh-mailchimp-api

[web] Replace node-mailchimp with own MailChimpClient

GitOrigin-RevId: 10207620c48f30ad29f4f0e7ea5193c11d256902
This commit is contained in:
roo hutton 2024-04-22 08:09:01 +01:00 committed by Copybot
parent 9601fd097a
commit 06cac44d84
5 changed files with 78 additions and 140 deletions

131
package-lock.json generated
View file

@ -27908,78 +27908,6 @@
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
"dev": true "dev": true
}, },
"node_modules/mailchimp-api-v3": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/mailchimp-api-v3/-/mailchimp-api-v3-1.15.0.tgz",
"integrity": "sha512-9TxCFG+VRpl14HOHgABHYmC5GvpCY7LYqyTefOXd4GtI07oXCiJ7W5fEvk3SJKBctlbjhKbzjB5qOZMQpacEUQ==",
"dependencies": {
"bluebird": "^3.4.0",
"lodash": "^4.17.14",
"request": "^2.88.0",
"tar": "^4.0.2"
}
},
"node_modules/mailchimp-api-v3/node_modules/fs-minipass": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
"dependencies": {
"minipass": "^2.6.0"
}
},
"node_modules/mailchimp-api-v3/node_modules/minipass": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"dependencies": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
}
},
"node_modules/mailchimp-api-v3/node_modules/minizlib": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
"dependencies": {
"minipass": "^2.9.0"
}
},
"node_modules/mailchimp-api-v3/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/mailchimp-api-v3/node_modules/tar": {
"version": "4.4.19",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz",
"integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==",
"dependencies": {
"chownr": "^1.1.4",
"fs-minipass": "^1.2.7",
"minipass": "^2.9.0",
"minizlib": "^1.3.3",
"mkdirp": "^0.5.5",
"safe-buffer": "^5.2.1",
"yallist": "^3.1.1"
},
"engines": {
"node": ">=4.5"
}
},
"node_modules/make-dir": { "node_modules/make-dir": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
@ -43585,7 +43513,6 @@
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"lru-cache": "^7.10.1", "lru-cache": "^7.10.1",
"mailchimp-api-v3": "^1.12.0",
"marked": "^4.1.0", "marked": "^4.1.0",
"method-override": "^2.3.3", "method-override": "^2.3.3",
"minimatch": "^7.4.2", "minimatch": "^7.4.2",
@ -52263,7 +52190,6 @@
"less-loader": "^11.1.3", "less-loader": "^11.1.3",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"lru-cache": "^7.10.1", "lru-cache": "^7.10.1",
"mailchimp-api-v3": "^1.12.0",
"marked": "^4.1.0", "marked": "^4.1.0",
"match-sorter": "^6.2.0", "match-sorter": "^6.2.0",
"mathjax": "^3.2.2", "mathjax": "^3.2.2",
@ -69281,63 +69207,6 @@
} }
} }
}, },
"mailchimp-api-v3": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/mailchimp-api-v3/-/mailchimp-api-v3-1.15.0.tgz",
"integrity": "sha512-9TxCFG+VRpl14HOHgABHYmC5GvpCY7LYqyTefOXd4GtI07oXCiJ7W5fEvk3SJKBctlbjhKbzjB5qOZMQpacEUQ==",
"requires": {
"bluebird": "^3.4.0",
"lodash": "^4.17.14",
"request": "^2.88.0",
"tar": "^4.0.2"
},
"dependencies": {
"fs-minipass": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz",
"integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==",
"requires": {
"minipass": "^2.6.0"
}
},
"minipass": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz",
"integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==",
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
}
},
"minizlib": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz",
"integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==",
"requires": {
"minipass": "^2.9.0"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"tar": {
"version": "4.4.19",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz",
"integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==",
"requires": {
"chownr": "^1.1.4",
"fs-minipass": "^1.2.7",
"minipass": "^2.9.0",
"minizlib": "^1.3.3",
"mkdirp": "^0.5.5",
"safe-buffer": "^5.2.1",
"yallist": "^3.1.1"
}
}
}
},
"make-dir": { "make-dir": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",

View file

@ -0,0 +1,60 @@
const { fetchJson, fetchNothing } = require('@overleaf/fetch-utils')
class MailChimpClient {
constructor(apiKey) {
this.apiKey = apiKey
this.dc = apiKey.split('-')[1]
this.baseUrl = `https://${this.dc}.api.mailchimp.com/3.0/`
this.fetchOptions = {
method: 'GET',
basicAuth: {
user: 'any',
password: this.apiKey,
},
}
}
async request(path, options) {
try {
const requestUrl = `${this.baseUrl}${path}`
if (options.method === 'GET') {
return await fetchJson(requestUrl, options)
}
await fetchNothing(requestUrl, options)
} catch (err) {
// if there's a json body in the response, expose it in the error (for compatibility with node-mailchimp)
const errorBody = err.body ? JSON.parse(err.body) : {}
const errWithBody = Object.assign(err, errorBody)
throw errWithBody
}
}
async get(path) {
return await this.request(path, this.fetchOptions)
}
async put(path, body) {
const options = Object.assign({}, this.fetchOptions)
options.method = 'PUT'
options.json = body
return await this.request(path, options)
}
async delete(path) {
const options = Object.assign({}, this.fetchOptions)
options.method = 'DELETE'
return await this.request(path, options)
}
async patch(path, body) {
const options = Object.assign({}, this.fetchOptions)
options.method = 'PATCH'
options.json = body
return await this.request(path, options)
}
}
module.exports = MailChimpClient

View file

@ -1,9 +1,9 @@
const logger = require('@overleaf/logger') const logger = require('@overleaf/logger')
const Settings = require('@overleaf/settings') const Settings = require('@overleaf/settings')
const crypto = require('crypto') const crypto = require('crypto')
const Mailchimp = require('mailchimp-api-v3')
const OError = require('@overleaf/o-error') const OError = require('@overleaf/o-error')
const { callbackify } = require('util') const { callbackify } = require('util')
const MailChimpClient = require('./MailChimpClient')
function mailchimpIsConfigured() { function mailchimpIsConfigured() {
return Settings.mailchimp != null && Settings.mailchimp.api_key != null return Settings.mailchimp != null && Settings.mailchimp.api_key != null
@ -38,7 +38,7 @@ class NonFatalEmailUpdateError extends OError {
} }
function makeMailchimpProvider(listName, listId) { function makeMailchimpProvider(listName, listId) {
const mailchimp = new Mailchimp(Settings.mailchimp.api_key) const mailchimp = new MailChimpClient(Settings.mailchimp.api_key)
const MAILCHIMP_LIST_ID = listId const MAILCHIMP_LIST_ID = listId
return { return {
@ -54,7 +54,7 @@ function makeMailchimpProvider(listName, listId) {
const result = await mailchimp.get(path) const result = await mailchimp.get(path)
return result?.status === 'subscribed' return result?.status === 'subscribed'
} catch (err) { } catch (err) {
if (err.status === 404) { if (err?.response?.status === 404) {
return false return false
} }
throw OError.tag(err, 'error getting newsletter subscriptions status', { throw OError.tag(err, 'error getting newsletter subscriptions status', {
@ -101,7 +101,7 @@ function makeMailchimpProvider(listName, listId) {
'finished unsubscribing user from newsletter' 'finished unsubscribing user from newsletter'
) )
} catch (err) { } catch (err) {
if (err.status === 404 || err.status === 405) { if ([404, 405].includes(err?.response?.status)) {
// silently ignore users who were never subscribed (404) or previously deleted (405) // silently ignore users who were never subscribed (404) or previously deleted (405)
return return
} }

View file

@ -123,7 +123,6 @@
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"lru-cache": "^7.10.1", "lru-cache": "^7.10.1",
"mailchimp-api-v3": "^1.12.0",
"marked": "^4.1.0", "marked": "^4.1.0",
"method-override": "^2.3.3", "method-override": "^2.3.3",
"minimatch": "^7.4.2", "minimatch": "^7.4.2",

View file

@ -1,5 +1,6 @@
const { expect } = require('chai') const { expect } = require('chai')
const sinon = require('sinon') const sinon = require('sinon')
const { RequestFailedError } = require('@overleaf/fetch-utils')
const SandboxedModule = require('sandboxed-module') const SandboxedModule = require('sandboxed-module')
const MODULE_PATH = '../../../../app/src/Features/Newsletter/NewsletterManager' const MODULE_PATH = '../../../../app/src/Features/Newsletter/NewsletterManager'
@ -28,11 +29,15 @@ describe('NewsletterManager', function () {
this.NewsletterManager = SandboxedModule.require(MODULE_PATH, { this.NewsletterManager = SandboxedModule.require(MODULE_PATH, {
requires: { requires: {
'mailchimp-api-v3': this.Mailchimp, './MailChimpClient': this.Mailchimp,
'@overleaf/settings': this.Settings, '@overleaf/settings': this.Settings,
}, },
globals: { AbortController },
}).promises }).promises
this.NewsletterManager.get = sinon.stub()
this.NewsletterManager.delete = sinon.stub()
this.user = { this.user = {
_id: 'user_id', _id: 'user_id',
email: 'overleaf.duck@example.com', email: 'overleaf.duck@example.com',
@ -59,9 +64,14 @@ describe('NewsletterManager', function () {
}) })
it('returns false on 404', async function () { it('returns false on 404', async function () {
const err = new Error() this.mailchimp.get.rejects(
err.status = 404 new RequestFailedError(
this.mailchimp.get.rejects(err) 'http://some-url',
{},
{ status: 404 },
'Not found'
)
)
const subscribed = await this.NewsletterManager.subscribed(this.user) const subscribed = await this.NewsletterManager.subscribed(this.user)
expect(subscribed).to.be.false expect(subscribed).to.be.false
}) })