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

Как настроить автоматическую очистку почтового ящика Exchange 2016: готовый скрипт и рекомендации

Написал PowerShell-скрипт, который:

  • находит письма старше заданного срока;

  • автоматически удаляет их пачками по 20 000 (с учётом лимитов командлета Search-Mailbox);

  • работает в тестовом режиме для проверки выборки;

  • делает резервный перенос в «безопасную папку» (опционально), а не только безвозвратное удаление.

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


Содержание

  1. Почему не подходит обычный Search-Mailbox?

  2. Особенности нашего скрипта

  3. Что нужно сделать перед запуском

  4. Пошаговая настройка параметров

  5. Как безопасно протестировать перед удалением

  6. Запуск в реальном режиме

  7. Возможные проблемы и как их избежать


Проблема лимита: почему не хватит одного вызова 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. Если письма с другой датой отправления не должны удаляться — запрос останется корректным.


Тестовый режим — обязателен!

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

  1. Выставьте $TestMode = $true.

  2. Запустите скрипт.

  3. Он скопирует первую пачку (до 20 000 писем) из очищаемого ящика в папку с суффиксом _ПРЕДПРОСМОТР в указанном $TargetMailbox.

  4. Откройте эту папку в Outlook или OWA и визуально проверьте, что там лежат именно те письма, которые нужно удалить (по дате, отправителю, теме).

  5. Если всё верно — удалите тестовую папку вручную, переключите $TestMode = $false и переходите к реальному запуску.

Тестовый режим не изменяет оригинальный ящик — письма остаются на месте.


Запуск в боевом режиме

Когда вы убедились, что условие отбора правильное, выполните:

  1. Установите $TestMode = $false.

  2. Определитесь с методом удаления:

    • Для перестраховки — оставьте $UseSafeMode = $true. Письма будут перенесены в папку, и вы всегда сможете вернуть их, если что‑то пошло не так.

    • Если ящик переполнен критично и вы уверены — можно выставить $UseSafeMode = $false для прямого удаления.

  3. Запустите скрипт.

  4. На консоли появится предупреждение о включенном 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:

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

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