Задача: нужно сменить расширение всех файлов в определённой директории, скажем с .txt на .log.

Задача имеет простое решение в PoSh:

Get-ChildItem -Path <SomePath> -Filter '*.txt' | Rename-Item -NewName {$_.name 
-replace '.txt','.log'}

Полезные ссылки:
Use PowerShell to Rename Files in Bulk

Задача: имеется список guid-ов объектов в AD, для которых необходимо получить некоторый набор свойств. Объекты могут быть как пользователями, так и группами или контактами. Искать нужно внутри многодоменного леса, объекты могут быть внутри любого домена в лесе. Найти объект, вне зависимости от его типа, можно через командлет Get-ADObject:

Get-ADObject $guid

Небольшое затруднение возникает, если необходимо делать поиск по всем доменам одного леса. Очевидное решение в лоб – взять список доменов и в каждом из них провести поиск:

$Domains = (Get-ADForest).Domains

ForEach ($Domain in $Domains)
{
$Search = Get-ADObject $guid -Server (Get-ADDomain $Domain).PDCEmulator -ErrorAction Stop
}

Работать будет, но с ошибками. Очевидно, что по-конкретному guid-у объект будет возвращаться только из одного домена. Поиск в остальных доменах вернёт ошибку, так как такого объекта там не будет. Поэтому имеет смысл внести в скрипт try-catch:

$Domains = (Get-ADForest).Domains
$Results = @()
ForEach ($Domain in $Domains)
{
  try
  {
    $Search = Get-ADObject $guid -Server (Get-ADDomain $Domain).PDCEmulator -ErrorAction Stop
    $Found = $true
  }
  catch
  {
    $Found = $false
  }
  If ($Found)
  {
    $Results += New-Object PSObject -Property @{
    guid = $guid
    Domain = $Domain
    DistinguishedName = $Search.DistinguishedName
    ObjectClass = $Search.ObjectClass
    Found = $Found
    }
  }
}

Добавляем загрузку из txt-файла списка guid-ов:

$guids = Import-Csv -Path C:\guids.txt
$Domains = (Get-ADForest).Domains
$Results = @()
ForEach ($guid in $guids)
{
  ForEach ($Domain in $Domains)
  {
    try
    {
      $Search = Get-ADObject $guid -Server (Get-ADDomain $Domain).PDCEmulator -ErrorAction Stop
      $Found = $true
    }
    catch
    {
      $Found = $false
    }
    If ($Found)
    {
      $Results += New-Object PSObject -Property @{
      guid = $guid
      Domain = $Domain
      DistinguishedName = $Search.DistinguishedName
      ObjectClass = $Search.ObjectClass
      Found = $Found
      }
    }
  }
}
$Results | Where { $_.Found } | Select guid,Domain,DistinguishedName,ObjectClass,Found

Задача решена и можно запускать скрипт и ждать. Если доменов у нас с десяток, объектов для поиска больше сотни, то ждать придётся долго. Что с эти делать? Искать в глобальном каталоге! Правда, стоит помнить, что он содержит не все аттрибуты объектов. Для поиска в глобальном каталоге используем тот же Get-ADObject, но в ключе -Server нужно будет указать любой ближайший глобальный каталог, по-умолчанию доступный при обращении к порту 3268:

$LocalSite = (Get-ADDomainController -Discover).Site
$NewTargetGC = Get-ADDomainController -Discover -Service GlobalCatalog -SiteName $LocalSite
if (!$NewTargetGC)
{ $NewTargetGC = Get-ADDomainController -Discover -Service GlobalCatalog -NextClosestSite }
$NewTargetGCHostName = $NewTargetGC.HostName
$LocalGC = "$NewTargetGCHostName" + ":3268"

Try-catch при таком поиске нам будет нужен разве что для обработки поиска несуществующих guid-ов. Скрипт немного упростится:

$guids = Import-Csv -Path C:\guids.txt
$Results = @()
$LocalSite = (Get-ADDomainController -Discover).Site
$NewTargetGC = Get-ADDomainController -Discover -Service GlobalCatalog -SiteName $LocalSite
if (!$NewTargetGC)
{ $NewTargetGC = Get-ADDomainController -Discover -Service GlobalCatalog -NextClosestSite }
$NewTargetGCHostName = $NewTargetGC.HostName
$LocalGC = "$NewTargetGCHostName" + ":3268"
ForEach ($guid in $guids) 
{
  Try 
  {
    $Search = Get-ADObject -Identity $guid.guid -Server $LocalGC
  }
  Catch {
  }
  $Results += New-Object PSObject -Property @{
  guid = $guid.guid
  DistinguishedName = $Search.DistinguishedName
  ObjectClass = $Search.ObjectClass
  }
}
$Results | Select guid,DistinguishedName,ObjectClass

Скрипт при этом отрабатывать будет на пару порядков быстрее.

Полезные ссылки:
Powershell Code: Discover a Local Global Catalog for Forest Queries
PowerShell Code: Find User in Active Directory Forest
Get-ADDomainController
Get-ADObject

e2010Ранее я вскользь касался момента с переносом общих папок из базы на одном сервере в базу на другом сервере. Использование скрипта MoveAllReplicas.ps1 помогает в большинстве случаев. Но иногда, возникает ситуация, когда конкретная реплика общей папки не удаляется из базы общих папок. То есть в свойствах общей папки в репликах отсутствует некоторая база общих папок, но де-факто реплика из этой базы по какой-то причине не удалилась. Если таких папок немного, то можно их выгрузить в pst-файл и папку пересоздать. Если таких папок много – то приходится искать другие механизмы. Самый простой – попробовать добавить реплику общей папку в проблемную базу и удалить её через скрипт RemoveReplicaFromPFRecursive.ps1. Проще всего это сделать через PowerShell:

#$PFs - содержит список неудалившихся реплик
$PFs = Get-PublicFolderStatistics -Server pfserver -ResultSize unlimited | Get-PublicFolder
#$PFDB - база, которую вычищаем
$PFDB = Get-PublicFolderDatabase -Server pfserver

foreach ($PF in PFs) {
#Получаем список реплик и добавляем в них проблемную базу
$repl = Get-PublicFolder -Identity $PF.Identity -Server pfserver
$repl.Replicas+= $PFDB.Identity
Set-PublicFolder -Identity $PF.Identity -Replicas $repl.Replicas

#Обновляем содержимое проблемных реплик
Update-PublicFolder -Identity $PF.Identity -Server pfserver;
}

По итогам выполнения скрипта запускаем процедуру удаления реплики:

.RemoveReplicaFromPFRecursive.ps1 -TopPublicFolder  -ServerToRemove pfserver

После этого ждём, пока отработает этот скрипт. Можно отслеживать процесс относительно интерактивно. Для этого необходимо повысить уровень логгирования для баз общих папок на проблемном сервере:

Set-EventLogLevel 'psserverMSExchangeIS9001 PublicReplication*' -Level High

События в логе с номером 3005 будут сигнализировать о том реплика общей папки была удалена из базы общих папок на проблемном сервере:

Log Name:      Application
Source:        MSExchangeIS Public Store
Event ID:      3005
Task Category: Replication AD Updates
Level:         Information
Keywords:      Classic
User:          N/A
Computer:      psserver
Description:
A replica of public folder (6-6891E5C6D7) IPM_SUBTREESome Folder
was removed.
Database PF-DATABASE.

PowerShell LogoИлья Сазонов недавно жаловался на проблему массового удаления большого количества объектов из папки RecoverableItems. Решил он эту проблему с помощью редактора MFCMapi. Но, задача так же имеет решение через EWS. При этом необходимо держать в уме Throttling Policy, которые, возможно, необходимо будет подправить для учётной записи, из под которой мы будем удалять объекты. Не растекаясь долго мысью по древу просто выложу скрипт, который удаляет объекты из папки RecoverableItems ящика с адресом user.name@domain.com.

Import-Module -Name "C:Program FilesMicrosoftExchangeWeb Services2.0Microsoft.Exchange.WebServices.dll"
$FromMailbox = "user.name@domain.com"

$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2)
# Impersonation
$service.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress,$FromMailbox)
# Адрес CAS-сервера получаем через службу автообнаружения
$service.AutodiscoverUrl($FromMailbox)

# Размер страницы вывода (количество объектов, возвращаемых за один раз)
$pageSize = 50
$Offset = 0
do {
$ItemView = new-object Microsoft.Exchange.WebServices.Data.ItemView($pageSize,$Offset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning)
# Получаем объекты из папки RecoverableItemsRoot, параметры вывода берём из переменной $ItemView
$FindItems = $service.FindItems([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::RecoverableItemsRoot, $ItemView)
foreach ($Item in $FindItems.Items){
# Операция жёсткого удаления объекта
$Item.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete) }
$Offset += $pageSize }
# Продолжаем перебор объектов, пока остаются непросмотренные
while ($FindItems.MoreAvailable)

Полезные ссылки:
EWS throttling in Exchange
WellKnownFolderName enumeration
ExchangeService.FindItems method (FolderId, ViewBase)
DeleteMode enumeration
ItemView constructor (Int32, Int32, OffsetBasePoint)
OffsetBasePoint enumeration
Configuring Exchange Impersonation

PowerShell LogoВ Outlook пользователь на конкретные папки может достаточно гибко раздавать права доступа другим пользователям организации. Обычно этот сценарий используется руководителями, которые делегируют часть прав на свой ящик секретарю. И в некоторый момент может понадобиться сделать простейший аудит прав. Права на конкретную папку в почтовом ящике пользователя можно получить с командлетом Get-MailboxFolderPermission. Например:

[PS] C:>Get-MailboxFolderPermission stbul:calendar | fl User, AccessRights

User         : Default
AccessRights : {AvailabilityOnly}

User         : Anonymous
AccessRights : {None}

Главная сложность в том, что в качестве основного параметра в этот командлет необходимо передавать имя/идентификатор папки. И в ответ мы получаем не конкретные значения, а объекты, которые нам интересны не полностью, а только некоторые их свойства (User и AccessRights). То есть необходимо получить полный список папок ящика пользователя и по каждой из них получить набор пар User-AccessRights. Полный список папок ящика пользователя проще всего получить через командлет Get-MailboxFolderStatistics, который вернёт как имена папок, так и их идентификаторы (которые использовать проще, чем имена папок). Итоговый скрипт выглядит примерно так:

. 'C:Program FilesMicrosoftExchange ServerV14binRemoteExchange.ps1'
Connect-ExchangeServer -auto sleep 5

$Mailbox = Get-Mailbox mailbox.name
$MbxStat = Get-MailboxFolderStatistics $Mailbox

foreach ($item in $MbxStat){
    $folder = $Mailbox.Name + ":" + $item.FolderId
    $FolderPerms = Get-MailboxFolderPermission $folder
    foreach ($FolderPerm in $FolderPerms){
        Write-Host $item.FolderPath " | " $FolderPerm.User " | " $FolderPerm.AccessRights }
}

Полезные ссылки:
Get-MailboxFolderPermission
Get-MailboxFolderStatistics

PowerShell LogoПериодически приходится писать крипты, автоматизирующие те или иные действия администратора. Иногда это бывает обычный сбор информации. Иногда это внесение изменений в конфигурацию/учётные записи, связанные с наступлением тех или иных событий. Тонкость возникает там, где необходимо работать с объектами Exchange. обычно, для этого достаточно в скрипте импортировать оснастку PS Microsoft.Exchange.Management.PowerShell.E2010 (Excahnge 2010):

Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010

Однако, в случае, когда нам в скрипте необходимо использовать командлет Get-MailboxStatistics получаем следующую ошибку:

PS C:> Get-Mailbox user | Get-MailboxStatistics
Get-MailboxStatistics : Failed to commit the change on object "MB-DATABASE" because access is denied.
At line:1 char:45
+ Get-Mailbox buldakov | Get-MailboxStatistics <<<<
+ CategoryInfo          : NotSpecified: (0:Int32) [Get-MailboxStatistics], MapiAccessDeniedException
+ FullyQualifiedErrorId : 46BEBAF0,Microsoft.Exchange.Management.MapiTasks.GetMailboxStatistics

Проверка прав для учётки, из под которой запускается скрипт показала, что права на запуск есть. Тем более странной стала выглядеть ситуация.

PS C:>Get-ManagementRoleEntry "*Get-MailboxStatistics" | ? {$_.Role -like "Mail  Recipients"} | fl Name,Role
Name : Get-MailboxStatistics
Role : Mail Recipients

PS C:>Get-ManagementRoleAssignment -Role "Mail Recipients" -GetEffectiveUsers | ? {$_.EffectiveUserName -like "svc_Name"} | fl Role,EffectiveUserName
Role              : Mail Recipients
EffectiveUserName : svc_Name

Нашёлся следующий workaround. Для подключения к Exchange используем стандартный скрипт RemoteExchange.ps1:

. 'C:Program FilesMicrosoftExchange ServerV14binRemoteExchange.ps1'
Connect-ExchangeServer -auto

Точку с пробелом в начале ставить обязательно.

e2010Следующий сценарий родился в процессе миграции пользователей с одного из сторонних почтовиков. Итак, у нас имеется набор почтовых ящиков, которые созданы для миграции со стороннего почтового решения. В некоторый момент времени к стандартным smtp-адресам (например, domain.com) добавляются smtp-адреса, которые находятся у пользователей во внешней почтовой системе (например, домен domain.net). Так как изначально процесс не сильно автоматизирован, то в некоторый момент времени оказывается, что не все почтовые ящики имеют smtp-адрес из домена domain.net. А час X (когда мы добавим домен domain.net в авторизованные) приближается и необходимо выяснить какие-же почтовые ящики мы не охватили. То есть необходимо найти те почтовые ящики (предположим, что мы их пока складируем в отдельных почтовых базах), которые примут пользователей при миграции, но при этом старый smtp-адрес для них не прописан.

Первым делом давайте посмотрим в каком виде у нас хранятся smtp-адреса. Все они находятся в свойстве EmailAddresses почтового ящика. Свойство это многозначное, то есть фактически представляет собой некий массив разнородных данных (помним, что кроме smtp-адресов в этом свойстве могут записываться и другие типы адресов, например, sip или X400). При обращении к нему выведется набор объектов, каждый из которых представляет определённый адрес, прописанный в свойствах ящика:

[PS] C:>(Get-Mailbox stbul).EmailAddresses

SmtpAddress        : stbul@domain.com
AddressString      : stbul@domain.com
ProxyAddressString : SMTP:stbul@domain.com
Prefix             : SMTP
IsPrimaryAddress   : True
PrefixString       : SMTP

AddressString      : C=RU;A= ;P=DOMAIN;O=DOMAIN;S=Buldakov (Test);G=Stanislav;
ProxyAddressString : X400:C=RU;A= ;P=DOMAIN;O=DOMAIN;S=Buldakov (Test);G=Stanislav;
Prefix             : X400
IsPrimaryAddress   : True
PrefixString       : X400

Очевидно, что работать мы будем не со всеми адресами, а только с smtp. Поэтому имеет смысл фильтровать по свойству ProxyAddressString, и выбирать только те объекты-адреса, у которых это свойство начинается с SMTP. Простейший вариант поиска только smtp-адресов будет выглядеть следующим образом:

$mailbox = Get-Mailbox somemailbox
for ($i=0; $i -lt $mailbox.EmailAddresses.Count; $i++){
    if ($mailbox.EmailAddresses[$i].ProxyAddressString -like "smtp:*"){
        $mailbox.EmailAddresses[$i]}
}

В нашем случае процесс усложняется тем, что перед нами стоит обратная задача. То есть надо собрать не все почтовые ящики с адресами в определённом домене, а те, у которых адресов из нужного нам домена нет. Один из вариантов решения данной проблемы – использование специального счётчика, который будет увеличиваться при нахождении в свойствах ящика нужного нам smtp-адреса. А в итоге, выводить будем те объекты, значение счётчика для которых будет равно нулю (то есть он не будет содержать нужного нам smtp-адреса). Итоговый скрипт получается примерно следующий:

$mailboxes = Get-MailboxDatabase new-db* | Get-Mailbox
foreach ($mailbox in $mailboxes){
    $i = 0
    $count = 0
    for ($i=0; $i -lt $mailbox.EmailAddresses.Count; $i++){
    if ($mailbox.EmailAddresses[$i].ProxyAddressString -like "smtp:*domain.net"){
        $count = $count + 1}
    }
    if ($count -eq 0){
        $mailbox.Name}
}

Аналогично, можно искать, например, группы распространения. В этом случае в переменную $mailboxes помещается набор нужных нам групп.

e2010В очередной раз натолкнулся на проблему некорректной отработки RBAC в случае работы с общими папками. Стоит задача – делегировать права на заведение mail-enabled общих папок. То есть по факту, на командлет Enable-MailPublicFolder. Право на запуск этого командлета делегировано всего одной роли:

[PS] C:Windowssystem32>Get-ManagementRoleEntry '*Enable-MailPublicFolder' | fl Name, Role

Name : Enable-MailPublicFolder
Role : Mail Enabled Public Folders

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

[PS] C:Windowssystem32>Enable-MailPublicFolder -Identity 'Public Folder'
MapiExceptionNoAccess: Unable to set properties on object. (hr=0x80070005, ec=-2147024891)
...
+ CategoryInfo          : NotSpecified: (0:Int32) [Enable-MailPublicFolder], MapiExceptionNoAccess
+ FullyQualifiedErrorId : 1CA8E050,Microsoft.Exchange.Management.MapiTasks.EnableMailPublicFolder

Самая вкуснятина в конце предпоследней строки: MapiExceptionNoAccess. Оказывается, чтобы создавать mail-enabled общие папки штатного механизма RBAC не достаточно. В Exchange 2000/2003 за процедуру создания mail-enabled общих папок отвечало специальное разрешение ms-Exch-Mail-Enabled-Public-Folder. Назначалось оно на уровне конкретной организации Exchange в разделе конфигурации в AD. Что интересно, в Exchange 2010 оно там же и осталось. Более того, этим разрешением обладают две группы – Organization Management и Public Folder Management:

[PS] C:Windowssystem32>Get-ADPermission -Identity "CN=OrgName,CN=Microsoft Exchange,CN=Services,CN=Configuration,DC=o365test,DC=pro" | ? {$_.ExtendedRights -like 'ms-Exch-Mail-Enabled-Public-Folder'} | select User

User
----
Organization Management
Public Folder Management

Ради интереса привожу полный список разрешений группы Public Folder Management на контейнер почтовой организации:

[PS] C:Windowssystem32>Get-ADPermission -Identity "CN=OrgName,CN=Microsoft Exchange,CN=Services,CN=Configuration,DC=0365test,DC=pro" | ? {$_.User -like '*Public Folder Management'} | select AccessRights, ExtendedRights

AccessRights                                                ExtendedRights
------------                                                --------------
{GenericRead}
{ExtendedRight}                                             {ms-Exch-Create-Public-Folder}
{ExtendedRight}                                             {ms-Exch-Modify-Public-Folder-Deleted-Item-Retention}
{ExtendedRight}                                             {ms-Exch-Modify-Public-Folder-Replica-List}
{ExtendedRight}                                             {ms-Exch-Modify-Public-Folder-Expiry}
{ExtendedRight}                                             {ms-Exch-Modify-PF-Admin-ACL}
{ExtendedRight}                                             {ms-Exch-Modify-Public-Folder-Quotas}
{ExtendedRight}                                             {ms-Exch-Mail-Enabled-Public-Folder}
{ExtendedRight}                                             {ms-Exch-Modify-PF-ACL}
{ExtendedRight}                                             {ms-Exch-Store-Create-Named-Properties}
{ExtendedRight}                                             {ms-Exch-Store-Admin}
{ExtendedRight}                                             {ms-Exch-Store-Visible}
{ExtendedRight}                                             {ms-Exch-Create-Top-Level-Public-Folder}

Дело осталось за малым – назначить соответствующие разрешения для нашего делегата:

Add-ADPermission -Identity "CN=OrgName,CN=Microsoft Exchange,CN=Services,CN=Configuration,DC=0365test,DC=pro" -User "Public Folders Delegates" -AccessRights GenericRead

Add-ADPermission -Identity "CN=OrgName,CN=Microsoft Exchange,CN=Services,CN=Configuration,DC=0365test,DC=pro" -User "Public Folders Delegates" -ExtendedRights ms-Exch-Mail-Enabled-Public-Folder 

Интересные ссылки:
Beyond RBAC: Delegating the ‘Mail-enable Public Folders’ right
Permissions Available in Exchange

e2010Как я написал в предыдущей заметке, для решения проблемы с невозможностью выдачи права Send-As необходимо менять владельца объекта общей папки в Active Directory. Эта задача скучная и нудная, особенно, если у нас имеется несколько сотен/тысяч таких объектов. Нам в её решении поможет PowerShell. Для начала, имеет смысл выгрузить существующих владельцев:

Set-Location ad:
Get-ADObject -LDAPFilter "(ObjectClass=publicFolder)" -SearchBase 'CN=Microsoft Exchange System Objects,DC=o365lab,DC=pro' |
select Name, DistinguishedName, @{Name="Owner";expression={(Get-Acl "$_").Owner}} |
Export-Csv -Delimiter ";" -Path c:tmppfOwners.txt -Encoding UTF8

Теперь приступаем к написанию скрипта, который всех этих владельцев будет исправлять. Командлет Get-Acl имеет метод SetOwner(), который может помочь поменять владельца объекта. Правда, для замены нам понадобится использовать командлет Set-Acl, чтобы это изменение применить к объекту. Подробнее процедура расписана здесь. В нашем случае процедура замены будет выглядеть примерно следующим образом:

$NewOwner = New-Object System.Security.Principal.NTAccount("O365LABNEWMAILSERVER$")
$PF = Get-ADObject -LDAPFilter "(ObjectClass=publicFolder)" -SearchBase 'CN=test6,CN=Microsoft Exchange System Objects,DC=o365lab,DC=pro'
$Acl = Get-Acl $PF
$Acl.SetOwner($NewOwner)
Set-Acl -AclObject $Acl -Path $PF.DistinguishedName

Осталось эту процедуру запустить в цикл, в котором она будет применена ко всем общим папкам в контейнере Microsoft Exchange System Objects:

Import-Module ActiveDirectory
Set-Location ad:

$PFs = Get-ADObject -LDAPFilter "(ObjectClass=publicFolder)" -SearchBase 'CN=Microsoft Exchange System Objects,DC=o365lab,DC=pro'
$NewOwner = New-Object System.Security.Principal.NTAccount("O365LABNEWMAILSERVER$")

ForEach ($PF in $PFs) {
$Acl = Get-Acl $PF
$Acl.SetOwner($NewOwner)
Set-Acl -AclObject $Acl -Path $PF.DistinguishedName   }

Полезные ссылки:
How Can I Use Windows PowerShell to Determine the Owner of a File?
Can I Determine a Folder’s Access Rights and Who Has Them?
Cannot add send as permission for public folder on Exchange 2010

apple-devilВ очередной раз (внезапно!) Apple устроил холокост для своих верных поклонников, которые используют Exchange в качестве корпоративной почтовой системы. В кратце – при обработке запросов на организацию встреч (meeting requests) с устройства Apple с новой прошивкой можно организовать зацикливание. Это может привести к резкому росту логов транзакций, и в случае, если не уследить за дисковым пространством, на котором хранятся логи, то можно легко положить серверы почтовых ящиков.

Рекомендации:

  • Не ставить пока прошивку iOS 6.1
  • Заблокировать устройства, которые уже обновились

Ну а для начала имеет смысл понять кто успел обновиться. Информацию по устройствам, с которых пользователь подключался к почтовому ящику даёт комадлет Get-ActivesyncDeviceStatistics. Имеет смысл отправлять в него только почтовые ящики, к которым уже подключались через мобильные устройства. У них параметр HasActivesyncDevicePartnership имеет значение true. Полный скрипт получается примерно следующий:

$Mbx = Get-CASMailbox -Filter {HasActivesyncDevicePartnership -eq $True}
-ResultSize unlimited;
$Mbx | %{$Name = $_.Name;
$Device = Get-ActiveSyncDeviceStatistics -Mailbox $_.Identity |
?{$_.DeviceUserAgent -like "Apple*1002*"} |
%{Write-Host $Name, $_.DeviceModel, $_.DeviceUserAgent, $_.DeviceId,
$_.FirstSyncTime, $_.LastSuccessSync}}

Исходный скрипт взят отсюда. Спасибо Олегу Крылову и Сергею Мариничеву за внесённые правки.

Оказывается, имеется даже целый скрипт в галерее скриптов. Спасибо комментатору Олегу.