Перейти к содержимому

Как автоматически архивировать почтовые ящики Exchange и безопасно удалять отключенные учетные записи Active Directory с помощью PowerShell

описание скрипта архивации почтовых ящиков Exchange и удаления отключенных пользователей Active Directory

В крупных организациях удаление учетных записей сотрудников требует строгого соблюдения процедур безопасности. Перед удалением необходимо сохранить корпоративную переписку, выполнить резервное копирование данных Active Directory, убедиться в корректности экспортированных данных и только после этого удалить учетную запись и почтовый ящик. Представленный PowerShell-скрипт полностью автоматизирует этот процесс, минимизируя риск потери информации и человеческих ошибок.

Скрипт предназначен для работы в инфраструктуре Microsoft Active Directory и Microsoft Exchange. Он находит отключенные учетные записи пользователей, которые не изменялись более заданного количества дней, выполняет экспорт почтовых ящиков и архивов в формат PST, создает резервную копию объектов Active Directory, ведет подробное журналирование всех действий и, после успешного завершения экспорта, удаляет пользователей из Active Directory и отключает их почтовые ящики Exchange.

Основные возможности

Скрипт автоматически выполняет следующие действия:

  • поиск отключенных пользователей в указанном Organizational Unit Active Directory;
  • исключение пользователей из заранее определенного списка исключений;
  • фильтрацию по времени последнего изменения учетной записи;
  • обработку только корпоративных почтовых адресов;
  • создание резервной копии всех атрибутов пользователя Active Directory в формате XML;
  • экспорт основного почтового ящика Exchange в PST;
  • экспорт архивного почтового ящика (Online Archive) при его наличии;
  • определение размера основного и архивного почтовых ящиков;
  • контроль количества одновременно выполняемых экспортов Exchange для предотвращения перегрузки сервера;
  • ожидание завершения всех операций экспорта;
  • проверку успешности создания файлов PST;
  • безопасное отключение почтового ящика Exchange;
  • удаление учетной записи пользователя из Active Directory;
  • уведомление внешнего API о факте удаления пользователя;
  • формирование подробного CSV-отчета по каждому обработанному пользователю;
  • ведение подробного журнала всех операций и аудиторского транскрипта PowerShell.

Несколько режимов работы

Для безопасного использования предусмотрено несколько режимов выполнения.

TestOnly — тестовый режим. Скрипт не изменяет инфраструктуру, а только показывает, какие действия были бы выполнены. Это позволяет проверить настройки перед запуском.

ExportOnly — выполняется только экспорт почтовых ящиков и резервное копирование Active Directory без удаления пользователей.

Full — полный цикл обработки, включающий экспорт почтовых ящиков, проверку результатов, отключение почтовых ящиков Exchange и удаление учетных записей из Active Directory.

Защита от критических ошибок

Особое внимание уделено безопасности.

Перед началом экспорта скрипт проверяет соответствие логина Active Directory и почтового адреса пользователя. Это защищает от ситуации, когда атрибут mail был случайно изменен и указывает на почтовый ящик другого сотрудника. При обнаружении несоответствия обработка пользователя полностью прекращается, а информация записывается в отдельный журнал для последующей ручной проверки.

Кроме этого, удаление пользователя выполняется только после подтверждения успешного экспорта основного почтового ящика и архивного почтового ящика (если он существует). Если хотя бы один экспорт завершился с ошибкой или файл PST не был создан, учетная запись автоматически исключается из процесса удаления.

Автоматическое резервное копирование

Перед выполнением любых изменений создается резервная копия объекта Active Directory с сохранением всех атрибутов пользователя в XML-файл. Это позволяет восстановить свойства учетной записи при необходимости.

Для каждого пользователя также сохраняются:

  • основной PST-файл;
  • архивный PST-файл (при наличии);
  • XML-копия объекта Active Directory;
  • журнал обработки;
  • итоговый CSV-отчет.

Контроль экспорта Exchange

Во избежание чрезмерной нагрузки на сервер Exchange скрипт ограничивает количество одновременно выполняемых операций экспорта. После достижения установленного лимита новые задачи автоматически ставятся в очередь и запускаются только после завершения предыдущих.

Во время работы регулярно проверяется статус каждого запроса экспорта, отображается процент выполнения и автоматически удаляются завершенные запросы Exchange.

Подробное журналирование

Каждый этап обработки фиксируется в журнале.

В лог записываются:

  • найденные пользователи;
  • размеры почтовых ящиков;
  • создание запросов экспорта;
  • статус выполнения каждого экспорта;
  • ошибки Exchange;
  • резервное копирование Active Directory;
  • удаление пользователей;
  • ответы внешнего API;
  • итоговая статистика выполнения.

Дополнительно создается PowerShell Transcript, позволяющий полностью восстановить ход выполнения скрипта.

Итоговый отчет

После завершения работы автоматически формируется CSV-файл, содержащий подробную информацию по каждому обработанному пользователю:

  • имя пользователя;
  • логин;
  • адрес электронной почты;
  • размеры почтового ящика и архива;
  • имена созданных PST-файлов;
  • результат экспорта;
  • статус резервного копирования Active Directory;
  • статус удаления учетной записи;
  • сведения об отправке уведомления во внешнее API;
  • описание возникших ошибок (при наличии).

Преимущества решения

Использование данного скрипта позволяет значительно сократить время обслуживания инфраструктуры Exchange и Active Directory, исключить человеческие ошибки при удалении сотрудников, обеспечить сохранность корпоративной переписки, автоматизировать резервное копирование учетных записей и получить полный аудит всех выполненных операций. Благодаря тестовому режиму и встроенным механизмам проверки безопасности скрипт можно использовать даже в крупных корпоративных средах с большим количеством пользователей.

#------ ПАРАМЕТРЫ СКРИПТА ------#
$Mode   = 'TestOnly'  # Измените на 'Full', 'ExportOnly' или 'TestOnly'
$WhatIf = $true       # Установите в $true для тестового режима

# Автоматическое определение режима если указан WhatIf
if ($WhatIf -and $Mode -ne 'TestOnly') {
    Write-Host "ВНИМАНИЕ: `$Mode = '$Mode', но `$WhatIf = `$true — скрипт будет выполнен в режиме TestOnly." -ForegroundColor Red
}
if ($WhatIf) {
    $Mode = 'TestOnly'
}

Write-Host "=== РЕЖИМ ВЫПОЛНЕНИЯ: $Mode ===" -ForegroundColor Cyan
if ($Mode -eq 'TestOnly') {
    Write-Host "РЕЖИМ ПРОВЕРКИ: Ничего не будет экспортироваться или удаляться" -ForegroundColor Yellow
}
elseif ($Mode -eq 'ExportOnly') {
    Write-Host "РЕЖИМ ЭКСПОРТА: Будут экспортированы почтовые ящики, но пользователи не будут удалены" -ForegroundColor Yellow
}

#------ Переменные ------#
$ExceptedUsers = 'sd','admins'
$OrgUnit = "OU=Locations,DC=adminbd,DC=ru"
$domain  = "adminbd.ru"
$dt      = (Get-Date).AddDays(-60)
$dtstamp = Get-Date -Format "yyyy-MM-dd-hh-mm-ss"
$pattern = '[/\\:?*<>"|]'
$exppath = "\\ARC01\arc$\$dtstamp"
$maxConcurrentExports = 50   # Ограничение одновременных запросов экспорта
$exportPollSeconds    = 15   # Как часто проверять освобождение слотов при постановке в очередь

# Создание директории для архивации
if (-not (Test-Path -Path $exppath)) {
    New-Item -Path $exppath -ItemType Directory -Force | Out-Null
}

# Создание директории для бэкапов AD
$adBackupPath = "$exppath\AD_Backup"
if (-not (Test-Path -Path $adBackupPath)) {
    New-Item -Path $adBackupPath -ItemType Directory -Force | Out-Null
}

#------ Функции ------#

# Функция для получения размера в MB из ByteQuantifiedSize.
# При работе через удаленную сессию Exchange (Import-PSSession) объекты
# приходят как "Deserialized.Microsoft.Exchange.Data.ByteQuantifiedSize" —
# у них нет метода .ToBytes(), поэтому парсим значение из строкового представления,
# формат которого всегда вида: "12.34 MB (12,942,432 bytes)".
function Get-SizeMB {
    param(
        $SizeObject
    )

    if ($null -eq $SizeObject) { return 0 }

    try {
        $str = $SizeObject.ToString()
        if ($str -match '\(([\d,]+)\s*bytes\)') {
            $bytes = [int64]($matches[1] -replace ',', '')
            return [math]::Round($bytes / 1MB, 2)
        }
        else {
            Write-Log "Не удалось распарсить размер из строки: $str" -Level WARN
            return 0
        }
    }
    catch {
        Write-Log "Ошибка получения размера: $($_.Exception.Message)" -Level WARN
        return 0
    }
}

# Функция для записи логов
function Write-Log {
    param (
        [string]$Message,
        [ValidateSet('INFO','WARN','ERROR','TEST')]
        [string]$Level = 'INFO',
        [string]$LogFile = "$exppath\process.log"
    )
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logMessage = "$timestamp [$Level] - $Message"

    $colors = @{
        'INFO' = 'White'
        'WARN' = 'Yellow'
        'ERROR' = 'Red'
        'TEST' = 'Cyan'
    }

    Write-Host $logMessage -ForegroundColor $colors[$Level]
    $logMessage | Out-File -FilePath $LogFile -Append -Encoding UTF8
}

# Функция, ограничивающая число одновременно выполняющихся запросов экспорта.
# Блокирует выполнение, пока число активных (не завершившихся) запросов
# из $ActiveRequestsRef не опустится ниже $MaxConcurrent.
function Wait-ForExportSlot {
    param(
        [System.Collections.ArrayList]$ActiveRequestsRef,
        [int]$MaxConcurrent,
        [int]$PollSeconds = 15
    )

    while ($ActiveRequestsRef.Count -ge $MaxConcurrent) {
        Write-Log "Достигнут лимит одновременных экспортов ($MaxConcurrent). Ожидание освобождения слота..." -Level INFO
        Start-Sleep -Seconds $PollSeconds

        foreach ($reqName in @($ActiveRequestsRef)) {
            $status = Get-MailboxExportRequest -Name $reqName -ErrorAction SilentlyContinue
            if (-not $status -or $status.Status -in @("Completed","CompletedWithWarning") -or $status.Status -like "Failed*") {
                if ($status -and $status.Status -in @("Completed","CompletedWithWarning")) {
                    Write-Log "Экспорт $reqName завершен успешно (слот освобожден)" -Level INFO
                }
                elseif ($status -and $status.Status -like "Failed*") {
                    Write-Log "Экспорт $reqName завершился с ошибкой: $($status.Status) (слот освобожден)" -Level ERROR
                }
                $ActiveRequestsRef.Remove($reqName)
            }
        }
    }
}

# Функция для проверки статуса экспорта
function Wait-ExportCompletion {
    param (
        [string[]]$ExportRequests,
        [int]$TimeoutMinutes = 180
    )

    $endTime = (Get-Date).AddMinutes($TimeoutMinutes)
    $completedRequests = @()

    Write-Log "Ожидание завершения $($ExportRequests.Count) запросов экспорта..." -Level INFO

    while ((Get-Date) -lt $endTime -and $ExportRequests.Count -gt 0) {
        foreach ($request in @($ExportRequests)) {
            try {
                $status = Get-MailboxExportRequest -Name $request -ErrorAction SilentlyContinue

                if ($status) {
                    switch ($status.Status) {
                        { $_ -in @("Completed", "CompletedWithWarning") } {
                            Write-Log "Экспорт $request завершен успешно" -Level INFO
                            Remove-MailboxExportRequest -Identity $request -Confirm:$false -ErrorAction SilentlyContinue
                            $completedRequests += $request
                        }
                        { $_ -like "Failed*" } {
                            Write-Log "Экспорт $request завершился с ошибкой: $($status.Status)" -Level ERROR
                            Write-Log "Сообщение об ошибке: $($status.Message)" -Level ERROR
                            Remove-MailboxExportRequest -Identity $request -Confirm:$false -ErrorAction SilentlyContinue
                            $completedRequests += $request
                        }
                        default {
                            Write-Log "Экспорт $request в статусе: $($status.Status) ($($status.PercentComplete)%)" -Level INFO
                        }
                    }
                }
                else {
                    # Запрос уже не существует (например, удален вручную) — считаем завершенным
                    $completedRequests += $request
                }
            }
            catch {
                Write-Log "Ошибка проверки статуса $request : $($_.Exception.Message)" -Level ERROR
            }
        }

        $ExportRequests = $ExportRequests | Where-Object { $_ -notin $completedRequests }
        $completedRequests = @()

        if ($ExportRequests.Count -gt 0) {
            Write-Log "Ожидаю завершения $($ExportRequests.Count) запросов..." -Level INFO
            Start-Sleep -Seconds 30
        }
    }

    foreach ($request in $ExportRequests) {
        try {
            Remove-MailboxExportRequest -Identity $request -Confirm:$false -ErrorAction SilentlyContinue
            Write-Log "Удален запрос экспорта $request (таймаут)" -Level WARN
        }
        catch {
            Write-Log "Не удалось удалить запрос $request : $($_.Exception.Message)" -Level ERROR
        }
    }
}

# Функция для безопасного экспорта архива
function Export-MailboxArchive {
    param(
        [string]$Email,
        [string]$FilePath,
        [string]$RequestName,
        [bool]$TestMode = $false
    )

    try {
        Write-Log "Проверка архива для: $Email" -Level INFO

        $mailbox = Get-Mailbox -Identity $Email -ErrorAction SilentlyContinue
        if (-not $mailbox) {
            Write-Log "Почтовый ящик $Email не найден" -Level WARN
            return $null
        }

        if ($null -eq $mailbox.ArchiveDatabase -or $mailbox.ArchiveStatus -ne "Active") {
            Write-Log "У пользователя $Email нет активного архива." -Level INFO
            return @{ Status = "NoArchive" }
        }

        $archiveStats = Get-MailboxStatistics -Archive -Identity $Email -ErrorAction SilentlyContinue
        if ($archiveStats) {
            $archiveSize = Get-SizeMB -SizeObject $archiveStats.TotalItemSize.Value
            Write-Log "Размер архива $Email : $archiveSize MB" -Level INFO

            if ($archiveSize -eq 0) {
                Write-Log "Архив пользователя $Email пустой." -Level INFO
                return @{ Status = "EmptyArchive" }
            }
        } else {
            Write-Log "Не удалось получить статистику архива для $Email" -Level WARN
            return @{ Status = "StatsUnavailable" }
        }

        if (-not $TestMode) {
            Write-Log "Начинаю экспорт архива в: $FilePath" -Level INFO
            $request = New-MailboxExportRequest -Mailbox $Email `
                -BadItemLimit 10 `
                -FilePath $FilePath `
                -IsArchive `
                -Name $RequestName `
                -ErrorAction SilentlyContinue `
                -WarningAction SilentlyContinue

            if ($request) {
                Write-Log "Создан запрос экспорта архива: $FilePath" -Level INFO
                return @{ Name = $request.Name; Status = "RequestCreated" }
            }
            else {
                Write-Log "Не удалось создать запрос экспорта архива для $Email" -Level WARN
                return @{ Status = "RequestFailed" }
            }
        } else {
            Write-Log "[ТЕСТ] Будет создан запрос экспорта архива: $FilePath" -Level TEST
            return @{ Name = "$RequestName-TEST"; Status = "RequestCreated" }
        }
    }
    catch {
        Write-Log "Ошибка экспорта архива для $Email : $($_.Exception.Message)" -Level ERROR
        return @{ Status = "Error: $($_.Exception.Message)" }
    }
}

# Функция для экспорта свойств пользователя AD
function Backup-ADUserProperties {
    param(
        [Microsoft.ActiveDirectory.Management.ADUser]$User,
        [bool]$TestMode = $false
    )

    $backupFile = "$adBackupPath\$($User.SamAccountName)_backup.xml"

    try {
        if (-not $TestMode) {
            $userProperties = Get-ADUser -Identity $User.SamAccountName -Properties * -ErrorAction Stop
            $userProperties | Select-Object * | Export-Clixml -Path $backupFile -Force -ErrorAction Stop
            Write-Log "Свойства AD сохранены в: $backupFile" -Level INFO
            return $backupFile
        } else {
            Write-Log "[ТЕСТ] Будет создан файл бэкапа AD: $backupFile" -Level TEST
            return $backupFile
        }
    }
    catch {
        Write-Log "Ошибка резервного копирования свойств AD для $($User.SamAccountName): $($_.Exception.Message)" -Level ERROR
        return $null
    }
}

# Функция для безопасного удаления пользователя AD
function Remove-ADUserSafely {
    param(
        [string]$SamAccountName,
        [string]$UserPrincipalName,
        [bool]$TestMode = $false
    )

    try {
        Write-Log "Обработка удаления пользователя: $SamAccountName" -Level INFO

        $adUser = Get-ADUser -Identity $SamAccountName -ErrorAction SilentlyContinue
        if (-not $adUser) {
            Write-Log "Пользователь $SamAccountName уже удален из AD" -Level WARN
            return $true
        }

        $groups = Get-ADPrincipalGroupMembership -Identity $SamAccountName -ErrorAction SilentlyContinue |
            Where-Object { $_.Name -ne "Domain Users" }

        if ($groups -and $groups.Count -gt 0) {
            $groupNames = $groups.Name -join ', '
            Write-Log "Пользователь $SamAccountName состоит в группах: $groupNames" -Level WARN
            "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $SamAccountName группы: $groupNames" |
                Out-File "$exppath\group_memberships.txt" -Append
        }

        if (-not $TestMode) {
            Remove-ADUser -Identity $SamAccountName -Confirm:$false -ErrorAction Stop
            Write-Log "Пользователь $SamAccountName успешно удален из AD" -Level INFO

            Send-UserDeleteToAPI -UserPrincipalName $UserPrincipalName -TestMode $TestMode

            return $true
        } else {
            Write-Log "[ТЕСТ] Пользователь $SamAccountName БЫЛ БЫ удален из AD" -Level TEST
            return $true
        }
    }
    catch {
        Write-Log "Ошибка удаления пользователя $SamAccountName из AD: $($_.Exception.Message)" -Level ERROR
        return $false
    }
}

# Функция для отправки информации в API
function Send-UserDeleteToAPI {
    param(
        [string]$UserPrincipalName,
        [bool]$TestMode = $false
    )

    try {
        if ([string]::IsNullOrEmpty($UserPrincipalName)) {
            Write-Log "Не указан UserPrincipalName для отправки в API" -Level WARN
            return $false
        }

        if (-not $TestMode) {
            $data = @{
                "login"     = $UserPrincipalName
                "timestamp" = Get-Date -Format "yyyy-MM-ddTHH:mm:ss"
                "action"    = "delete"
            }

            $headers = @{
                "accept"       = 'application/json'
                "Content-Type" = "application/json"
            }

            $jsonBody = $data | ConvertTo-Json

            Write-Log "Отправка запроса в API для пользователя: $UserPrincipalName" -Level INFO

            $response = Invoke-WebRequest -Method 'Post' `
                -Uri 'https://adeventhandler.adminbd.ru/User/Delete' `
                -Headers $headers `
                -Body $jsonBody `
                -TimeoutSec 30 `
                -ErrorAction Stop

            Write-Log "API ответил статусом: $($response.StatusCode) для $UserPrincipalName" -Level INFO
            return $true
        } else {
            Write-Log "[ТЕСТ] Запрос в API БЫЛ БЫ отправлен для: $UserPrincipalName" -Level TEST
            return $true
        }
    }
    catch {
        Write-Log "Ошибка отправки в API для $UserPrincipalName : $($_.Exception.Message)" -Level ERROR
        return $false
    }
}

# Функция для отключения почтового ящика
function Disable-UserMailbox {
    param(
        [string]$Email,
        [bool]$TestMode = $false
    )

    try {
        if (-not $TestMode) {
            Disable-Mailbox -Identity $Email -Confirm:$false -ErrorAction Stop
            Write-Log "Почтовый ящик отключен: $Email" -Level INFO
            return $true
        } else {
            Write-Log "[ТЕСТ] Почтовый ящик БЫЛ БЫ отключен: $Email" -Level TEST
            return $true
        }
    }
    catch {
        Write-Log "Ошибка отключения почтового ящика $Email : $($_.Exception.Message)" -Level ERROR
        return $false
    }
}

# Функция для создания запроса экспорта
function New-ExportRequest {
    param(
        [string]$Email,
        [string]$FilePath,
        [string]$RequestName,
        [bool]$TestMode = $false
    )

    try {
        if (-not $TestMode) {
            $request = New-MailboxExportRequest -Mailbox $Email `
                -BadItemLimit 10 `
                -FilePath $FilePath `
                -Name $RequestName `
                -ErrorAction SilentlyContinue `
                -WarningAction SilentlyContinue

            if ($request) {
                Write-Log "Создан запрос экспорта: $FilePath" -Level INFO
                return $request
            }
            else {
                Write-Log "Не удалось создать запрос экспорта для $Email" -Level ERROR
                return $null
            }
        } else {
            Write-Log "[ТЕСТ] Будет создан запрос экспорта: $FilePath" -Level TEST
            return @{Name = "$RequestName-TEST"}
        }
    }
    catch {
        Write-Log "Ошибка создания запроса экспорта: $($_.Exception.Message)" -Level ERROR
        return $null
    }
}

# Функция проверки: логин AD (SamAccountName / UserPrincipalName) должен
# соответствовать почтовому адресу (атрибут EmailAddress/mail).
# ЗАЩИТА ОТ КРИТИЧЕСКОЙ ОШИБКИ: если у отключенной учетки атрибут mail ошибочно
# указывает на ящик другого (возможно активного) сотрудника, скрипт не должен
# экспортировать и удалять этот чужой ящик, приняв решение по данным не того человека.
function Test-LoginMatchesEmail {
    param(
        [string]$SamAccountName,
        [string]$UserPrincipalName,
        [string]$EmailAddress
    )

    if ([string]::IsNullOrWhiteSpace($EmailAddress)) {
        # Нет почты — сравнивать не с чем, эта ветка обрабатывается отдельно (UsersWithoutMailbox)
        return $true
    }

    $emailLocalPart = ($EmailAddress -split '@')[0]

    if ($SamAccountName -and ($SamAccountName.ToLower() -eq $emailLocalPart.ToLower())) {
        return $true
    }

    if ($UserPrincipalName) {
        $upnLocalPart = ($UserPrincipalName -split '@')[0]
        if ($upnLocalPart.ToLower() -eq $emailLocalPart.ToLower()) {
            return $true
        }
    }

    return $false
}

#------ Основной процесс ------#
Write-Host "`n=== ЗАПУСК СКРИПТА АРХИВАЦИИ ===" -ForegroundColor Cyan
Write-Host "Для начала работы выполните следующие шаги:" -ForegroundColor White
Write-Host "1. Установите сессию Exchange" -ForegroundColor White
Write-Host "2. Импортируйте модуль ActiveDirectory" -ForegroundColor White
Write-Host "`nВот команды для запуска:" -ForegroundColor Yellow
Write-Host "`n# 1. Установка сессии Exchange" -ForegroundColor Green
Write-Host "`$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://MXS01.adminbd.ru/PowerShell/ -Authentication Kerberos" -ForegroundColor Gray
Write-Host "Import-PSSession `$Session -DisableNameChecking -AllowClobber" -ForegroundColor Gray

Write-Host "`n# 2. Импорт модуля AD" -ForegroundColor Green
Write-Host "Import-Module ActiveDirectory" -ForegroundColor Gray

Write-Host "`n# 3. Очистка памяти" -ForegroundColor Green
Write-Host "[System.GC]::Collect()" -ForegroundColor Gray

Write-Host "`n# 4. Установите режим работы:" -ForegroundColor Green
Write-Host "# Тестовый режим (только проверка):" -ForegroundColor Yellow
Write-Host "`$Mode = 'TestOnly'" -ForegroundColor Gray
Write-Host "`$WhatIf = `$true" -ForegroundColor Gray

Write-Host "`n# Только экспорт (без удаления):" -ForegroundColor Yellow
Write-Host "`$Mode = 'ExportOnly'" -ForegroundColor Gray
Write-Host "`$WhatIf = `$false" -ForegroundColor Gray

Write-Host "`n# Полный режим (экспорт + удаление):" -ForegroundColor Yellow
Write-Host "`$Mode = 'Full'" -ForegroundColor Gray
Write-Host "`$WhatIf = `$false" -ForegroundColor Gray

Write-Host "`n# 5. Запустите основную часть скрипта:" -ForegroundColor Green
Write-Host "# Скопируйте и выполните весь код ниже после установки режима" -ForegroundColor Yellow

Read-Host "`nНажмите Enter для продолжения..."

$TestMode       = ($Mode -eq 'TestOnly')
$ExportOnlyMode = ($Mode -eq 'ExportOnly')

if ($TestMode) {
    Write-Log "=== РЕЖИМ ТЕСТИРОВАНИЯ ===" -Level TEST
    Write-Log "Ничего не будет экспортироваться или удаляться" -Level TEST
    Write-Log "Будет показано, какие действия были бы выполнены" -Level TEST
}

$transcriptPath = "$exppath\audit_transcript_$(Get-Date -Format 'yyyyMMdd').txt"
Start-Transcript -Path $transcriptPath -Append

# Получение списка пользователей
try {
    $adminbd= Get-ADUser -Server $domain `
        -SearchBase $OrgUnit `
        -Properties EmailAddress, Modified, Title, City, SamAccountName, WhenChanged, LastLogonDate, DistinguishedName, UserPrincipalName `
        -Filter "Enabled -eq 'False'" `
        -ErrorAction Stop |
        Where-Object {
            $_.SamAccountName -notin $ExceptedUsers -and
            $_.Modified -le $dt -and
            ($_.EmailAddress -like "*@adminbd.ru" -or $null -eq $_.EmailAddress)
        }

    Write-Log "Найдено $($adminbd.Count) пользователей для обработки" -Level INFO
}
catch {
    Write-Log "Ошибка получения пользователей из AD: $($_.Exception.Message)" -Level ERROR
    Stop-Transcript
    throw
}

Write-Host "`n=== НАЙДЕННЫЕ ПОЛЬЗОВАТЕЛИ ===" -ForegroundColor Green
$adminbd| Format-Table Name, EmailAddress, SamAccountName, Modified, City -AutoSize

Write-Log "Список пользователей для обработки:" -Level INFO
foreach ($user in $adminbd) {
    Write-Log "  - $($user.Name) ($($user.EmailAddress)) [Modified: $($user.Modified)]" -Level INFO
}

# Массивы для хранения данных.
# $ExportRequests хранит ВСЕ созданные запросы (для финального ожидания),
# $ActiveExports — только те, что еще не завершились (для троттлинга).
$ExportRequests = New-Object System.Collections.ArrayList
$ActiveExports  = New-Object System.Collections.ArrayList
$Result         = @()

$UsersWithMailbox    = $adminbd| Where-Object { $_.EmailAddress -like "*@adminbd.ru" }
$UsersWithoutMailbox = $adminbd| Where-Object { $_.EmailAddress -notlike "*@adminbd.ru" }

Write-Host "`n=== СВОДКА ===" -ForegroundColor Cyan
Write-Host "Всего пользователей: $($adminbd.Count)" -ForegroundColor White
Write-Host "С почтой @adminbd.ru: $($UsersWithMailbox.Count)" -ForegroundColor White
Write-Host "Без почты @adminbd.ru: $($UsersWithoutMailbox.Count)" -ForegroundColor White

if ($UsersWithMailbox.Count -gt 0) {
    Write-Host "`n=== ПОЛЬЗОВАТЕЛИ С ПОЧТОЙ (будут экспортированы) ===" -ForegroundColor Yellow
    $UsersWithMailbox | Format-Table Name, EmailAddress, Modified -AutoSize
}

if ($UsersWithoutMailbox.Count -gt 0) {
    Write-Host "`n=== ПОЛЬЗОВАТЕЛИ БЕЗ ПОЧТЫ (будут удалены из AD) ===" -ForegroundColor Yellow
    $UsersWithoutMailbox | Format-Table Name, SamAccountName, Modified -AutoSize
}

# Этап архивации почтовых ящиков (только для пользователей с @adminbd.ru)
if ($UsersWithMailbox.Count -gt 0) {
    Write-Log "=== ЭТАП АРХИВАЦИИ ПОЧТОВЫХ ЯЩИКОВ ===" -Level INFO

    foreach ($user in $UsersWithMailbox) {
        Write-Log "Обработка пользователя: $($user.Name) ($($user.EmailAddress))" -Level INFO

        $detailed = [ordered]@{
            DT                     = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
            Name                   = $user.Name
            Login                  = $user.UserPrincipalName
            Email                  = $user.EmailAddress
            SamAccountName         = $user.SamAccountName
            ModDT                  = if ($user.Modified) { $user.Modified.ToString("yyyy-MM-dd HH:mm:ss") } else { "" }
            Title                  = if ($user.Title) { $user.Title -replace $pattern, '_' } else { "" }
            City                   = if ($user.City) { $user.City -replace $pattern, '_' } else { "" }
            PST_File               = ""
            Archive_PST_File       = ""
            PST_Size_MB            = 0
            Archive_Size_MB        = 0
            PST_Export_Status      = ""
            Archive_Export_Status  = ""
            AD_Backup_File         = ""
            AD_Delete_Status       = ""
            API_Status             = ""
            Error                  = ""
            Status                 = ""
            Action                 = ""
        }

        try {
            $backupFile = Backup-ADUserProperties -User $user -TestMode $TestMode
            $detailed.AD_Backup_File = if ($backupFile) { (Split-Path $backupFile -Leaf) } else { "FAILED" }

            # КРИТИЧЕСКАЯ ПРОВЕРКА: логин AD должен соответствовать почтовому адресу.
            # Если атрибут mail ошибочно указывает на ящик другого человека —
            # НЕ трогаем почту вообще (ни экспорт, ни удаление), чтобы не задеть чужой ящик.
            if (-not (Test-LoginMatchesEmail -SamAccountName $user.SamAccountName -UserPrincipalName $user.UserPrincipalName -EmailAddress $user.EmailAddress)) {
                $detailed.Status = "LoginEmailMismatch - SkippedForSafety"
                $detailed.Action = "Пропуск: логин AD ($($user.SamAccountName)) не соответствует почте ($($user.EmailAddress)) — требуется ручная проверка"
                Write-Log "НЕСООТВЕТСТВИЕ ЛОГИНА И ПОЧТЫ: $($user.SamAccountName) <-> $($user.EmailAddress). Пользователь пропущен, почта не тронута." -Level ERROR
                "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - SamAccountName: $($user.SamAccountName) | UPN: $($user.UserPrincipalName) | Email: $($user.EmailAddress)" |
                    Out-File "$exppath\login_email_mismatch.txt" -Append
                $Result += New-Object PSObject -Property $detailed
                continue
            }

            $Mailbox = Get-Mailbox -Identity $user.EmailAddress -ErrorAction SilentlyContinue
            if (-not $Mailbox) {
                $detailed.Status = "Mailbox not found in Exchange"
                $detailed.Action = "Пропуск: Почтовый ящик не найден"
                Write-Log "Почтовый ящик $($user.EmailAddress) не найден в Exchange" -Level WARN
                $Result += New-Object PSObject -Property $detailed
                continue
            }

            $safeCity  = if ($user.City) { $user.City -replace $pattern, '_' } else { "NoCity" }
            $safeTitle = if ($user.Title) { $user.Title -replace $pattern, '_' } else { "NoTitle" }
            $safeEmail = $user.EmailAddress.Replace('@', '_at_')

            $PrimaryPST = "$exppath\$safeCity-$safeTitle-$safeEmail.pst"
            $ArchivePST = "$exppath\$safeCity-$safeTitle-$safeEmail-Archive.pst"

            $detailed.PST_File         = (Split-Path $PrimaryPST -Leaf)
            $detailed.Archive_PST_File = (Split-Path $ArchivePST -Leaf)

            $mailboxStats = Get-MailboxStatistics -Identity $user.EmailAddress -ErrorAction SilentlyContinue
            if ($mailboxStats) {
                $mailboxSize = Get-SizeMB -SizeObject $mailboxStats.TotalItemSize.Value
                Write-Log "Размер почтового ящика $($user.EmailAddress): $mailboxSize MB" -Level INFO
                $detailed.PST_Size_MB = $mailboxSize
            }

            if (-not $TestMode) {
                $detailed.Action = "Экспорт почтового ящика"

                # Ограничение одновременных экспортов
                Wait-ForExportSlot -ActiveRequestsRef $ActiveExports -MaxConcurrent $maxConcurrentExports -PollSeconds $exportPollSeconds

                # Экспорт основного почтового ящика
                Write-Log "Экспорт основного ящика в: $PrimaryPST" -Level INFO
                try {
                    $primaryRequestName = "$($user.SamAccountName)-Primary-$dtstamp"
                    $primaryRequest = New-ExportRequest -Email $user.EmailAddress `
                        -FilePath $PrimaryPST `
                        -RequestName $primaryRequestName `
                        -TestMode $TestMode

                    if ($primaryRequest) {
                        [void]$ExportRequests.Add($primaryRequest.Name)
                        [void]$ActiveExports.Add($primaryRequest.Name)
                        $detailed.PST_Export_Status = "RequestCreated"
                        Write-Log "Запрос экспорта основного ящика создан: $($primaryRequest.Name)" -Level INFO
                    }
                    else {
                        $detailed.PST_Export_Status = "RequestFailed"
                        Write-Log "Не удалось создать запрос экспорта основного ящика для $($user.EmailAddress)" -Level ERROR
                    }
                }
                catch {
                    $detailed.PST_Export_Status = "Error: $($_.Exception.Message)"
                    Write-Log "Ошибка создания запроса экспорта основного ящика: $($_.Exception.Message)" -Level ERROR
                }

                # Ограничение одновременных экспортов перед архивом
                Wait-ForExportSlot -ActiveRequestsRef $ActiveExports -MaxConcurrent $maxConcurrentExports -PollSeconds $exportPollSeconds

                # Экспорт архива почтового ящика (с проверкой наличия)
                Write-Log "Проверка наличия архива для $($user.EmailAddress)" -Level INFO
                $archiveResult = Export-MailboxArchive `
                    -Email $user.EmailAddress `
                    -FilePath $ArchivePST `
                    -RequestName "$($user.SamAccountName)-Archive-$dtstamp" `
                    -TestMode $TestMode

                if ($archiveResult.Name) {
                    [void]$ExportRequests.Add($archiveResult.Name)
                    [void]$ActiveExports.Add($archiveResult.Name)
                }
                $detailed.Archive_Export_Status = $archiveResult.Status

            } else {
                $detailed.Action = "[ТЕСТ] Был бы экспортирован почтовый ящик"
                $detailed.PST_Export_Status = "[ТЕСТ] Был бы создан файл: $PrimaryPST"

                $archiveResult = Export-MailboxArchive `
                    -Email $user.EmailAddress `
                    -FilePath $ArchivePST `
                    -RequestName "$($user.SamAccountName)-Archive-$dtstamp" `
                    -TestMode $TestMode
                $detailed.Archive_Export_Status = $archiveResult.Status

                Write-Log "[ТЕСТ] Был бы создан запрос экспорта для $($user.EmailAddress)" -Level TEST
            }

        }
        catch {
            $detailed.Error = $_.Exception.Message
            Write-Log "Критическая ошибка обработки пользователя $($user.SamAccountName): $($detailed.Error)" -Level ERROR
        }

        $Result += New-Object PSObject -Property $detailed
    }
}

# Ожидание завершения всех запросов экспорта (только в реальном режиме)
if ($ExportRequests.Count -gt 0 -and -not $TestMode) {
    Write-Log "=== ОЖИДАНИЕ ЗАВЕРШЕНИЯ ЭКСПОРТОВ ===" -Level INFO
    Wait-ExportCompletion -ExportRequests @($ExportRequests)
    Write-Log "Экспорт всех ящиков завершен." -Level INFO
} elseif ($TestMode) {
    Write-Log "[ТЕСТ] Были бы ожиданы завершения экспортов" -Level TEST
} else {
    Write-Log "Нет запросов экспорта для ожидания." -Level INFO
}

# Обновляем финальные статусы экспорта по факту наличия/размера файлов на диске
for ($i = 0; $i -lt $Result.Count; $i++) {
    $record = $Result[$i]
    if ($record.Email -notlike "*@adminbd.ru") { continue }

    if ($record.PST_File -and $record.PST_File -ne "" -and (Test-Path "$exppath\$($record.PST_File)")) {
        $pstSize = (Get-Item "$exppath\$($record.PST_File)").Length / 1MB
        $record.PST_Size_MB = [math]::Round($pstSize, 2)
        $record.PST_Export_Status = if ($pstSize -gt 0) { "Completed ($($record.PST_Size_MB) MB)" } else { "EmptyFile" }
    } elseif ($record.PST_Export_Status -eq "RequestCreated") {
        $record.PST_Export_Status = "FileNotFound"
    }

    if ($record.Archive_PST_File -and $record.Archive_PST_File -ne "" -and (Test-Path "$exppath\$($record.Archive_PST_File)")) {
        $archSize = (Get-Item "$exppath\$($record.Archive_PST_File)").Length / 1MB
        $record.Archive_Size_MB = [math]::Round($archSize, 2)
        $record.Archive_Export_Status = if ($archSize -gt 0) { "Completed ($($record.Archive_Size_MB) MB)" } else { "EmptyFile" }
    } elseif ($record.Archive_Export_Status -eq "RequestCreated") {
        $record.Archive_Export_Status = "FileNotFound"
    }
}

# Обработка пользователей без почты (удаление из AD)
if ($UsersWithoutMailbox.Count -gt 0) {
    Write-Log "=== ОБРАБОТКА ПОЛЬЗОВАТЕЛЕЙ БЕЗ ПОЧТЫ ===" -Level INFO

    foreach ($user in $UsersWithoutMailbox) {
        Write-Log "Обработка пользователя без почты: $($user.Name) ($($user.SamAccountName))" -Level INFO

        $detailed = [ordered]@{
            DT                     = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
            Name                   = $user.Name
            Login                  = $user.UserPrincipalName
            Email                  = $user.EmailAddress
            SamAccountName         = $user.SamAccountName
            ModDT                  = if ($user.Modified) { $user.Modified.ToString("yyyy-MM-dd HH:mm:ss") } else { "" }
            Title                  = if ($user.Title) { $user.Title -replace $pattern, '_' } else { "" }
            City                   = if ($user.City) { $user.City -replace $pattern, '_' } else { "" }
            PST_File               = "N/A"
            Archive_PST_File       = "N/A"
            PST_Size_MB            = 0
            Archive_Size_MB        = 0
            PST_Export_Status      = "NoMailbox"
            Archive_Export_Status  = "NoMailbox"
            AD_Backup_File         = ""
            AD_Delete_Status       = ""
            API_Status              = ""
            Error                  = ""
            Status                 = ""
            Action                 = ""
        }

        try {
            $backupFile = Backup-ADUserProperties -User $user -TestMode $TestMode
            $detailed.AD_Backup_File = if ($backupFile) { (Split-Path $backupFile -Leaf) } else { "FAILED" }

            if ($TestMode) {
                $detailed.Action = "[ТЕСТ] Был бы удален из AD (без почты)"
                $detailed.Status = "[ТЕСТ] Был бы удален"
                Write-Log "[ТЕСТ] Пользователь $($user.SamAccountName) БЫЛ БЫ удален из AD" -Level TEST
            }
            elseif ($ExportOnlyMode) {
                $detailed.Action = "Только экспорт режим - пропуск удаления"
                $detailed.Status = "ExportOnlyMode - Skipped"
                Write-Log "Режим ExportOnly - пропускаем удаление $($user.SamAccountName)" -Level INFO
            }
            else {
                $detailed.Action = "Удаление из AD (без почты)"

                $deleteResult = Remove-ADUserSafely -SamAccountName $user.SamAccountName `
                    -UserPrincipalName $user.UserPrincipalName `
                    -TestMode $TestMode

                $detailed.AD_Delete_Status = if ($deleteResult) { "Success" } else { "Failed" }
                $detailed.Status = "ADUserDeleted"
                $detailed.API_Status = if ($deleteResult) { "Sent" } else { "Failed" }
            }
        }
        catch {
            $detailed.Error = $_.Exception.Message
            Write-Log "Ошибка обработки пользователя без почты $($user.SamAccountName): $($detailed.Error)" -Level ERROR
        }

        $Result += New-Object PSObject -Property $detailed
    }
}

# Удаление пользователей с почтой (только в полном режиме)
if (-not $TestMode -and -not $ExportOnlyMode -and $UsersWithMailbox.Count -gt 0) {
    Write-Log "=== ЭТАП УДАЛЕНИЯ ПОЛЬЗОВАТЕЛЕЙ С ПОЧТОЙ ===" -Level INFO

    foreach ($record in $Result | Where-Object {$_.Email -like "*@adminbd.ru"}) {
        Write-Log "Обработка удаления для: $($record.Name) ($($record.Email))" -Level INFO

        try {
            # Удаляем ТОЛЬКО если и основной ящик, и архив (если он был) успешно экспортированы.
            $pstOk = ($record.PST_Export_Status -like "Completed*" -or $record.PST_Export_Status -eq "EmptyFile")
            $archiveOk = ($record.Archive_Export_Status -like "Completed*" -or
                          $record.Archive_Export_Status -eq "EmptyFile" -or
                          $record.Archive_Export_Status -eq "NoArchive" -or
                          $record.Archive_Export_Status -eq "EmptyArchive")

            if ($pstOk -and $archiveOk) {
                Write-Log "Удаление почтового ящика: $($record.Email)" -Level INFO

                try {
                    $disableResult = Disable-UserMailbox -Email $record.Email -TestMode $TestMode
                    if ($disableResult) {
                        $record.Status = "MailboxDisabled"
                    }

                    $deleteResult = Remove-ADUserSafely -SamAccountName $record.SamAccountName `
                        -UserPrincipalName $record.Login `
                        -TestMode $TestMode

                    $record.AD_Delete_Status = if ($deleteResult) { "Success" } else { "Failed" }
                    $record.Action = "Удален почтовый ящик и пользователь AD"

                }
                catch {
                    $record.Error = "Mailbox/AD Error: $($_.Exception.Message)"
                    Write-Log "Ошибка удаления почтового ящика $($record.Email): $($record.Error)" -Level ERROR
                }

            } else {
                $record.Status = "ExportFailed - SkippedDeletion"
                $record.Action = "Пропуск удаления: PST=$($record.PST_Export_Status); Archive=$($record.Archive_Export_Status)"
                Write-Log "Пропускаем удаление $($record.Email) — экспорт не подтвержден (PST: $($record.PST_Export_Status), Archive: $($record.Archive_Export_Status))" -Level WARN
            }

        }
        catch {
            $record.Error = "Processing Error: $($_.Exception.Message)"
            Write-Log "Критическая ошибка обработки пользователя $($record.SamAccountName): $($record.Error)" -Level ERROR
        }
    }
}

# Сохранение результата в CSV
$csvPath = "$exppath\Results.csv"
try {
    $Result | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8 -Delimiter ";" -Force
    Write-Log "Результаты сохранены в файл: $csvPath" -Level INFO
}
catch {
    Write-Log "Ошибка сохранения CSV: $($_.Exception.Message)" -Level ERROR
}

Write-Log "Скрипт завершен." -Level INFO

Stop-Transcript

# Вывод сводки
$pstCompletedCount = ($Result | Where-Object {$_.PST_Export_Status -like 'Completed*'}).Count
$deletedCount      = ($Result | Where-Object {$_.AD_Delete_Status -eq 'Success'}).Count
$mismatchCount     = ($Result | Where-Object {$_.Status -eq 'LoginEmailMismatch - SkippedForSafety'}).Count

if ($mismatchCount -gt 0) {
    Write-Host "`n=== ВНИМАНИЕ: НЕСООТВЕТСТВИЯ ЛОГИН/ПОЧТА ===" -ForegroundColor Red
    Write-Host "Найдено $mismatchCount пользователь(ей), у которых логин AD не совпадает с почтой." -ForegroundColor Red
    Write-Host "Эти учетные записи НЕ обрабатывались (почта не тронута). Список: $exppath\login_email_mismatch.txt" -ForegroundColor Red
}

Write-Host "`n=== ИТОГОВАЯ СВОДКА ===" -ForegroundColor Cyan
Write-Host "Режим выполнения: $Mode" -ForegroundColor White
Write-Host "Всего обработано пользователей: $($adminbd.Count)" -ForegroundColor White
Write-Host "С почтой @adminbd.ru: $($UsersWithMailbox.Count)" -ForegroundColor White
Write-Host "Без почты @adminbd.ru: $($UsersWithoutMailbox.Count)" -ForegroundColor White

if ($TestMode) {
    Write-Host "`n=== РЕЖИМ ТЕСТИРОВАНИЯ ===" -ForegroundColor Yellow
    Write-Host "Ничего не было экспортировано или удалено" -ForegroundColor White
    Write-Host "Проверьте файл Results.csv для просмотра планируемых действий" -ForegroundColor White
}
elseif ($ExportOnlyMode) {
    Write-Host "`n=== РЕЖИМ ТОЛЬКО ЭКСПОРТА ===" -ForegroundColor Yellow
    Write-Host "Создано запросов экспорта: $($ExportRequests.Count)" -ForegroundColor White
    Write-Host "Пользователи не были удалены" -ForegroundColor White
}
else {
    Write-Host "Создано запросов экспорта: $($ExportRequests.Count)" -ForegroundColor White
    Write-Host "Успешных экспортов PST: $pstCompletedCount" -ForegroundColor White
    Write-Host "Удалено пользователей: $deletedCount" -ForegroundColor White
}

Write-Host "Результаты сохранены в: $exppath" -ForegroundColor White
Write-Host "Лог файл: $exppath\process.log" -ForegroundColor White
Write-Host "Аудит транскрипт: $transcriptPath" -ForegroundColor White
Write-Host "Бэкапы AD: $adBackupPath" -ForegroundColor White

Write-Host "`n=== ИНСТРУКЦИЯ ПО ЗАКРЫТИЮ СЕССИИ ===" -ForegroundColor Green
Write-Host "Не забудьте закрыть сессию Exchange после завершения работы:" -ForegroundColor White
Write-Host "Remove-PSSession `$Session -ErrorAction SilentlyContinue" -ForegroundColor Gray

 

Similar Posts:

Добавить комментарий

Яндекс.Метрика