Подчищаем хвосты за Microsoft Exchange Server 2016 используя Powershell

    image

    Работая в течении полугода с Microsoft Exchange Server 2016 в компании, где более 500 сотрудников использует корпоративную почту, я столкнулся с проблемой полноценного удаления информации о пользователях, отключенных в Active Directory.

    Задачи, которые мы хотим автоматизировать, после отключения учетки пользователя в AD:

    • Экспорт всех писем из основого и архивного ящика в .pst файл;
    • Полная блокировка почтового ящика после экспорта писем;
    • Очистка всех списков рассылки от «мертвых пользователей» (автоматически не очищается);
    • Обновление Global Address List и Offline Address Book, чтобы активные пользователи не видели отключенных.

    Испытывая полнейшую нелюбовь к ручной работе, было принято решение максимально автоматизировать все эти задачи с помощью PowerShell.

    Подготовка:


    Подключаем библиотеку Exchange Management PowerShell:

    Add-PSSnapin Microsoft.Exchange.Management.PowerShell.SnapIn;
    

    Получаем список всех отключенных в Active Directory пользователей и исключаем некоторые служебные записи:

    $DisableUsers = get-user -Filter {(UserAccountControl -eq 'AccountDisabled, NormalAccount') -and (RecipientType -eq 'UserMailbox')} | ? {($_.SamAccountName -ne 'krbtgt') -and ($_.SamAccountName -ne 'SM_2013a5b0c2bd4ca2a') -and ($_.SamAccountName -ne 'testvc')}
    

    Объявляем переменные:

    # Объявляем переменную для объединения нескольких запросов на экспорт.
    $BatchName = 'MassRequest'
    # Создаем пути для экспорта
    $CMounth = (Get-Date).month 
    $CYear = (Get-Date).year 
    $CurrentDate = "$CYear.$CMounth" # Получаем будущее имя папки вида Год.Месяц
    $MainDir = "\\%Ваш путь%"
    $ExportPath = $MainDir + $CurrentDate + "\"
    

    Обработка:


    Чтобы было удобнее найти .pst архив уволенного пользователя, было принято решение создавать папку вида Год.Месяц. Так, все уволенные пользователи в апреле 2017 года попадут в папку 2017.4, уволенные в мае в папку 2017.5 и тд.

    # Проверяем, есть ли уже папка Год.Месяц, если нет, то создаем.
    if ((Test-Path $ExportPath -PathType Container) -eq $false){
        New-Item -Path $MainDir -Name $CurrentDate -ItemType "directory"
    } 
    

    В цикле по отключенным пользователям выгружаем их почту в .pst файл из основного и архивного почтовых ящиков и сохраняем в папку Год.Месяц.

    С помощью параметра -BatchName объединяем запросы под одним именем, для возможности отслеживать статус всей выгрузки сразу, а не каждый запрос отдельно.

    foreach($User in $DisableUsers){
    $PrimaryPath = $ExportPath + $User.SamAccountName + ".pst"
    $ArhivePath = $ExportPath + $User.SamAccountName + "_Archive.pst"
    
    New-MailboxExportRequest -Mailbox $User.SamAccountName -BatchName $BatchName -FilePath $PrimaryPath
    New-MailboxExportRequest -Mailbox $User.SamAccountName -BatchName $BatchName -FilePath $ArhivePath -IsArchive     
    } 
    

    Ждем, пока скрипт закончит работу. Ждать нужно обязательно, т.к. дальше мы переводим ящики в статус Disable и хотим быть уверены, что перед этим выгрузка почты закончилась.

    # Ждем, пока скрипт закончит работу
    $i=1;
    while ((Get-MailboxExportRequest -BatchName $BatchName | Where {($_.Status -eq “Queued”) -or ($_.Status -eq “InProgress”)})) {
        sleep 60
        Write-Host "Скрипт работает $i минут. Ожидаем завершения.."
        $i=$i+1
    }
    

    После завершения экспорта удаляем все запросы, которые получили статус Completed

    # После завершения экспорта удаляем все запросы
    Get-MailboxExportRequest -Status Completed | Remove-MailboxExportRequest -Confirm:$false 
    

    Первую часть сделали, начинаем чистить списки рассылки. Для начала получаем массив всех списков:

    # Начинаем чистить списки рассылок. Сначала получаем их полный список. 
    $DistribList = Get-DistributionGroup
    

    В цикле пробегаемся по всем спискам рассылки и удаляем отключенных пользователей:

    # В цикле удаляем отключенных пользователей из всех списков
    foreach($List in $DistribList){
        foreach($User in $DisableUsers){        
            Remove-DistributionGroupMember -Identity $List -Member $User -Confirm:$false -ErrorAction Ignore  
        }
    } 
    

    Предпоследний этап: отключение почтовых ящиков. Из учетки пользователя в AD пропадает E-mail, а сам почтовый ящик удаляется. Теперь его только в течении некоторого времени можно восстановить стандартными средствами Exchange.

    # Начинаем отключать почтовые ящики, после чего они пропадут из адресной книги
    foreach($User in $DisableUsers){
        Disable-Mailbox -Identity $User.SamAccountName -Archive -Confirm:$false 
        Disable-Mailbox -Identity $User.SamAccountName -Confirm:$false       
    }
    

    Обновляем GAL и OAB, чтобы пользователи увидели изменения как можно быстрее.

    # Обновляем Global Adress List, чтобы клиенты увидели изменения в адресной книге
    Get-GlobalAddressList | Update-GlobalAddressList
    Get-OfflineAddressBook | Update-OfflineAddressBook
    Get-AddressList | Update-AddressList
    

    Небольшой комментарий:


    В своей фирме мы прицепили эту обработку к кастомной кнопке в 1С. Отдел кадров в профиле сотрудника выставляет ему статус «Уволен» и скрипт начинает работать.

    Тем самым отключенных пользователей в адресной книге практически невозможно увидеть, а уволенный сотрудник сразу теряет доступ к почте. (Если только выключить учетку в Active Directory, то зайти в почту сотрудник все равно может, что по нашей корпоративной политике недопустимо).

    Надеюсь кому-то скрипт будет полезен. Спасибо!
    Share post

    Comments 13

      0
      > Если только выключить учетку в Active Directory, то зайти в почту сотрудник все равно может, что по нашей корпоративной политике недопустимо

      Вот этот кусок сильно смущает. Как сотрудник может зайти в почту, если его учетка disabled? Или проблема в репликации AD?
        –3
        Авторизацию в почту идет по логину и паролю из домена. Если мы заблокировали учетку пользователя в AD, то он не может авторизоваться на рабочей станции и запустить Outlook.
        Но если с другого утройства он зайдет на веб-морду почтовика, то под своими, уже заблокированными учетными данными, он все равно сможет в почту войти.
        Вроде как отправлять почту он не сможет, но всю переписку может прибить или скопировать что-то важное.
        С архивной почтой это тоже работает.
          0
          Вот реально не верю. Чтобы проходила авторизация на веб-морде (owa) под заблокированными учетными данными, либо должна быть ситуация, при которой owa при проверке получает ответ «не заблокировано», либо есть баг в Exchange, от которого надо избавляться любой ценой (то есть запросить Микрософт и выяснить, почему это у вас такое позволительно). Одно дело если это кэшированная сессия подключенного мобильного устройства (ActiveSync) — тогда надо после блокирования пользователя делать
          Get-MobileDevice -mailbox $disableduser.mail | Remove-MobileDevice
          (можно даже сделать clear-mobiledevice, если политикой требуется), в этом случае все сессии activesync будут разорваны, и устройство, попытавшись получить доступ под сохраненными учетными данными, получит ошибку account disabled и не сможет ничего получить, тем более удалить из ящика. Но если реально после репликации AD заход с логином/паролем на owa даст пользователю доступ — это глюк.
            +2
            Просто CAS кеширует учетные данные, есть несколько статей на эту тему вроде https://support.microsoft.com/en-us/help/267568/an-old-password-still-works-after-you-change-it-in-outlook-on-the-web. Кто-то, кто заморачивается, отключает таким учеткам OWA.
            Но там кэш всего на 15 минут, вроде это ни для кого не было критичным. Если время менять, то OWA может начать притормаживать.
              0
              Интересно, и в принципе логично, однако это работает только если пользователь сразу перед тем, как его заблокировали, авторизовался на OWA. Тогда он, узнав, что его заблочили, может с веб-морды что-то удалить. А обойти такой косяк тоже можно, если запихать CAS-сервера в отдельный сайт, рядом к ним поставить DC/RODC и его посадить в тот же сайт. После чего уменьшить кэширование токенов на IIS на скажем три минуты (это может привести к тому, что пользователи начнут получать диалог «войдите заново», тогда лучше так все-таки не делать — но по описанию токен кэшируется, т.е. IIS будет воссоздавать токен без запроса логина/пароля, так как сессия авторизации пользователя на ПО не истекла, и хотя производительность и просядет слегка, пользователи не будут получать диалог повторной авторизации, но если их заблочили, то будут блокироваться быстрее). А если паранойя, то правильно будет именно оторвать доступ к owa при отключении пользователя, причем ещё до того, как экспортировать его почтовый ящик. Экспорт может занять довольно много времени, если ящик большой, а канал внезапно узкий.
                0
                Сложно всегда побороться с штатной работой продукта.
                А побыстрее отбивать, думаю получится через Disable-Mailbox, Update-StoreMailboxState, Connect-Mailbox к временной учетке и New-MailboxExportRequest.
                0
                Я не хочу разводить тут спор, но мы проводили тесты в продакшене. Доступ у заблокированной учетки остается до 24 часов и кэш тут не причем. Exchange получает инфу о отключении учетки только во время maintenance mode, что может длиться довольно долго.
                Поэтому мы предпочли ручками отключать эту mailbox.
          0
          Отличная тема! Спасибо!
          Может быть всю вашу задумку я не буду использовать, но частично обязательно реализую!

            0
            Можно так же использовать отрубание функционала для пользователя:

            set-casmailbox -owaenabled $false -activesyncenabled $false -popenabled $false -imapenabled $false -mapienabled $false -owafordevicesenabled $false


            Так же если используете линк, то обязательно делать disable юзеру, потому что даже если его заблочить, сменить пароль, еще с неделю он сможет использовать линк.

            get-csuser %username% | disable-csuser
              +1

              Процедурных вопросов касаться не буду, хотя там есть странности (например, зачем вообще оставлять окно между откючением учетки и удалением ящика, если уж вы так боитесь, что злобный вредитель зайдет в свою почту).


              Чисто по самому скрипту:


              Отдел кадров в профиле сотрудника выставляет ему статус «Уволен» и скрипт начинает работать.

              … работать, и еще раз работать, перелопачивая ВСЕ (!) отключенные учетки вместо того, чтобы работать только с теми, для которых выставлена галка.


              Проверяем, есть ли уже папка Год.Месяц, если нет, то создаем.

              Зачем вы вообще делаете эту проверку, если в дальнейшем всё равно нет никакой обработки ошибок? А вот не создался у вас путь по какой-то причине, что будет? Если вы только создаете файл — это ещё не так страшно. А если бы удаляли, да по маске?


              Ждем, пока скрипт закончит работу. Ждать нужно обязательно, т.к. дальше мы переводим ящики в статус Disable и хотим быть уверены, что перед этим выгрузка почты закончилась.

              А как вы можете быть в этом уверены, если вы даже статус задачи не проверяете по её окончании? А если там не Completed, а Failed?


              Get-MailboxExportRequest -Status Completed | Remove-MailboxExportRequest -Confirm:$false

              Снова тот же косяк, что в самом начале — не ограничивается область действия, удаляются ВСЕ запросы с подобным статусом, а не только созданные самим скриптом на предыдущих этапах.


              В своей фирме мы прицепили эту обработку к кастомной кнопке в 1С.

              А вы как-то учитывали, что у скрипта время исполнения, мягко говоря, ненулевое? Продумывали, что будет, если несколько инстансов скрипта окажутся запущены параллельно?


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

                0
                Молодцы, расширили мою идею)

                Не могу не добавить, что до 24 часов у сотрудника скорее всего будет доступ в почту через OWA/ActiveSync.
                Чтобы этого избежать — нужно делать reload APP пулов в IIS CAS Exchange.
                  0

                  То есть вот откуда эта засада по доступности OWA после блокирования учетной записи… Надо учесть, потому что такая реализация на IIS это бред редкостный и дыра в безопасности. Но помимо сброса всех IIS'ов обойти эту дыру можно, оторвав OWA и ActiveSync:


                  set-mailbox $user -activesyncenabled:$false -owaenabled:$false


                  Источник. Если посмотрите, в ветке выше эта проблема была заявлена.

                  +1
                  Может пригодиться:
                  Хранение настроек скрипта в файле
                  Хранение настроек скрипта в файле, после можно подписать файл скрипта, что бы избежать несанкционированного изменения скрипта…

                  #region Get setting from config
                  $myDir = Split-Path -Parent $MyInvocation.MyCommand.Path
                  #$myDir = "$pwd\"
                  If (!(Test-Path $MyDir\Settings.xml))  {write-host 'Setting file ' $MyDir\Settings.xml ' not found..' -ForegroundColor Red ; exit}
                  [xml]$ConfigFile = Get-Content "$MyDir\Settings.xml"
                  if ($ConfigFile -eq $null) {write-host 'Setting file ' $MyDir\Settings.xml ' has error...' -ForegroundColor Red ; exit}
                  
                  $global:Path2Export = $ConfigFile.Settings.Path2Export
                  $global:SecondsToSleep = $ConfigFile.Settings.SecondsToSleep
                  $global:ExportGroupName = $ConfigFile.Settings.ExportGroupName
                  $global:ExchangeServer = $ConfigFile.Settings.ExchangeServer
                  #endregion 
                  

                  Пример XML
                  <?xml version="1.0"?>
                  <Settings>
                    <Path2Export>\\FileServer.contoso.com\BackupPst$\</Path2Export>
                    <ExportGroupName>ExportDisableUser2PST</ExportGroupName>   
                    <SecondsToSleep>30</SecondsToSleep>
                    <ExchangeServer>ExchangeServer.contoso.com</ExchangeServer>
                  </Settings>
                  
                  


                  Права на папку для бэкапа:
                  New-MailboxExportRequest
                  На расшаренную папку необходимо предоставить права группе «Exchange Trusted Subsystem»
                  You need to grant the following permission to the group Exchange Trusted Subsystem to the network share where you want to export or import PST files:
                  * To import PST files from the share: Read permission
                  * To save exported PST files to the share: Read/Write permission.


                  Ожидание окончания очереди, с выводом процента выполненного объема:
                  Фрагмент выдран, и немного причесан…
                  Рисует два прогресс бара для ящика и архивного ящика, если он есть.
                  Очереди называются по маске с именем пользователя, поэтому всегда можно увидеть кто выгружается и кто завершился с ошибкой…
                  $NameParamQueuedMailbox=@{Name="$ExportGroupName-$ADUser"}
                  $NameParamQueuedArchiving=@{Name="$ExportGroupName-$ADUser-archiving"}
                  #....
                  $isarchiving=Get-Mailbox -Identity $ADUser|select archivedatabase|% {if ($_.archivedatabase -eq $null) {$false} else {$true}} 
                  #...
                  #region Waiting status and show progress
                  do {
                                  #region Mailbox progressbar
                                  $StatusExport = get-MailboxExportRequest @NameParamQueuedMailbox | select  -ExpandProperty Status
                                  $MailboxPercent=get-MailboxExportRequest @NameParamQueuedMailbox | Get-MailboxExportRequestStatistics|select -ExpandProperty PercentComplete
                                  Write-Progress -id 1 -Activity "Export $ADUser Mailbox" -status "Status $StatusExport complite  $MailboxPercent percent" -percentComplete $MailboxPercent
                                  #endregion
                                  #region Archiving mailbox progressbar
                                  if ($isarchiving) {
                                      $StatusExportArchiving = get-MailboxExportRequest @NameParamQueuedArchiving| select  -ExpandProperty Status
                                      $archivingPercent=get-MailboxExportRequest @NameParamQueuedArchiving| Get-MailboxExportRequestStatistics|select -ExpandProperty PercentComplete
                                      Write-Progress -Id 2 -ParentId 1 -Activity "Export $ADUser Archiving mailbox" -status "Status $StatusExportArchiving complite $archivingPercent percent" -percentComplete $archivingPercent
                                  } else {$StatusExportArchiving = 'Completed' }
                                  #endregion
                                  if ($StatusExport -eq 'Failed') {$StatusExport = 'Completed'} 
                                  if ($StatusExportArchiving -eq 'Failed' ) {$StatusExportArchiving = 'Completed'}
                                  if (($StatusExportArchiving -eq 'Completed') -and  ($StatusExport -eq 'Completed')) {$ExitWhile=$true} else {$ExitWhile=$false}
                  #Start-Sleep -s $SecondsToSleep #
                     } while ($ExitWhile -ne $true)
                  #endregion
                  

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

                  Запуск из/через CMD
                  <# :
                    @echo off
                      if '%1' == '' echo Please run %0 scrip with param - Username or email&&echo.&&echo %0 DisableUser&&exit
                  
                      set curentpath=%~dp0
                      cd  /d %curentpath:~0,-1%
                  
                      powershell /nologo /noprofile /command ^
                  		 "&{[ScriptBlock]::Create((cat """%~f0""") -join [Char[]]10).Invoke(@(&{$args}%*))}"
                    exit /b
                  #>
                  #Ниже вставляем PowerShell Script
                  #$myDir = Split-Path -Parent $MyInvocation.MyCommand.Path
                  $myDir = "$pwd\"
                  
                  ....
                  


                  Only users with full accounts can post comments. Log in, please.