migrate-user-emails.js 5.6 KB

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