Automating Mailbox Repairs – Exchange 2010

While running through a series of mailbox repairs, I was looking for a way at automating this task. Since Exchange 2010 logs the output of the command New-MailboxRepairRequest to the Event Viewer, I would have to pull the results from here as part of this automation.

A brief outline of the automation to follow goes like this. Run New-MailboxRepairRequest against a mailbox database with a detectonly parameter, once completed gather all mailboxes from Event Viewer which contain corruption and continue to run a mailbox repair over them.

Some of the errors which can be resolved include;

10033 – A folder is being scoped by a search which no longer exists. 

Part of the script can also be used if you want to pull a list of corruption affected mailboxes.

NOTE:

  • New-MailboxRepairRequest has a maximum of simultaneous repair requests to 1 database at a time or 100 mailboxes.
  • The command may impact user connectivity to the mailbox while repairing corruption not while using the -detectonly parameter.
  • If running against a database rather than per user mailbox Exchange will only disconnect the user during their mailbox scan, in other words the command is completed in sequence rather than in parallel.
  • A repair request can only be cancelled by dismounting the entire database.

Begin by running the New-MailboxRepairRequest over each database.(Due to potential performance issues you are limited to running 1 repair per database)

</pre>
<pre class="lang:ps decode:true " title="New-MailboxRepairRequest">Get-MailboxDatabase "&lt;Database" | New-MailboxRepairRequest -CorruptionType AggregateCounts, Searchfolder, Folderview, Provisionedfolder -DetectOnly 

You will see the request begin in the Application log of the Event Viewer with Event 10059;

2017-01-22_20-04-57

You will then need to wait until the scan has finished at which point you will see the Event 10047;

2017-01-22_20-04-5

Once the initial detection has been completed as above, we can continue to pull the data from Event Viewer and run a per mailbox repair on each of the mailboxes found to have corruption. I have broken down what exactly the script is doing below.

Custom event query for codes relevant to mailbox repair. (Can be found by creating custom a custom view and selecting the XML tab and copying the XML code.)

$EventXML =  @"
<QueryList>
  <Query Id="0" Path="Application">
    <Select Path="Application">*[System[Provider[@Name='MSExchangeIS Mailbox Store'] and (EventID=10044 or EventID=10045 or EventID=10146 or EventID=10047 or EventID=10048 or EventID=10049 or EventID=10050 or EventID=10051 or EventID=10059 or EventID=10062)]]</Select>
  </Query>
</QueryList>
"@

Get events using the XML filter supplied in $EventXML

$events = Get-WinEvent -FilterXml $EventXML

Declare $Accounts variable as array to be able to query after ForEach

$Accounts= @()

For each object in $events variable get data in brackets which will be the mailbox name and add it to the $Accounts variable

ForEach ($event in $events){ 
$Account = $event.message.Split('()')[1]
$Accounts += $Account
}

Select the unique objects in $Accounts variable and store in $Mailboxes variable

$Mailboxes = $Accounts | Select -uniq

The final part of the script will take each object in the $mailboxes variable, store in $Mailbox variable and if the object isn’t empty then get the mailbox using the object as the identity using Get-Mailbox and store in $UserMailbox. We then Run a New-MailboxRepairRequest on each valid mailbox.

ForEach ($Mailbox in $Mailboxes){
		If ($Mailbox -ne $null) {
			$UserMailbox = Get-Mailbox -identity $Mailbox
		}
	Write-Host "Beginning Mailbox Repair on mailbox" $UserMailbox.Name -foregroundcolor green
	$UserMailbox | New-MailboxRepairRequest -CorruptionType AggregateCounts, Searchfolder, Folderview, Provisionedfolder
	}

Once the script has completed, clear and save off the Application log ready to run through the same for any additional databases which need to be checked!

Hopefully the above will save some time and effort, you can of course run a repair using the command over the database without the -detectonly parameter but if you, like me, would prefer to only run against the corrupt mailboxes this should assist to some degree.

Two scripts are available, the first can be used just to view the corrupt mailboxes and write them to screen EventScript.ps1. The second is the script that has been outlined in this article PerMailboxRepair.ps1.

$EventXML =  @"
<QueryList>
  <Query Id="0" Path="Application">
    <Select Path="Application">*[System[Provider[@Name='MSExchangeIS Mailbox Store'] and (EventID=10044 or EventID=10045 or EventID=10146 or EventID=10047 or EventID=10048 or EventID=10049 or EventID=10050 or EventID=10051 or EventID=10059 or EventID=10062)]]</Select>
  </Query>
</QueryList>
"@

$events = Get-WinEvent -FilterXml $EventXML
$Accounts= @()
ForEach ($event in $events){	
	$Account = $event.message.Split('()')[1]
	$Accounts += $Account
	}
$Accounts | Select -Uniq
$EventXML =  @"
<QueryList>
  <Query Id="0" Path="Application">
    <Select Path="Application">*[System[Provider[@Name='MSExchangeIS Mailbox Store'] and (EventID=10044 or EventID=10045 or EventID=10146 or EventID=10047 or EventID=10048 or EventID=10049 or EventID=10050 or EventID=10051 or EventID=10059 or EventID=10062)]]</Select>
  </Query>
</QueryList>
"@

$events = Get-WinEvent -FilterXml $EventXML
$Accounts= @()
ForEach ($event in $events){	
	$Account = $event.message.Split('()')[1]
	$Accounts += $Account
	}
$Mailboxes = $Accounts | Select -uniq

ForEach ($Mailbox in $Mailboxes){
		If ($Mailbox -ne $null) {
			$UserMailbox = Get-Mailbox -identity $Mailbox
		}
	Write-Host "Beginning Mailbox Repair on mailbox" $UserMailbox.Name -foregroundcolor green
	$UserMailbox | New-MailboxRepairRequest -CorruptionType AggregateCounts, Searchfolder, Folderview, Provisionedfolder
	}

Importing contacts into Exchange user mailbox

This is the second part of the original post Exporting Outlook contacts with PowerShell. I will be going through the process of importing these exported contacts directly into Exchange user mailboxes, in this case we will be using Exchange 2013. If you are using an older or newer version of Exchange server, you will need to use the relevant version of the EWS API, also you will need to adjust the dll path that exists in the PowerShell script supplied.

 

The brief steps to complete are as follows.

Install EWS API 2.1

Assign Role ApplicationImpersonation to Account used to complete this procedure

Modify and Save the ContactImport.ps1 script with your Exchange CAS server, Impersonation Account + Credentials and CSV share

Save the Import-MailboxContacts.ps1 script to the location specified in ContactImport.ps1

Open an Exchange Management Shell  and run ContactImport.ps1 script to import 

Else create session to Exchange CAS with the below and run ContactImport.ps1 changing EXCHANGESERVER to your Exchange CAS

$session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionURI http://EXCHANGESERVER/powershell/ -Authentication kerberos -AllowRedirection
import-PSSession $session -AllowClobber

 

Prerequisites to this procedure include;

EWS API 2.1  – Install –> https://www.microsoft.com/en-us/download/details.aspx?id=42022 (Enables enhanced exchange management for third party applications)

CSV Files – If the CSV files have been created per user with the post Exporting Outlook contacts with PowerShell, the below script already includes all possible mapping for contacts properties, if a custom CSV file has been created then these mapping will need to be modified.

Exchange Impersonation Rights (Allows impersonation of users to enable the ability to import the contacts directly into mailboxes without the users credentials or full access rights to mailbox) See below –>

To configure impersonation rights, you will need to complete through either the Exchange Control Panel or Exchange Management Shell.

The steps to configure impersonation rights through ECP:

Access the ECP URL where EXCHANGESERVER is the name of your CAS and login with an administrative account e.g Exchange Domain Admins–> https://EXCHANGESERVER/ecp/

Select permissions –> admin roles –>

Permissions

Enter a relevant name e.g Impersonation –> Leave scope as Default –> Add Role ApplicationImpersonation –> Add the user in which you will use to complete the import under Member –> Click Save.

RoleGroup

Steps to configure impersonation through PowerShell:

Open Exchange Management Shell with an administrative account

New-ManagementRoleAssignment -Name ImpersonationRole -Role ApplicationImpersonation -User DOMAIN\ACCOUNTNAME

Now that impersonation is configured we can look to start the import process. In this specific use case the name of the CSV files are the name of the user account in the new domain, if you have a case where the new mailbox names differ from the CSV generated name, you will either need to change the generated name of the CSV or create a mapping between the CSV name and the new user account name.

 

 

Breakdown of the ContactImport script

Get all CSV file names from share and store in $list variable (Change SERVER\SHARE as appropriate)

$list = Get-childitem -Path "\\SERVER\SHARE\" | Where {$_.name -like "*.csv*"}

Loop through each CSV name in Share, If the name matches the UserPrincipalName property of the mailbox then import to users mailbox else display “No Address Found”

The ForEach uses the Import-MailboxContacts script which will be explained later in the post with the relevant parameters for EWS. You will need to change the EXCHANGESERVER name to your Exchange CAS server with the user name used earlier which has the impersonation rights.

ForEach ($Name in $list) {
    $filename = $name.FullName
    write-host $filename 
        
    IF ($email = Get-Mailbox | Where-Object {$_.UserPrincipalName -match $Name.basename}){
        write-host $email.PrimarySMTPAddress
        $CSV = $name.FullName
        C:\Scripts\Import-MailboxContacts.ps1 -CSVFileName "$filename" -EmailAddress $email.PrimarySMTPAddress -EwsUrl https://EXCHANGESERVER/EWS/Exchange.asmx -Username ACCOUNTNAME@DOMAINNAME -Impersonate $true -Password PASSWORD
        }ELSE {
            Write-Host "No Address Found!"       
        }
 }

 

 

Import-MailboxContacts script

At the bottom of this post is the Import-MailboxContacts script, thanks to Steve Goodman and has been configured to be used with the Exporting Outlook contacts with Powershell post.

The script needs to be saved as Import-MailboxContacts.ps1 and is called by the ContactImport script. The ContactMappings array has been modified to work with the export from Outlook and the script has been updated with the corrects paths for use with EWS 2.1.

And thats it, if all is configured correctly, your users should have newly imported contacts in their mailbox.

Hope this helps!

Full Scripts with comments below;

 

Import-MailboxContacts script

param([string]$CSVFileName,[string]$EmailAddress,[string]$Username,[string]$Password,[string]$Domain,[bool]$Impersonate,[string]$EwsUrl,[string]$EWSManagedApiDLLFilePath,[bool]$Exchange2007);

#
# Import-MailboxContacts.ps1
#
# By Steve Goodman, Use at your own risk.
# 	17/12/15 Edited by Chris Prevel to run on Exchange 2013, using EWS 2.1 API
# 	Added settings to run through proxy, changed mappings to match powershell export of contacts, changed dll path, updated attributes
#
# Parameters
#  Mandatory:
# -CSVFileName : Filename of the CSV file to import contacts for this user from. Same format as Outlook Export.
# -EmailAddress : Account SMTP email address. Required, but only used when impersonating or with Autodiscover - otherwise uses the user you login as
#  Optional:
# -Impersonate : Set to $true to use impersonation.
# -Username : The username to use. If this isn't specified (along with Password), attempts to use the logged on user.
# -Password : Used with above
# -Domain : Used with above - optional.
# -EwsUrl : The URL for EWS if you don't want to use Autodiscover. Typically https://casserver/EWS/Exchange.asmx
# -EWSManagedApiDLLFilePath : (Optional) Overwrite the filename and path to the DLL for EWS Managed API. By default, uses the default install location.
# -Exchange2007 : Set to $true to use the Exchange 2007 SP1+ version of the Managed API.
#

# Contact Mapping - this maps the attributes in the CSV file (left) to the attributes EWS uses.
# NB: If you change these, please note "FirstName" is specified at line 102 as a required attribute and
# "FirstName" and "LastName" are hard coded at lines 187-197 when constructing NickName and FileAs.
$ContactMapping=@{
    "FirstName" = "GivenName";
    "MiddleName" = "MiddleName";
    "LastName" = "Surname";
    "CompanyName" = "CompanyName";
    "Department" = "Department";
    "JobTitle" = "JobTitle";
    "BusinessAddressStreet" = "Address:Business:Street";
    "BusinessAddressCity" = "Address:Business:City";
    "BusinessAddressState" = "Address:Business:State";
    "BusinessAddressPostalCode" = "Address:Business:PostalCode";
    "BusinessAddressCountry" = "Address:Business:CountryOrRegion";
    "HomeAddressStreet" = "Address:Home:Street";
    "HomeAddressCity" = "Address:Home:City";
    "HomeAddressState" = "Address:Home:State";
    "HomeAddressPostalCode" = "Other:Home:PostalCode";
    "HomeAddressCountry" = "Address:Home:CountryOrRegion";
    "OtherAddressStreet" = "Address:Other:Street";
    "OtherAddressCity" = "Address:Other:City";
    "OtherAddressState" = "Address:Other:State";
    "OtherAddressPostalCode" = "Address:Other:PostalCode";
    "OtherAddressCountry" = "Address:Other:CountryOrRegion";
    "AssistantTelephoneNumber" = "Phone:AssistantPhone";
    "BusinessFaxNumber" = "Phone:BusinessFax";
    "BusinessTelephoneNumber" = "Phone:BusinessPhone";
    "Business2TelephoneNumber" = "Phone:BusinessPhone2";
    "CallbackTelephoneNumber" = "Phone:CallBack";
    "CarTelephoneNumber" = "Phone:CarPhone";
    "CompanyMainTelephoneNumber" = "Phone:CompanyMainPhone";
    "HomeFaxNumber" = "Phone:HomeFax";
    "HomeTelephoneNumber" = "Phone:HomePhone";
    "Home2TelephoneNumber" = "Phone:HomePhone2";
    "ISDNNumber" = "Phone:ISDN";
    "MobileTelephoneNumber" = "Phone:MobilePhone";
    "OtherFaxNumber" = "Phone:OtherFax";
    "OtherTelephone" = "Phone:OtherTelephone";
    "PagerNumber" = "Phone:Pager";
    "PrimaryTelephoneNumber" = "Phone:PrimaryPhone";
    "RadioTelephoneNumber" = "Phone:RadioPhone";
    "TTYTDDTelephoneNumber" = "Phone:TtyTddPhone";
    "TelexNumber" = "Phone:Telex";
    "Anniversary" = "WeddingAnniversary";
    "Birthday" = "Birthday";
    "Email1Address" = "Email:EmailAddress1";
    "Email2Address" = "Email:EmailAddress2";
    "Email3Address" = "Email:EmailAddress3";
    "Initials" = "Initials";
    "OfficeLocation" = "OfficeLocation";
    "ManagerName" = "Manager";
    "Body" = "Body";
    "Profession" = "Profession";
    "Spouse" = "SpouseName";
    "WebPage" = "BusinessHomePage";
    "Contact Picture File" = "Method:SetContactPicture"
}
[System.Net.WebRequest]::DefaultWebProxy = $null

# CSV File Checks
# Check filename is specified
if (!$CSVFileName)
{
    throw "Parameter CSVFileName must be specified";
}

# Check file exists
if (!(Get-Item -Path $CSVFileName -ErrorAction SilentlyContinue))
{
    throw "Please provide a valid filename for parameter CSVFileName";
}

# Check file has required fields and check if is a single row, or multiple rows
$SingleItem = $false;
$CSVFile = Import-Csv -Path $CSVFileName;
if ($CSVFile."First Name")
{
    $SingleItem = $true;
} else {
    if (!$CSVFile[0]."FirstName")
    {
        throw "File $($CSVFileName) must specify at least the field 'First Name'";
    }
}

# Check email address
if (!$EmailAddress)
{
    throw "Parameter EmailAddress must be specified";
}
if (!$EmailAddress.Contains("@"))
{
    throw "Parameter EmailAddress does not appear valid";
}

# Check EWS Managed API available
if (!$EWSManagedApiDLLFilePath)
{
    $EWSManagedApiDLLFilePath = "C:\Program Files (x86)\Microsoft\Exchange\Web Services\2.1\Microsoft.Exchange.WebServices.dll"
}
if (!(Get-Item -Path $EWSManagedApiDLLFilePath -ErrorAction SilentlyContinue))
{
    throw "EWS Managed API not found at $($EWSManagedApiDLLFilePath). Download from http://www.microsoft.com/download/en/details.aspx?id=28952";
}

# Load EWS Managed API
[void][Reflection.Assembly]::LoadFile("C:\Program Files (x86)\Microsoft\Exchange\Web Services\2.1\Microsoft.Exchange.WebServices.dll");

# Create Service Object
if ($Exchange2007)
{
    $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2007_SP1)
} else {
    $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010)
}
# Set credentials if specified, or use logged on user.
if ($Username -and $Password)
{
    if ($Domain)
    {
        $service.Credentials = New-Object  Microsoft.Exchange.WebServices.Data.WebCredentials($Username,$Password,$Domain);
    } else {
        $service.Credentials = New-Object  Microsoft.Exchange.WebServices.Data.WebCredentials($Username,$Password);
    }
    
} else {
    $service.UseDefaultCredentials = $true;
}


# Set EWS URL if specified, or use autodiscover if no URL specified.
if ($EwsUrl)
{
    $service.URL = New-Object Uri($EwsUrl);
} else {
    try {
        $service.AutodiscoverUrl($EmailAddress);
    } catch {
        throw;
    }
}

# Perform a test - try and get the default, well known contacts folder.

if ($Impersonate)
{
    $service.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $EmailAddress);
}
try {
    $ContactsFolder = [Microsoft.Exchange.WebServices.Data.ContactsFolder]::Bind($service, [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Contacts);
} catch {
    throw;
}

# Add contacts
foreach ($ContactItem in $CSVFile)
{
    # If impersonate is specified, do so.
    if ($Impersonate)
    {
        $service.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $EmailAddress);
    }

    $ExchangeContact = New-Object Microsoft.Exchange.WebServices.Data.Contact($service);
    if ($ContactItem."FirstName" -and $ContactItem."LastName")
    {
        $ExchangeContact.NickName = $ContactItem."FirstName" + " " + $ContactItem."LastName";
    }
    elseif ($ContactItem."FirstName" -and !$ContactItem."LastName")
    {
        $ExchangeContact.NickName = $ContactItem."FirstName";
    }
    elseif (!$ContactItem."FirstName" -and $ContactItem."LastName")
    {
        $ExchangeContact.NickName = $ContactItem."LastName";
    }
    $ExchangeContact.DisplayName = $ExchangeContact.NickName;
    $ExchangeContact.FileAs = $ExchangeContact.NickName;
    
    $BusinessPhysicalAddressEntry = New-Object Microsoft.Exchange.WebServices.Data.PhysicalAddressEntry;
    $HomePhysicalAddressEntry = New-Object Microsoft.Exchange.WebServices.Data.PhysicalAddressEntry;
    $OtherPhysicalAddressEntry = New-Object Microsoft.Exchange.WebServices.Data.PhysicalAddressEntry;
    
    # This uses the Contact Mapping above to save coding each and every field, one by one. Instead we look for a mapping and perform an action on
    # what maps across. As some methods need more "code" a fake multi-dimensional array (seperated by :'s) is used where needed.
    foreach ($Key in $ContactMapping.Keys)
    {
        # Only do something if the key exists
        if ($ContactItem.$Key)
        {
            # Will this call a more complicated mapping?
            if ($ContactMapping[$Key] -like "*:*")
            {
                # Make an array using the : to split items.
                $MappingArray = $ContactMapping[$Key].Split(":")
                # Do action
                switch ($MappingArray[0])
                {
                    "Email"
                    {
                        $ExchangeContact.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::($MappingArray[1])] = $ContactItem.$Key;
                    }
                    "Phone"
                    {
                        $ExchangeContact.PhoneNumbers[[Microsoft.Exchange.WebServices.Data.PhoneNumberKey]::($MappingArray[1])] = $ContactItem.$Key;
                    }
                    "Address"
                    {
                        switch ($MappingArray[1])
                        {
                            "Business"
                            {
                                $BusinessPhysicalAddressEntry.($MappingArray[2]) = $ContactItem.$Key;
                                $ExchangeContact.PhysicalAddresses[[Microsoft.Exchange.WebServices.Data.PhysicalAddressKey]::($MappingArray[1])] = $BusinessPhysicalAddressEntry;
                            }
                            "Home"
                            {
                                $HomePhysicalAddressEntry.($MappingArray[2]) = $ContactItem.$Key;
                                $ExchangeContact.PhysicalAddresses[[Microsoft.Exchange.WebServices.Data.PhysicalAddressKey]::($MappingArray[1])] = $HomePhysicalAddressEntry;
                            }
                            "Other"
                            {
                                $OtherPhysicalAddressEntry.($MappingArray[2]) = $ContactItem.$Key;
                                $ExchangeContact.PhysicalAddresses[[Microsoft.Exchange.WebServices.Data.PhysicalAddressKey]::($MappingArray[1])] = $OtherPhysicalAddressEntry;
                            }
                        }
                    }
                    "Method"
                    {
                        switch ($MappingArray[1])
                        {
                            "SetContactPicture" 
                            {
                                if (!$Exchange2007)
                                {
                                    if (!(Get-Item -Path $ContactItem.$Key -ErrorAction SilentlyContinue))
                                    {
                                        throw "Contact Picture File not found at $($ContactItem.$Key)";
                                    }
                                    $ExchangeContact.SetContactPicture($ContactItem.$Key);
                                }
                            }
                        }
                    }
                
                }                
            } else {
                # It's a direct mapping - simple!
                if ($ContactMapping[$Key] -eq "Birthday" -or $ContactMapping[$Key] -eq "WeddingAnniversary")
                {
                    [System.DateTime]$ContactItem.$Key = Get-Date($ContactItem.$Key);
                }
                $ExchangeContact.($ContactMapping[$Key]) = $ContactItem.$Key;            
            }
            
        }    
    }
    # Save the contact    
    $ExchangeContact.Save();
    
    # Provide output that can be used on the pipeline
    $Output_Object = New-Object Object;
    $Output_Object | Add-Member NoteProperty FileAs $ExchangeContact.FileAs;
    $Output_Object | Add-Member NoteProperty GivenName $ExchangeContact.GivenName;
    $Output_Object | Add-Member NoteProperty Surname $ExchangeContact.Surname;
    $Output_Object | Add-Member NoteProperty EmailAddress1 $ExchangeContact.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::EmailAddress1]
    $Output_Object;
}

 

 

 

ContactImport Script

#Get all items of share with .csv file type
$list = Get-childitem -Path "\\SERVER\SHARE\" | Where {$_.name -like "*.csv*"}

#Loop through each .csv file in $list and if a mailbox 
#with the same User Principal Name exists then complete import
ForEach ($Name in $list) {
    $filename = $name.FullName
    write-host $filename 
        
    IF ($email = Get-Mailbox | Where-Object {$_.UserPrincipalName -match $Name.basename}){
        write-host $email.PrimarySMTPAddress
        $CSV = $name.FullName
        C:\Scripts\Import-MailboxContacts.ps1 -CSVFileName "$filename" -EmailAddress $email.PrimarySMTPAddress -EwsUrl https://EXCHANGESERVER/EWS/Exchange.asmx -Username ACCOUNTNAME@DOMAINNAME -Impersonate $true -Password PASSWORD
            }
        ELSE {
            Write-Host "No Address Found!"
          
    }
 }


 

Exporting Outlook contacts with PowerShell

Who new you could utilise PowerShell to drill into Outlook (while running) and pull out tons of stuff, awesome. I have documented a script which can be used to do just this, a couple of caveats…

*Outlook must be running

*This doesn’t export the contacts picture

*PowerShell will need an Execution Policy set during running e.g Bypass (unless run in a PowerShell windows on user session)

See more about execution policies on the Microsoft technet site –> https://technet.microsoft.com/en-us/library/ee176961.aspx

 

A bit of background: I had a rare instance where the data held in a user mailbox who was moving to a new company within the umbrella of a corporation was sensitive, so a mailbox migration couldn’t be completed and they wanted to take across their contacts to their new mailbox, which spurred the creation of this script and in turn this post.

In this instance for ease of use I will be running the script initiated from a batch script with a bypass execution policy.

powershell.exe -executionpolicy bypass -File "\\SERVER\SHARE\ContactsExport.ps1"

Copy the line of code above into notepad and save as ContactExport.bat, you will be running this batch script through whatever means you choose e.g GPO, management agent, SCCM etc.

I have broken the script down into sections to explain each part:

The $Outlook variable holds the New-Object command which is allowing control of the current session of Outlook, you need to have Outlook already running else PowerShell will attempt to create a new session and error.

$Outlook= NEW-OBJECT –comobject Outlook.Application

We can then drill down to individual folders to extract information, in this case (10) is contacts.

$Contacts = $Outlook.session.GetDefaultFolder(10).items

*** Additional script below to include folders within contacts (pointed out by Mike in comments section)

 

————————————————————————————————————————————————————————————————————————————

$ContactFolders = $Outlook.session.GetDefaultFolder(10).Folders

Exempt additional folders as by default there are Recipient Cache folder, Global address lists and any other type of created address lists. (the one variable exempted all folders other than user created ones, which I found strange, but it works so hey!)

$folderexempt1 = "*Recipient*"

Declare the array so that objects gathered within the For loop can be used outside of itself

$ContactsNested = @()

For loop to loop through each folder and pull contact items, exempting additional folders. (the Folders.items array only accepted integers, which may be a restriction of using Outlook this way )

***Note you need the exemption as if your users have GAL’s this is going to pull all the contacts in there! So be warned

For ($i = 1;$i -le $ContactFolders.count;$i++){

$ContactsFoldersItems = $ContactFolders.Item($i) | Where-Object {($_.Name -notlike "$folderexempt1")}
$ContactsNested += $ContactsFoldersItems.Items 

}

Finally add the contacts within the contact folders to the original $Contacts variable.

$contacts += $ContactsNested

————————————————————————————————————————————————————————————————————————————

 

I have listed all of the different folder numbers and what they relate to below:

GetDefaultFolder(3) - Deleted Items
GetDefaultFolder(4) - Outbox
GetDefaultFolder(5) - Sent Items
GetDefaultFolder(6) - Inbox
GetDefaultFolder(9) - Calendar
GetDefaultFolder(10) - Contacts
GetDefaultFolder(11) - Journal
GetDefaultFolder(12) - Notes
GetDefaultFolder(13) - Tasks
GetDefaultFolder(16) - Drafts
GetDefaultFolder(18) - All Public Folders
GetDefaultFolder(19) - Conflicts
GetDefaultFolder(20) - Sync Issues
GetDefaultFolder(21) - Local Failures
GetDefaultFolder(22) - Server Failures
GetDefaultFolder(23) - Junk Email
GetDefaultFolder(25) - RSS Feeds
GetDefaultFolder(28) - To-Do List
GetDefaultFolder(30) - Suggested Contacts

Next we get the OS’s environmental variable UserName (currently logged in user) ready for naming the exported .csv file.

$user = [Environment]::UserName

We then need to select all of the attributes and details for each contact from the $Contacts variable, here I have selected everything but have listed it all to pick and choose.

$Info = $Contacts | Select FirstName,MiddleName,LastName,CompanyName,Department,JobTitle,BusinessAddressStreet,BusinessAddressCity,BusinessAddressState,BusinessAddressPostalCode,BusinessAddressCountry,HomeAddressStreet,HomeAddressCity,HomeAddressState,HomeAddressPostalCode,HomeAddressCountry,OtherAddressStreet,OtherAddressCity,OtherAddressState,OtherAddressPostalCode,OtherAddressCountry,AssistantTelephoneNumber,BusinessFaxNumber,BusinessTelephoneNumber,Business2TelephoneNumber,CallbackTelephoneNumber,CarTelephoneNumber,CompanyMainTelephoneNumber,HomeFaxNumber,HomeTelephoneNumber,Home2TelephoneNumber,ISDNNumber,MobileTelephoneNumber,OtherFaxNumber,OtherTelephone,PagerNumber,PrimaryTelephoneNumber,RadioTelephoneNumber,TTYTDDTelephoneNumber,TelexNumber,Anniversary,Birthday,Email1Address,Email2Address,Email3Address,Initials,OfficeLocation,ManagerName,Body,Profession,Spouse,WebPage

This is then exported to a .csv file named as the logged in user with the $User variable. The Encoding is set to ensure any contacts which contain funky characters are not made worse.

$Info | Export-Csv -Encoding Unicode -NoTypeInformation "c:\SERVER\SHARE\$user.csv"

That’s it, this should export all contacts to a .csv file ready for importing elsewhere. I will be writing an article on importing this into users mailboxes through Exchange using PowerShell in the coming weeks.

Full Script with comments below, hope it helps.(Updated to include Contact Folders)

#Create Outlook session
$Outlook=NEW-OBJECT –comobject Outlook.Application
#Store the Contacts objects
$Contacts=$Outlook.session.GetDefaultFolder(10).items
#Store the folder objects
$Contactsfolders = $Outlook.session.GetDefaultFolder(10).Folders
#Get the logged in User
$user = [Environment]::UserName
#Contacts folder exemption **Required to remove GAL entries and Recipient Cache**
$folderexempt1 = "*Recipient*"
#Create array for nested contacts (within folders)
$contactsnested = @()
#For Loop takes the total folders starting at folder 1, while less than the total count, will loop with an addition of 1 each time
#The script block cycles through each Object in the Item array where the Object name is not like the exemption and we add this to the variable $conectsnested
For ($i = 1;$i -le $contactfolders.count;$i++){

$ContactsFoldersItems = $contactfolders.Item($i) | Where-Object {($_.Name -notlike "$folderexempt1")}
$contactsnested += $ContactsFoldersItems.Items 

}

$contacts += $Contactsnested
#Select attributes from Contacts
$Info = $Contacts | Select FirstName,MiddleName,LastName,CompanyName,Department,JobTitle,BusinessAddressStreet,BusinessAddressCity,BusinessAddressState,BusinessAddressPostalCode,BusinessAddressCountry,HomeAddressStreet,HomeAddressCity,HomeAddressState,HomeAddressPostalCode,HomeAddressCountry,OtherAddressStreet,OtherAddressCity,OtherAddressState,OtherAddressPostalCode,OtherAddressCountry,AssistantTelephoneNumber,BusinessFaxNumber,BusinessTelephoneNumber,Business2TelephoneNumber,CallbackTelephoneNumber,CarTelephoneNumber,CompanyMainTelephoneNumber,HomeFaxNumber,HomeTelephoneNumber,Home2TelephoneNumber,ISDNNumber,MobileTelephoneNumber,OtherFaxNumber,OtherTelephone,PagerNumber,PrimaryTelephoneNumber,RadioTelephoneNumber,TTYTDDTelephoneNumber,TelexNumber,Anniversary,Birthday,Email1Address,Email2Address,Email3Address,Initials,OfficeLocation,ManagerName,Body,Profession,Spouse,WebPage
#Export .CSV of contacts named with username
$Info | Export-Csv -Encoding Unicode -NoTypeInformation "c:\SERVER\SHARE\$user.csv" 

PowerShell Script to run commands per Active Directory OU

I regularly run into a case in which it is handy to have a script to hand to run against a group of windows desktops or servers in an Active Directory OU.

Requirements to run the below are below.

  1. WinRM needs to be running on the relevant desktops and servers (can be completed by GPO) or by running “winrm quickconfig” in a PowerShell session on the machine
  2. Remote Server Admin Tools need to be installed on the desktop or server in which you are running the script (not required on DC’s)

The script is broken down below.

Import-Module ActiveDirectory

Import the AD module (RSAT requirement)

# OU Name
$OU = "OU=SETOFCOMPUTERS,OU=COMPUTEROU,DC=DOMAINNAME,DC=COM"

The $OU variable holds the full LDAP filter of the targeted OU

$Script = "ipconfig /flushdns"

The $Script variable holds the command to which you would want to run against the computers. (installations, batch scripts or any other commands)

# Window Title
$Host.UI.RawUI.WindowTitle = "Processing Computers in OU " + $OU

# Connectivity Timeout
$timeoutSeconds = 20

The window title of the PowerShell windows will display “Processing Computers in OU OU=SETOFCOMPUTERS,OU=COMPUTEROU,DC=DOMAINNAME,DC=COM” while the Connectivity Timeout variable is used later to complete inital connectivity of the computer before completing the script.

$ComputerNames = Get-ADComputer -Filter * -SearchBase "$OU" | Select Name

The $ComputerNames variable uses the AD command Get-ADComputer with the filter of the $OU variable to select all computers in the targeted OU.

FOREACH ($Computer in $ComputerNames) {
    if(Test-Connection -ComputerName $($Computer).Name -Count 1 -TimeToLive $timeoutSeconds -ErrorAction 0){
	Write-Host $Computer.Name -ForegroundColor Green 
        Invoke-command -COMPUTER $Computer.Name -ScriptBlock {'$Script'}
    }
    else {Write-Host "Computer NOT FOUND $Computer.Name" -Foreground Red
    }

}

The foreach loop runs a test-connection or ping with a TTL of 20 seconds, if this fails the “Computer Not Found COMPUTERNAME” message will be returned. If successful then the invoke-command will run a remote PowerShell session to execute the $Script variable on the targeted desktop.

Enjoy.

Full Code:

Import-Module ActiveDirectory

# OU Name
$OU = "OU=SETOFCOMPUTERS,OU=COMPUTEROU,DC=DOMAINNAME,DC=COM"

#Script to run on each computer
$Script = "ipconfig /flushdns"

# Window Title
$Host.UI.RawUI.WindowTitle = "Processing Computers in OU " + $OU

# Connectivity Timeout
$timeoutSeconds = 20
The window title of the PowerShell windows will display "Processing Computers in OU OU=SETOFCOMPUTERS,OU=COMPUTEROU,DC=DOMAINNAME,DC=COM" while the Connectivity Timeout variable is used later to complete inital connectivity of the computer before completing the script.

# Computer name list
$ComputerNames = Get-ADComputer -Filter * -SearchBase $OU | Select Name

# ForEach loop to complete command on each Computer
FOREACH ($Computer in $ComputerNames) {
    if(Test-Connection -ComputerName $($Computer).Name -Count 1 -TimeToLive $timeoutSeconds -ErrorAction 0){
	
    Write-Host $Computer.Name -ForegroundColor Green 
    Invoke-command -COMPUTER $Computer.Name -ScriptBlock {'$Script'}
    
    }
    else {Write-Host "Computer NOT FOUND $Computer.Name" -Foreground Red
    }

}

 

SCCM Site Code Change with PowerShell

I came across a case whereby a test SCCM installation had been completed and needed to be removed and replaced with a production instance. There are a few cleanup operations but in this case I needed to automate a way to change the clients to point to the new Site Code.

This can be completed with the below PowerShell command replacing SITECODE with the new Site Code (PowerShell run as administrator)

([wmiclass]'ROOT\ccm:SMS_Client').SetAssignedSite('SITECODE')

Running across VM’s in vCenter

I also have a script for completing in PowerCLI by using the Get-VM command, this is broken down below.

This presumes you have already created a connection to your vCenter through PowerCLI

*Use PowerCLI x86

#Get vm name and check if it exists within vCenter, loop until true

$Name = Read-Host "Enter the Name or Names using wildcard * of the VM e.g Server*"
$VM = Get-VM $Name | Where {($_.Powerstate -eq 'PoweredOn')}           
DO{
   IF ($VM){
                  $Isname = $True
                  Write-Host "VM FOUND :)" -Foreground GREEN
                  } ELSE {
                  $Isname = $False
                  Write-Error "VM NOT FOUND! :(" -Background RED -Foreground Black
				  }
}UNTIL ($Isname -ne $False)

Note that there is no error checking on the below, replace the GuestUser and GuestPassword parameters with your own credentials with administrative rights on the VM Guest OS.

#Run script in Guest

Write-Host "Running Script..."

$ScriptText = "PowerShell.exe -NoProfile -Command ""([wmiclass]'ROOT\ccm:SMS_Client').SetAssignedSite('GSY')"""

Invoke-VMScript -VM $VM -ScriptText $ScriptText -ScriptType bat -GuestUser administrator -GuestPassword *Password*

In the perfect world this environment would have WinRM enabled across the server estate but alas it didn’t. This saved a fair amount of manual work for me and I hope it does you too!

Complete script available here –> SCCMSiteCodeChange just paste into PowerShell ISE, save and run from PowerCLI.

VMware vShield Driver Installation through PowerCLI

While deploying vShield I have found the easiest way to automate the installation of the additional vShield Driver (Now called Guest Introspection Driver) is to use PowerCLI and complete through the invoke-vmscript command.

I will presume that you have PowerCLI installed, else it can be obtained from VMware.com.

*If using an x64 OS you will need to use the x86 version of PowerCLI*

Connect to your vCenter Server where *vCenterServer* is the name of your vCenter Server, I use the $Credential variable to store the credential for this session but you can skip that and use the -user and -password parameters after the connect-viserver if preferred;

$Credential = Get-Credential
Connect-Viserver *vCenterServer* -Credential $Credential

The $Name variable stores the name that is input when the script is run, if you specify a wildcard * for the name this will run against all servers so be warned! In turn you can run against VM1 and VM2 with VM*.

The name is then checked using the Get-VM command to see if the server exists displaying “VM FOUND 🙂” if true or “VM NOT FOUND 🙁” if false. Until the correct name is entered the script will keep prompting for a name.

#Get Name of VM and check against vCenter
DO {
      $Name = Read-Host "Enter the Name or Names using wildcard * of the VM e.g VMName*"
            
            IF (Get-VM $Name){
                               $Isname = $True
                               Write-Output "VM FOUND :)" -Foreground GREEN
                              } ELSE {
                                       $Isname = $False
                                       Write-Warning "VM NOT FOUND! :(" -Background RED -Foreground BLACK
                              }
} UNTIL($Isname = $True)

The below code is only required if the hostname differs from the name of the VM.

#Only required if the name of the VM isn't the hostname (and you use AD)
#Check entered name against AD and list if found

$ComputerNames = Get-ADComputer -LDAPFilter "(name=$Name)" | Select Name

FOREACH ($Computer in $ComputerNames) {
	                                Write-Output $Computer.Name
                                        $VMName = Get-VM -Name $Name 
                                        Write-Host "Attempting to Mount Tools...." 
                                       }TRY{ 
                                             $Mounttools = Mount-Tools -VM $VMName -ErrorAction Stop                   }CATCH{ 
                                             Write-Warning "UNABLE TO MOUNT TOOLS" 
}

Next we attempt to mount the VM tools to the VM

Get the drive letter of the VM and store in the $DriveLetter variable (I have seen this fail if the VM has WMI issues).

$DriveLetter = Get-WmiObject Win32_CDROMDrive -Credential $Credential -ComputerName $Computer.Name | Where-Object {$_.VolumeName -match "VMware Tools"} | Select-Object -ExpandProperty Drive

Storing the entire script in $ScriptText to run in the Invoke-VMScript command, replace Administrator and *Password* with your local admin account credentials.

Write-Output "Running Script..."
$ScriptText = "$DriveLetter\setup64.exe /S /v ""%TEMP%\vmmsi.log""""/qn REBOOT=R ADDLOCAL=ALL REMOVE=Hgfs,WYSE"""

#Attempt to run the script with local administrator and password
        TRY{
            	Invoke-VMScript -VM $VMName -ScriptText $ScriptText -ScriptType bat -GuestUser Administrator -GuestPassword *Password* 
            
        	} CATCH {
            		write-error "UNABLE TO RUN GUEST SCRIPT" -Background RED -Foreground Black
                       
}

Entire Script available below with additional menu option, split into Functions as I attempt to learn Powershell!
I apologise to any programmers/script writers as my script writing has a lot to be improved.

vShieldDriverInstall