описание скрипта архивации почтовых ящиков 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:
- Как сделать скрипт для архивировании почты exchange перед удаление учетной записи.
- Как создать учётные записи с помощью powershell в AD из csv
- Как сделать архив почтового ящика exchange 2016 в pst перед удалением уз.
- Как найти битые профили fslogix и найти профили которые не монтировались на сервер
- Как массово создать учётные записи в АД из csv файла