gmail-to-exchange365
Version:
Complete Gmail to Exchange 365 migration tool with UI - Migrate emails, attachments, and folders seamlessly
187 lines (152 loc) • 5.59 kB
text/typescript
import { fetchAllEmails, fetchGmailLabels } from "./gmailFetcher";
import { uploadEmail, getGraphClient } from "./exchangePusher";
import { mapGmailLabelsToFolders, getFolderIdForLabel } from "./folderMapper";
import { EmailMessage, MigrationProgress } from "./types";
interface GmailToken {
access_token: string;
refresh_token?: string;
expiry_date?: number;
}
interface MSToken {
access_token: string;
refresh_token?: string;
expires_in?: number;
}
export interface MigrationOptions {
batchSize?: number;
delayBetweenBatches?: number;
retryAttempts?: number;
retryDelay?: number;
}
export class Migrator {
private progress: MigrationProgress = {
current: 0,
total: 0,
status: 'idle'
};
private onProgressCallback?: (progress: MigrationProgress) => void;
private shouldPause: boolean = false;
private shouldStop: boolean = false;
constructor(
private gToken: GmailToken,
private msToken: MSToken,
private options: MigrationOptions = {}
) {
this.options = {
batchSize: options.batchSize || 10,
delayBetweenBatches: options.delayBetweenBatches || 1000,
retryAttempts: options.retryAttempts || 3,
retryDelay: options.retryDelay || 5000
};
}
onProgress(callback: (progress: MigrationProgress) => void) {
this.onProgressCallback = callback;
}
pause() {
this.shouldPause = true;
this.progress.status = 'paused';
this.notifyProgress();
}
resume() {
this.shouldPause = false;
this.progress.status = 'running';
this.notifyProgress();
}
stop() {
this.shouldStop = true;
this.progress.status = 'idle';
this.notifyProgress();
}
private notifyProgress() {
if (this.onProgressCallback) {
this.onProgressCallback({ ...this.progress });
}
}
private async waitIfPaused() {
while (this.shouldPause && !this.shouldStop) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
async migrate(): Promise<{ total: number; successful: number; failed: number }> {
this.progress.status = 'running';
this.progress.current = 0;
this.shouldStop = false;
this.shouldPause = false;
try {
this.progress.message = "Fetching emails from Gmail...";
this.notifyProgress();
const emails = await fetchAllEmails(this.gToken);
this.progress.total = emails.length;
this.progress.message = `Found ${emails.length} emails. Starting migration...`;
this.notifyProgress();
const client = getGraphClient(this.msToken);
// Fetch and map folders
const labels = await fetchGmailLabels(this.gToken);
const folderMappings = await mapGmailLabelsToFolders(client, labels);
let successful = 0;
let failed = 0;
const batchSize = this.options.batchSize!;
for (let i = 0; i < emails.length; i += batchSize) {
if (this.shouldStop) {
this.progress.message = "Migration stopped by user";
this.progress.status = 'idle';
this.notifyProgress();
break;
}
await this.waitIfPaused();
const batch = emails.slice(i, i + batchSize);
for (const email of batch) {
if (this.shouldStop) break;
await this.waitIfPaused();
const folderId = getFolderIdForLabel(folderMappings, email.labels || []);
let retries = 0;
let success = false;
while (retries < this.options.retryAttempts! && !success) {
try {
await uploadEmail(client, email, folderId);
successful++;
success = true;
} catch (error: any) {
retries++;
if (retries >= this.options.retryAttempts!) {
console.error(`Failed to migrate email ${email.id} after ${retries} attempts:`, error.message);
failed++;
} else {
await new Promise(resolve => setTimeout(resolve, this.options.retryDelay!));
}
}
}
this.progress.current = i + batch.indexOf(email) + 1;
this.progress.message = `Migrated ${this.progress.current}/${this.progress.total} emails`;
this.notifyProgress();
}
// Delay between batches to avoid rate limiting
if (i + batchSize < emails.length) {
await new Promise(resolve => setTimeout(resolve, this.options.delayBetweenBatches!));
}
}
this.progress.status = 'completed';
this.progress.message = `Migration completed! ${successful} successful, ${failed} failed`;
this.notifyProgress();
return { total: emails.length, successful, failed };
} catch (error: any) {
this.progress.status = 'error';
this.progress.error = error.message;
this.progress.message = `Migration failed: ${error.message}`;
this.notifyProgress();
throw error;
}
}
}
export async function migrateUser(
gToken: GmailToken,
msToken: MSToken,
onProgress: (current: number, total: number, message?: string) => void,
options?: MigrationOptions
): Promise<{ total: number; successful: number; failed: number }> {
const migrator = new Migrator(gToken, msToken, options);
migrator.onProgress((progress) => {
onProgress(progress.current, progress.total, progress.message);
});
return await migrator.migrate();
}