package migration import ( "context" "strings" "github.com/jackc/pgx/v5/pgxpool" ) // HasPendingMigrationInvite reports whether an unclaimed invite exists for the email. func HasPendingMigrationInvite(ctx context.Context, db *pgxpool.Pool, email string) (bool, error) { if db == nil { return false, nil } email = normalizeInviteEmail(email) if email == "" { return false, nil } var exists bool err := db.QueryRow(ctx, ` SELECT EXISTS( SELECT 1 FROM migration_invites WHERE status = 'invited' AND lower(email) = lower($1) ) `, email).Scan(&exists) return exists, err } // ProvisionAudit counts user-linked provision artifacts for test verification. type ProvisionAudit struct { Users int Mailboxes int MailAccounts int NCPrincipals int } // AuditProvisionByEmail counts rows tied to an email across users, mailboxes, mail accounts, and Nextcloud credentials. func AuditProvisionByEmail(ctx context.Context, db *pgxpool.Pool, email string) (ProvisionAudit, error) { var audit ProvisionAudit if db == nil { return audit, nil } email = strings.ToLower(strings.TrimSpace(email)) if email == "" { return audit, nil } if err := db.QueryRow(ctx, ` SELECT COUNT(*) FROM users WHERE lower(email) = $1 `, email).Scan(&audit.Users); err != nil { return audit, err } if err := db.QueryRow(ctx, ` SELECT COUNT(*) FROM mailboxes mb JOIN mail_domains md ON md.id = mb.domain_id WHERE lower(mb.local_part || '@' || md.name) = $1 `, email).Scan(&audit.Mailboxes); err != nil { return audit, err } if err := db.QueryRow(ctx, ` SELECT COUNT(*) FROM mail_accounts WHERE lower(email) = $1 `, email).Scan(&audit.MailAccounts); err != nil { return audit, err } if err := db.QueryRow(ctx, ` SELECT COUNT(*) FROM nextcloud_dav_credentials WHERE nc_user_id = $1 `, email).Scan(&audit.NCPrincipals); err != nil { return audit, err } return audit, nil } // LinkHostedMailboxByEmail attaches orphan mailboxes/mail_accounts (e.g. from claim-before-enroll) to a user. func LinkHostedMailboxByEmail(ctx context.Context, db *pgxpool.Pool, userID, email string) error { email = strings.ToLower(strings.TrimSpace(email)) if email == "" { return nil } _, err := db.Exec(ctx, ` UPDATE mailboxes SET user_id = $1::uuid, updated_at = NOW() WHERE user_id IS NULL AND lower(local_part || '@' || (SELECT name FROM mail_domains d WHERE d.id = mailboxes.domain_id)) = $2 `, userID, email) if err != nil { return err } _, err = db.Exec(ctx, ` UPDATE mail_accounts ma SET user_id = $1::uuid, updated_at = NOW() FROM mailboxes mb JOIN mail_domains md ON md.id = mb.domain_id WHERE mb.mail_account_id = ma.id AND ma.user_id IS NULL AND mb.user_id = $1::uuid AND lower(mb.local_part || '@' || md.name) = $2 `, userID, email) return err }