diff --git a/services/web/scripts/check_removed_emails.mjs b/services/web/scripts/check_removed_emails.mjs new file mode 100644 index 0000000000..d9eb483e60 --- /dev/null +++ b/services/web/scripts/check_removed_emails.mjs @@ -0,0 +1,214 @@ +// @ts-check + +import { db, ObjectId } from '../app/src/infrastructure/mongodb.js' +import fs from 'node:fs/promises' +import * as csv from 'csv' +import { promisify } from 'node:util' +import { scriptRunner } from './lib/ScriptRunner.mjs' +import { READ_PREFERENCE_SECONDARY } from '@overleaf/mongo-utils/batchedUpdate.js' + +const CSV_FILENAME = '/tmp/unconfirmed_emails.csv' + +/** + * @type {(csvString: string) => Promise} + */ +const parseAsync = promisify(csv.parse) + +/** + * Checks the fallout of services/web/scripts/remove_unconfirmed_emails.mjs + * which wrongly removed some emails that have been confirmed by users + */ +async function main(trackProgress) { + console.time('check_removed_emails') + + const csvContent = await fs.readFile(CSV_FILENAME, 'utf8') + const rows = await parseAsync(csvContent) + rows.shift() // Remove header row + const emailsByUserId = {} + + for (const [userId, email] of rows) { + if (!emailsByUserId[userId]) { + emailsByUserId[userId] = [] + } + emailsByUserId[userId].push(email) + } + + const userIds = Object.keys(emailsByUserId) + let processedUsersCount = 0 + + const counts = { + /** @type {string[]} */ + userNotFound: [], + /** @type {string[]} */ + notDeleted: [], + deleted: 0, + /** @type {string[]} */ + wasConfirmed: [], + /** @type {string[]} */ + wasConfirmedLegacy: [], + /** @type {string[]} */ + madePrimary: [], + /** @type {string[]} */ + madeSecondary: [], + /** @type {string[]} */ + isPrimary: [], + /** @type {string[]} */ + isAddedAgain: [], + } + + console.log('Total emails in the CSV:', rows.length) + console.log('Total users in the CSV:', userIds.length) + + for (const userId of userIds) { + const userEmails = emailsByUserId[userId] + + const user = await db.users.findOne( + { _id: new ObjectId(userId) }, + { readPreference: READ_PREFERENCE_SECONDARY } + ) + + if (!user) { + counts.userNotFound.push(userId) + continue + } + + for (const email of userEmails) { + const deletionLog = await db.userAuditLogEntries.findOne( + { + userId: new ObjectId(userId), + operation: 'remove-email', + 'info.removedEmail': email, + 'info.note': 'remove unconfirmed secondary emails', + }, + { readPreference: READ_PREFERENCE_SECONDARY } + ) + if (!deletionLog) { + counts.notDeleted.push(email) + continue + } + counts.deleted++ + + if (user.email === email) { + counts.isPrimary.push(email) + } + + const confirmationLog = await db.userAuditLogEntries.findOne( + { + userId: new ObjectId(userId), + operation: 'confirm-email-via-code', + 'info.email': email, + timestamp: { $gt: new Date('2025-02-25') }, + }, + { readPreference: READ_PREFERENCE_SECONDARY } + ) + if (confirmationLog) { + counts.wasConfirmed.push(email) + } + + const confirmationLegacyLog = await db.userAuditLogEntries.findOne( + { + userId: new ObjectId(userId), + operation: 'confirm-email', + 'info.email': email, + timestamp: { $gt: new Date('2025-02-25') }, + }, + { readPreference: READ_PREFERENCE_SECONDARY } + ) + if (confirmationLegacyLog) { + counts.wasConfirmedLegacy.push(email) + } + + const madePrimaryLog = await db.userAuditLogEntries.findOne( + { + userId: new ObjectId(userId), + operation: 'change-primary-email', + 'info.newPrimaryEmail': email, + timestamp: { $gt: new Date('2025-02-25') }, + }, + { readPreference: READ_PREFERENCE_SECONDARY } + ) + if (madePrimaryLog) { + counts.madePrimary.push(email) + } + + const madeSecondaryLog = await db.userAuditLogEntries.findOne( + { + userId: new ObjectId(userId), + operation: 'change-primary-email', + 'info.oldPrimaryEmail': email, + timestamp: { $gt: new Date('2025-02-25') }, + }, + { readPreference: READ_PREFERENCE_SECONDARY } + ) + if (madeSecondaryLog) { + counts.madeSecondary.push(email) + } + + if (user.emails.some(item => item.email === email)) { + counts.isAddedAgain.push(email) + } + } + + processedUsersCount++ + if (processedUsersCount % 100 === 0) { + trackProgress(`Processed ${processedUsersCount} users`) + } + } + + console.log() + console.log('Total emails in the CSV:', rows.length) + console.log('Total users in the CSV:', userIds.length) + console.log('Total users processed:', processedUsersCount) + console.log() + console.log('Users not found:', JSON.stringify(counts.userNotFound)) + console.log() + console.log('Emails not deleted:', counts.notDeleted.length) + console.log('Emails deleted:', counts.deleted) + console.log() + console.log('Emails that were confirmed:', counts.wasConfirmed.length) + console.log( + 'Emails that were confirmed:', + JSON.stringify(counts.wasConfirmed) + ) + console.log() + console.log( + 'Emails that were confirmed (legacy):', + counts.wasConfirmedLegacy.length + ) + console.log( + 'Emails that were confirmed (legacy):', + JSON.stringify(counts.wasConfirmedLegacy) + ) + console.log() + console.log('Emails that are primary:', counts.isPrimary.length) + console.log('Emails that are primary:', JSON.stringify(counts.isPrimary)) + console.log() + console.log('Emails that were made primary:', counts.madePrimary.length) + console.log( + 'Emails that were made primary:', + JSON.stringify(counts.madePrimary) + ) + console.log() + console.log('Emails that were made secondary:', counts.madeSecondary.length) + console.log( + 'Emails that were made secondary:', + JSON.stringify(counts.madeSecondary) + ) + console.log() + console.log('Emails that were added again:', counts.isAddedAgain.length) + console.log( + 'Emails that were added again:', + JSON.stringify(counts.isAddedAgain) + ) + console.log() + console.timeEnd('check_removed_emails') + console.log() +} + +try { + await scriptRunner(main) + process.exit(0) +} catch (error) { + console.error(error) + process.exit(1) +}