Написал PowerShell-скрипт, который:
-
находит письма старше заданного срока;
-
автоматически удаляет их пачками по 20 000 (с учётом лимитов командлета
Search-Mailbox); -
работает в тестовом режиме для проверки выборки;
-
делает резервный перенос в «безопасную папку» (опционально), а не только безвозвратное удаление.
Давайте разберем, как его применить, чтобы не потерять важные данные и не перегрузить сервер.
Содержание
-
Почему не подходит обычный Search-Mailbox?
-
Особенности нашего скрипта
-
Что нужно сделать перед запуском
-
Пошаговая настройка параметров
-
Как безопасно протестировать перед удалением
-
Запуск в реальном режиме
-
Возможные проблемы и как их избежать
Проблема лимита: почему не хватит одного вызова Search-Mailbox
Командлет Search-Mailbox в Exchange 2016 за один вызов обрабатывает не более 20 000 элементов (10 000 из основного хранилища + 10 000 из архива). Если под условие попадает 50 000 писем, однократный запуск удалит только первые двадцать тысяч, и скрипт завершится, оставив остальные.
Наш скрипт строит цикл: он повторяет удаление, пока очередная пачка не окажется пустой. Это гарантирует полную очистку, сколько бы писем ни накопилось.
Главные «фишки» скрипта
-
Циклическая обработка — автоматически повторяется до 100 раз (можно увеличить), пока письма не закончатся.
-
Безопасный тестовый режим — сначала вы копируете одну пачку писем в отдельную папку другого ящика и проверяете, что условие отбора (
Received <= дата) работает верно. -
Два способа удаления:
-
Safe Mode (рекомендуется) — письма переносятся в специальную папку целевого ящика (можно восстановить).
-
Безвозвратное удаление (
-DeleteContent) — письма удаляются из ящика напрямую (только для уверенных).
-
-
Проверка судебных удержаний (Hold) — если на ящике включён Litigation Hold или In‑Place Hold, скрипт предупредит и запросит подтверждение.
-
Пауза между итерациями — чтобы снизить нагрузку на диск и не вызвать проблемы с I/O.
-
Подробный лог — все действия сохраняются в текстовый файл.
Подготовка перед запуском
1. Права доступа
Запускать скрипт нужно из Exchange Management Shell (или обычного PowerShell с загруженным модулем Exchange) под учётной записью, у которой есть права на:
-
Get-Mailbox -
Search-Mailbox -
доступ к целевой папке и ящику (если используется Safe Mode).
2. Убедитесь, что ящик существует
Скрипт проверяет существование почтового ящика на старте, но лучше заранее выполнить Get-Mailbox -Identity нужный@адрес.
3. Целевой ящик для «безопасного» переноса (если планируете)
Если вы выставите $UseSafeMode = $true, обязательно укажите реальный существующий почтовый ящик, куда будут перемещаться старые письма. Лучше завести отдельную учётную запись для архивации (например, archive@company.ru).
Настройка скрипта для своих задач
Откройте скрипт в текстовом редакторе и найдите блок «НАСТРОЙКИ». Вам нужно изменить всего 4–5 параметров.
| Переменная | Описание | Пример |
|---|---|---|
$Mailbox |
Адрес очищаемого ящика | user@adminbd.ru |
$MonthsOld |
Срок давности (месяцев). Удаляются письма строго старше этого числа месяцев. | 1 (старше 1 месяца), 6, 12 |
$TestMode |
Если $true — только тестовая копия одной пачки. Если $false — реальное удаление. |
$true или $false |
$UseSafeMode |
$true — перенос в папку (восстановимо). $false — безвозвратное удаление. |
$true (рекомендуется на первых порах) |
$TargetMailbox |
Ящик для переноса (если $UseSafeMode = $true). |
mosk@adminbd.ru |
$TargetFolder |
Имя папки, куда складывать удаляемые письма. | Автоматически генерируется как Purged_имяЯщика_дата |
$PauseBetweenBatchesSec |
Пауза между пачками в секундах. | 30 (стандартно) |
$MaxIterations |
Защита от бесконечного цикла. Максимум пачек. | 100 (при 20 000 писем за пачку = 2 млн) |
Важно: Параметр
-MonthsOldиспользует дату отсечения по полюReceived. Если письма с другой датой отправления не должны удаляться — запрос останется корректным.
Тестовый режим — обязателен!
Никогда не запускайте скрипт в боевом режиме без предварительного теста.
Для этого:
-
Выставьте
$TestMode = $true. -
Запустите скрипт.
-
Он скопирует первую пачку (до 20 000 писем) из очищаемого ящика в папку с суффиксом
_ПРЕДПРОСМОТРв указанном$TargetMailbox. -
Откройте эту папку в Outlook или OWA и визуально проверьте, что там лежат именно те письма, которые нужно удалить (по дате, отправителю, теме).
-
Если всё верно — удалите тестовую папку вручную, переключите
$TestMode = $falseи переходите к реальному запуску.
Тестовый режим не изменяет оригинальный ящик — письма остаются на месте.
Запуск в боевом режиме
Когда вы убедились, что условие отбора правильное, выполните:
-
Установите
$TestMode = $false. -
Определитесь с методом удаления:
-
Для перестраховки — оставьте
$UseSafeMode = $true. Письма будут перенесены в папку, и вы всегда сможете вернуть их, если что‑то пошло не так. -
Если ящик переполнен критично и вы уверены — можно выставить
$UseSafeMode = $falseдля прямого удаления.
-
-
Запустите скрипт.
-
На консоли появится предупреждение о включенном Hold (если он есть) и запрос на ввод слова УДАЛИТЬ (в верхнем регистре) — это финальный предохранитель.
После подтверждения начнётся цикл удаления. В логе вы увидите каждую итерацию, количество обработанных писем и общий итог.
Основные риски и решения
1. На ящике включен Litigation Hold
-
Причина: письма не удаляются физически, а уходят в папку Recoverable Items, занимая место.
-
Решение: скрипт выведет предупреждение. Либо временно отключите Hold (согласовав с юристами/архивариусами), либо примите факт, что место в базе данных не освободится.
2. В ящике больше 2 000 000 писем
-
$MaxIterationsпо умолчанию = 100, что достаточно для 2 млн писем (100 × 20 000). Если у вас больше — увеличьте это значение.
3. Ошибка доступа к целевому ящику (Safe Mode)
-
Убедитесь, что у вас есть права на запись в
$TargetMailbox. Можно выполнитьSearch-Mailbox -Identity $TargetMailbox -EstimateResultOnlyдля проверки.
4. Пауза между итерациями не помогает при очень высокой нагрузке
-
Увеличьте
$PauseBetweenBatchesSecдо 60–120 секунд. Это снизит нагрузку на дисковую подсистему и позволит серверу «дышать».
5. После удаления место не освободилось сразу
-
В Exchange база данных освобождает пространство со временем (в процессе обслуживания). Можно запустить дефрагментацию базы (офлайн), но обычно достаточно подождать несколько часов.
Заключение
Представленный скрипт — это готовое решение для регулярной автоматической очистки почтовых ящиков в среде Exchange 2016 (on‑premises). Он:
-
безопасен — тестовый режим и финальное подтверждение;
-
гибок — можно выбрать перенос или удаление;
-
масштабируем — цикл отработает любое количество писем;
-
прозрачен — ведётся полный лог.
Встроив этот скрипт в ежемесячный регламент (по расписанию через Task Scheduler), вы избавитесь от ручной чистки и продлите жизнь своим почтовым базам.
Скачать готовый скрипт можно по ссылке ниже (или скопируйте его из листинга в начале статьи). Не забудьте изменить параметры под свою организацию.
Важно: перед любыми массовыми операциями с почтой сделайте полную резервную копию базы данных Exchange. Ответственность за удаление данных лежит на администраторе. Будьте внимательны и всегда тестируйте на тестовом ящике.
# ============================================================
# СКРИПТ УДАЛЕНИЯ СТАРЫХ ПИСЕМ ИЗ ЯЩИКА (Exchange 2016 on-prem)
# Учитывает лимит Search-Mailbox: до 20 000 писем за один вызов
# (10 000 Primary + 10 000 Archive), поэтому удаление идёт циклом.
# ============================================================
# ---------------- НАСТРОЙКИ (редактировать здесь) ----------------
$Mailbox = "user@adminbd.ru"
$MonthsOld = 1
$TestMode = $true # $true = только посмотреть выборку (одна пачка, без удаления)
# $false = реальное удаление ЦИКЛОМ до полного завершения
$UseSafeMode = $true # $true = переносить в TargetMailbox/TargetFolder (восстановимо)
# $false = -DeleteContent без переноса (безвозвратно)
$TargetMailbox = "mosk@adminbd.ru"
$TargetFolder = "Purged_$($Mailbox.Split('@')[0])_$(Get-Date -Format 'yyyyMMdd')"
$PauseBetweenBatchesSec = 30 # пауза между итерациями удаления (разгрузка диска)
$MaxIterations = 100 # предохранитель от бесконечного цикла (100 x 20000 = 2 млн писем, с запасом)
$LogFile = "C:\Logs\MailboxCleanup_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
# -------------------------------------------------------------
function Write-Log {
param([string]$Message, [string]$Color = "White")
$timestamp = Get-Date -Format 'HH:mm:ss'
$line = "[$timestamp] $Message"
Write-Host $line -ForegroundColor $Color
$line | Out-File -FilePath $LogFile -Append -Encoding UTF8
}
$logDir = Split-Path $LogFile -Parent
if (-not (Test-Path $logDir)) {
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
}
Write-Log "================================================" "Green"
Write-Log "СКРИПТ ОЧИСТКИ ЯЩИКА: $Mailbox" "Green"
Write-Log "================================================" "Green"
# ---------------- ПРОВЕРКА ЯЩИКА ----------------
try {
$mbx = Get-Mailbox -Identity $Mailbox -ErrorAction Stop
} catch {
Write-Log "ОШИБКА: не удалось найти ящик $Mailbox. $_" "Red"
return
}
# ---------------- ПРОВЕРКА HOLD ----------------
if ($mbx.LitigationHoldEnabled -or $mbx.InPlaceHolds) {
Write-Log "ВНИМАНИЕ: на ящике включён Litigation/In-Place Hold!" "Red"
Write-Log "Письма могут не удалиться полностью или остаться в Recoverable Items." "Red"
$holdConfirm = Read-Host "Продолжить всё равно? (yes/no)"
if ($holdConfirm -ne "yes") {
Write-Log "Отменено пользователем из-за Hold." "Yellow"
return
}
}
# ---------------- ФОРМИРОВАНИЕ ЗАПРОСА ----------------
$CutoffDate = (Get-Date).AddMonths(-$MonthsOld)
$FormattedDate = $CutoffDate.ToString("MM/dd/yyyy")
$Query = "Received<=$FormattedDate"
Write-Log "Дата отсечения: $($CutoffDate.ToString('dd.MM.yyyy')) (старше $MonthsOld мес.)" "Yellow"
Write-Log "Поисковый запрос: $Query" "Yellow"
Write-Log "Режим: $(if ($TestMode) {'ТЕСТ (одна пачка, без удаления)'} else {'РЕАЛЬНОЕ УДАЛЕНИЕ (циклом)'})" "Yellow"
Write-Log "Способ: $(if ($UseSafeMode) {"перенос в $TargetMailbox\$TargetFolder"} else {'БЕЗВОЗВРАТНОЕ (-DeleteContent)'})" "Yellow"
Write-Log "ВАЖНО: Search-Mailbox отдаёт максимум 20000 писем за вызов (10000 Primary + 10000 Archive)." "DarkGray"
Write-Log "Реальное общее количество писем под условие может быть значительно больше." "DarkGray"
# ---------------- ШАГ 1: ОЦЕНКА (одна пачка, для ориентира) ----------------
Write-Log "`n--- ШАГ 1: Оценка (первая пачка, максимум 20000) ---" "Cyan"
try {
$estimate = Search-Mailbox -Identity $Mailbox -SearchQuery $Query -EstimateResultOnly -ErrorAction Stop
} catch {
Write-Log "ОШИБКА при оценке: $_" "Red"
return
}
Write-Log "В первой пачке: $($estimate.ResultItemsCount) писем, размер: $($estimate.ResultItemsSize)" "Cyan"
if ($estimate.ResultItemsCount -ge 20000) {
Write-Log "Достигнут потолок 20000 — реальное количество писем под условие БОЛЬШЕ. Точное число можно узнать только через New-ComplianceSearch / New-MailboxSearch." "Yellow"
}
if ($estimate.ResultItemsCount -eq 0) {
Write-Log "Писем под условие не найдено. Завершение." "Green"
return
}
# ---------------- ШАГ 2: ТЕСТОВЫЙ ПРОСМОТР (одна пачка, без удаления) ----------------
if ($TestMode) {
Write-Log "`n--- ШАГ 2: ТЕСТ — просмотр примера писем БЕЗ удаления ---" "Magenta"
Write-Log "Перенесу первую пачку (до 20000) в '$TargetFolder`_ПРЕДПРОСМОТР' в ящике '$TargetMailbox', чтобы вы могли открыть и проверить письма глазами." "Magenta"
Write-Log "Оригиналы НЕ трогаются (нет -DeleteContent)." "Magenta"
$testResult = Search-Mailbox -Identity $Mailbox `
-SearchQuery $Query `
-TargetMailbox $TargetMailbox `
-TargetFolder "$TargetFolder`_ПРЕДПРОСМОТР" `
-Force `
-ErrorAction Stop
Write-Log "Скопировано в предпросмотр: $($testResult.ResultItemsCount) писем." "Green"
Write-Log "`nОткройте папку '$TargetFolder`_ПРЕДПРОСМОТР' в ящике $TargetMailbox и проверьте содержимое." "Yellow"
Write-Log "Когда убедитесь, что выборка верная — удалите тестовую папку вручную, установите `$TestMode = `$false` и запустите скрипт снова для полного удаления." "Yellow"
return
}
# ---------------- ШАГ 3: ПОДТВЕРЖДЕНИЕ ПЕРЕД РЕАЛЬНЫМ УДАЛЕНИЕМ ----------------
Write-Log "`n--- ШАГ 3: РЕАЛЬНОЕ УДАЛЕНИЕ (циклом, до полного завершения) ---" "Red"
Write-Host ""
Write-Host "!!! ВНИМАНИЕ !!!" -ForegroundColor Red
Write-Host "Скрипт будет запускать Search-Mailbox многократно (пачками до 20000), пока письма не закончатся." -ForegroundColor Red
Write-Host "Способ: $(if ($UseSafeMode) {"перенос в $TargetMailbox\$TargetFolder (восстановимо)"} else {'БЕЗВОЗВРАТНОЕ УДАЛЕНИЕ (-DeleteContent)'})" -ForegroundColor Red
Write-Host ""
$finalConfirm = Read-Host "Введите ТОЧНО слово 'УДАЛИТЬ' для подтверждения"
if ($finalConfirm -ne "УДАЛИТЬ") {
Write-Log "Отменено пользователем на финальном подтверждении." "Yellow"
return
}
# ---------------- ШАГ 4: ЦИКЛ УДАЛЕНИЯ ----------------
Write-Log "`nНачинаю удаление циклом..." "Green"
$Iteration = 1
$TotalDeleted = 0
do {
Write-Log "Итерация #$Iteration. Обработка очередной пачки (до 20000)..." "Magenta"
try {
if ($UseSafeMode) {
$result = Search-Mailbox -Identity $Mailbox `
-SearchQuery $Query `
-TargetMailbox $TargetMailbox `
-TargetFolder $TargetFolder `
-DeleteContent `
-Force `
-ErrorAction Stop
} else {
$result = Search-Mailbox -Identity $Mailbox `
-SearchQuery $Query `
-DeleteContent `
-Force `
-ErrorAction Stop
}
} catch {
Write-Log "ОШИБКА на итерации #$Iteration : $_" "Red"
break
}
$deletedCount = $result.ResultItemsCount
$TotalDeleted += $deletedCount
Write-Log "Итерация #$Iteration завершена. Обработано: $deletedCount. Всего с начала: $TotalDeleted." "Cyan"
if ($deletedCount -gt 0 -and $Iteration -lt $MaxIterations) {
Write-Log "Пауза $PauseBetweenBatchesSec сек. перед следующей итерацией..." "DarkGray"
Start-Sleep -Seconds $PauseBetweenBatchesSec
$Iteration++
}
} while ($deletedCount -gt 0 -and $Iteration -le $MaxIterations)
if ($Iteration -gt $MaxIterations) {
Write-Log "ВНИМАНИЕ: достигнут предохранитель $MaxIterations итераций. Возможно, писем ещё больше — перезапустите скрипт." "Yellow"
}
Write-Log "`n================================================" "Green"
Write-Log "ЗАВЕРШЕНО. Всего обработано писем: $TotalDeleted" "Green"
Write-Log "Полный лог сохранён в: $LogFile" "Green"
Write-Log "================================================" "Green"
Similar Posts:
- Как очистить Recoverable Items. Проблема долгая обработки почты в exchange 2016.
- Как автоматически архивировать почтовые ящики Exchange и безопасно удалять отключенные учетные записи Active Directory с помощью PowerShell
- Как исправить ошибку Database is mandatory on UserMailbox exchange.
- Как сделать автоматическое создание почтового ящика exchange.
- Как подключиться к почте Exchange через Powershell на другом сервере и создать почтовый ящик пользователю.