package migration import ( "context" "fmt" "io" "net/http" "strings" "time" "github.com/jackc/pgx/v5/pgxpool" "github.com/ultisuite/ulti-backend/internal/mail/credentials" ) type progressUpdater func(status string, cursor, stats map[string]any, jobErr string) error type migrationUser struct { Email string ExternalID string Name string } func resolveMigrationUser(ctx context.Context, db *pgxpool.Pool, userID string) (migrationUser, error) { var u migrationUser err := db.QueryRow(ctx, ` SELECT COALESCE(email, ''), COALESCE(external_id, ''), COALESCE(name, '') FROM users WHERE id = $1::uuid `, userID).Scan(&u.Email, &u.ExternalID, &u.Name) if err != nil { return migrationUser{}, fmt.Errorf("migration user not found") } if u.Email == "" { return migrationUser{}, fmt.Errorf("migration user email missing") } return u, nil } func migrationHTTPClient() *http.Client { return &http.Client{Timeout: 90 * time.Second} } func apiGet(ctx context.Context, client *http.Client, url, accessToken string) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+accessToken) resp, err := migrationDo(ctx, client, req) if err != nil { return nil, err } defer resp.Body.Close() return io.ReadAll(resp.Body) } func alreadyImported(store *ImportedItemStore, id string) bool { if store == nil { return false } return store.Has(id) } func calendarSyncTokens(cursor map[string]any) map[string]string { raw, _ := cursor["calendarSyncTokens"].(map[string]any) out := make(map[string]string, len(raw)) for k, v := range raw { if s, ok := v.(string); ok && s != "" { out[k] = s } } return out } func setCalendarSyncToken(cursor map[string]any, calID, token string) { if calID == "" || token == "" { return } raw, _ := cursor["calendarSyncTokens"].(map[string]any) if raw == nil { raw = map[string]any{} cursor["calendarSyncTokens"] = raw } raw[calID] = token } func calendarDeltaLinks(cursor map[string]any) map[string]string { raw, _ := cursor["calendarDeltaLinks"].(map[string]any) out := make(map[string]string, len(raw)) for k, v := range raw { if s, ok := v.(string); ok && s != "" { out[k] = s } } return out } func setCalendarDeltaLink(cursor map[string]any, calID, link string) { if calID == "" || link == "" { return } raw, _ := cursor["calendarDeltaLinks"].(map[string]any) if raw == nil { raw = map[string]any{} cursor["calendarDeltaLinks"] = raw } raw[calID] = link } func graphFolderDeltaLinks(cursor map[string]any) map[string]string { raw, _ := cursor["folderDeltaLinks"].(map[string]any) out := make(map[string]string, len(raw)) for k, v := range raw { if s, ok := v.(string); ok && s != "" { out[k] = s } } return out } func setGraphFolderDeltaLink(cursor map[string]any, folderID, link string) { if folderID == "" || link == "" { return } raw, _ := cursor["folderDeltaLinks"].(map[string]any) if raw == nil { raw = map[string]any{} cursor["folderDeltaLinks"] = raw } raw[folderID] = link } func readGraphFolderQueue(cursor map[string]any) []string { raw, _ := cursor["graphFolderQueue"].([]any) out := make([]string, 0, len(raw)) for _, v := range raw { if s, ok := v.(string); ok && s != "" { out = append(out, s) } } return out } func writeGraphFolderQueue(cursor map[string]any, ids []string) { queue := make([]any, 0, len(ids)) for _, id := range ids { if id != "" { queue = append(queue, id) } } cursor["graphFolderQueue"] = queue } func migrationContactPath(bookPath, provider, sourceID string) string { uid := sanitizeMigrationUID(provider, sourceID) return bookPath + uid + ".vcf" } func migrationEventPath(calPath, provider, sourceID string) string { uid := sanitizeMigrationUID(provider, sourceID) return calPath + uid + ".ics" } func sanitizeMigrationUID(provider, sourceID string) string { sourceID = strings.TrimSpace(sourceID) sourceID = strings.ReplaceAll(sourceID, "/", "-") return provider + "-" + sourceID + "@ultimail.migrated" } func applyOAuthToken(cred credentials.Credential, token *oauthToken) credentials.Credential { cred.AuthType = credentials.AuthOAuth2 cred.AccessToken = token.AccessToken if token.RefreshToken != "" { cred.RefreshToken = token.RefreshToken } if !token.Expiry.IsZero() { cred.Expiry = token.Expiry.UTC() } return cred } type oauthToken struct { AccessToken string RefreshToken string Expiry time.Time }