migrate-user-emails.mjs 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. // Script to migrate user emails using a CSV file with the following format:
  2. //
  3. // oldEmail,newEmail
  4. //
  5. // The script will iterate through the CSV file and update the user's email
  6. // address from oldEmail to newEmail, after checking all the email addresses
  7. // for duplicates.
  8. //
  9. // Intended for Server Pro customers migrating user emails from one domain to
  10. // another.
  11. import minimist from 'minimist'
  12. import os from 'os'
  13. import fs from 'fs'
  14. import * as csv from 'csv/sync'
  15. import EmailHelper from '../../../app/src/Features/Helpers/EmailHelper.mjs'
  16. import UserGetter from '../../../app/src/Features/User/UserGetter.mjs'
  17. import UserUpdater from '../../../app/src/Features/User/UserUpdater.mjs'
  18. import UserSessionsManager from '../../../app/src/Features/User/UserSessionsManager.mjs'
  19. const { parseEmail } = EmailHelper
  20. const hostname = os.hostname()
  21. const scriptTimestamp = new Date().toISOString()
  22. // support command line option of --commit to actually do the migration
  23. const argv = minimist(process.argv.slice(2), {
  24. boolean: ['commit', 'ignore-missing'],
  25. string: ['admin-id'],
  26. alias: {
  27. 'ignore-missing': 'continue',
  28. },
  29. default: {
  30. commit: false,
  31. 'ignore-missing': false,
  32. 'admin-id': '000000000000000000000000', // use a dummy admin ID for script audit log entries
  33. },
  34. })
  35. // display usage if no CSV file is provided
  36. if (argv._.length === 0) {
  37. console.log(
  38. 'Usage: node migrate_user_emails.mjs [--commit] [--continue|--ignore-missing] [--admin-id=ADMIN_USER_ID] <csv_file>'
  39. )
  40. console.log(' --commit: actually do the migration (default: false)')
  41. console.log(
  42. ' --continue|--ignore-missing: continue on missing or already-migrated users'
  43. )
  44. console.log(' --admin-id: admin user ID to use for audit log entries')
  45. console.log(' <csv_file>: CSV file with old and new email addresses')
  46. process.exit(1)
  47. }
  48. function filterEmails(rows) {
  49. // check that emails have a valid format
  50. const result = []
  51. const seenOld = new Set()
  52. const seenNew = new Set()
  53. for (const [oldEmail, newEmail] of rows) {
  54. const parsedOld = parseEmail(oldEmail)
  55. const parsedNew = parseEmail(newEmail)
  56. if (!parsedOld) {
  57. throw new Error(`invalid old email "${oldEmail}"`)
  58. }
  59. if (!parsedNew) {
  60. throw new Error(`invalid new email "${newEmail}"`)
  61. }
  62. // Check for duplicates and overlaps
  63. if (seenOld.has(parsedOld)) {
  64. throw new Error(`Duplicate old emails found in CSV file ${oldEmail}.`)
  65. }
  66. if (seenNew.has(parsedNew)) {
  67. throw new Error(`Duplicate new emails found in CSV file ${newEmail}.`)
  68. }
  69. if (seenOld.has(parsedNew) || seenNew.has(parsedOld)) {
  70. throw new Error(
  71. `Old and new emails cannot overlap ${oldEmail} ${newEmail}`
  72. )
  73. }
  74. seenOld.add(parsedOld)
  75. seenNew.add(parsedNew)
  76. result.push([parsedOld, parsedNew])
  77. }
  78. return result
  79. }
  80. async function checkEmailsAgainstDb(emails) {
  81. const result = []
  82. for (const [oldEmail, newEmail] of emails) {
  83. const userWithEmail = await UserGetter.promises.getUserByMainEmail(
  84. oldEmail,
  85. {
  86. _id: 1,
  87. }
  88. )
  89. if (!userWithEmail) {
  90. if (argv['ignore-missing']) {
  91. console.log(
  92. `User with email "${oldEmail}" not found, skipping update to "${newEmail}"`
  93. )
  94. continue
  95. } else {
  96. throw new Error(`no user found with email "${oldEmail}"`)
  97. }
  98. }
  99. const userWithNewEmail = await UserGetter.promises.getUserByAnyEmail(
  100. newEmail,
  101. {
  102. _id: 1,
  103. }
  104. )
  105. if (userWithNewEmail) {
  106. throw new Error(
  107. `new email "${newEmail}" already exists for user ${userWithNewEmail._id}`
  108. )
  109. }
  110. result.push([oldEmail, newEmail])
  111. }
  112. return result
  113. }
  114. async function doMigration(emails) {
  115. let success = 0
  116. let failure = 0
  117. let skipped = 0
  118. for (const [oldEmail, newEmail] of emails) {
  119. const userWithEmail = await UserGetter.promises.getUserByMainEmail(
  120. oldEmail,
  121. {
  122. _id: 1,
  123. }
  124. )
  125. if (!userWithEmail) {
  126. if (argv['ignore-missing']) {
  127. continue
  128. } else {
  129. throw new Error(`no user found with email "${oldEmail}"`)
  130. }
  131. }
  132. if (argv.commit) {
  133. console.log(
  134. `Updating user ${userWithEmail._id} email "${oldEmail}" to "${newEmail}"\n`
  135. )
  136. try {
  137. // log out all the user's sessions before changing the email address
  138. await UserSessionsManager.promises.removeSessionsFromRedis(
  139. userWithEmail
  140. )
  141. await UserUpdater.promises.migrateDefaultEmailAddress(
  142. userWithEmail._id,
  143. oldEmail,
  144. newEmail,
  145. {
  146. initiatorId: argv['admin-id'],
  147. ipAddress: hostname,
  148. extraInfo: {
  149. script: 'migrate_user_emails.js',
  150. runAt: scriptTimestamp,
  151. },
  152. }
  153. )
  154. success++
  155. } catch (err) {
  156. console.log(err)
  157. failure++
  158. }
  159. } else {
  160. console.log(`Dry run, skipping update from ${oldEmail} to ${newEmail}`)
  161. skipped++
  162. }
  163. }
  164. console.log('Success: ', success, 'Failure: ', failure, 'Skipped: ', skipped)
  165. if (failure > 0) {
  166. throw new Error('Some email migrations failed')
  167. }
  168. }
  169. async function migrateEmails() {
  170. console.log('Starting email migration script')
  171. const csvFilePath = argv._[0]
  172. const csvFile = fs.readFileSync(csvFilePath, 'utf8')
  173. const rows = csv.parse(csvFile)
  174. console.log('Number of users to migrate: ', rows.length)
  175. const emails = filterEmails(rows)
  176. const existingUserEmails = await checkEmailsAgainstDb(emails)
  177. await doMigration(existingUserEmails)
  178. }
  179. migrateEmails()
  180. .then(() => {
  181. console.log('Done.')
  182. process.exit(0)
  183. })
  184. .catch(error => {
  185. console.error(error)
  186. process.exit(1)
  187. })