Tuesday, 9 June 2015

Uploading an Exchange UM Voicemail Greeting

A colleague recently asked if I knew of a way to upload an audio file as an Exchange UM voice-mail greeting message - you can do this through Exchange for UM attendants, but not mailboxes. The background of the request is that we use Exchange UM for handling inbound calls, and calls which cannot be routed to a team or queue go to a team mailbox rather than an individuals. We have around 80 of these, and were looking at changing their greetings. Having to manually dial into each one and record the new greeting seemed cumbersome.

I had some experience with programmatically retrieving voice-mail greeting recordings from Exchange UM, so I guess that this might be possible too. It turns out that with the Exchange Web Services Managed API it is possible, and works fairly well.

The main caveat with this approach is that you need to be mindful of the size and quality of the WAV file you are uploading. I found that it worked well with a 16KHz, mono, audio file. I tested files up to about 350KB. With some higher-rate files the upload succeeded, but when calling the mailbox Exchange announced that 'a system error had occurred'. This problem could be resolved by re-recording another greeting over the phone.

The script does not support other files types, such as mp3, etc.

The script takes two parameters, the username and the filename.

# Set-UMGreeting.ps1
# Script to upload a .wav file as a voicemail greeting
# Ben Lye - www.onesimplescript.com

# Parameters for the script
[CmdletBinding(DefaultParametersetName="Common")]
param(
 [Parameter(Mandatory=$true)] $User,
 [Parameter(Mandatory=$true)] $FilePath
 )

# Get the UM mailbox
if ( !($Mailbox = Get-Mailbox $User -ErrorAction SilentlyContinue) ) {
 throw "Mailbox could not be found for user '$($User)'."
}

# Check that the audio file path is a .wav file
if ($FilePath -notlike "*.wav") {
 throw "Audio file is not a .wav file: '$($FilePath)'."
}

# Check that the audio file exists
if ( !(Test-Path $FilePath) ) {
 throw "Audio file not found: '$($FilePath)'."
}

# Load the data from the audio file
$AudioData = [IO.File]::ReadAllBytes("$FilePath")

# Check that the audio file is *really* a WAV file
If (! ([System.Text.Encoding]::ASCII.GetString(($AudioData[8],$AudioData[9],$AudioData[10],$AudioData[11])) -eq "WAVE")) {
 throw "Audio file does not contain WAV data: '$($FilePath)'."
}

# Get the SMTP address from the mailbox
$SmtpAddress = $Mailbox.PrimarySmtpAddress.ToString()

# Path to the Exchange Web Services DLL
$EWSManagedApiPath = "C:\Program Files (x86)\Microsoft\Exchange\Web Services\2.1\Microsoft.Exchange.WebServices.dll"
if ( !(Get-Item -Path $EWSManagedApiPath -ErrorAction SilentlyContinue) ) {
 throw "EWS Managed API could not be found at $($EWSManagedApiPath)."
}
 
# Load EWS Managed API
Add-Type -Path $EWSManagedApiPath
 
# Create EWS Object
$Service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013)

# Get the autodiscover URL
$Service.AutodiscoverUrl($SmtpAddress)

# Use EWS Impersonation to locate the root folder in the mailbox
$Service.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $SmtpAddress)
$FolderId = [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Root
$Folder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service,$FolderId)

If (-not $Folder) {
 throw "Couldn not bind to mailbox root folder."
}

# Create the search filter to find the UM custom greetings
$searchFilter = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+ContainsSubstring([Microsoft.Exchange.WebServices.Data.ItemSchema]::ItemClass,"IPM.Configuration.Um.CustomGreetings",[Microsoft.Exchange.WebServices.Data.ContainmentMode]::Substring,[Microsoft.Exchange.WebServices.Data.ComparisonMode]::IgnoreCase);

# Define the EWS search view
$view = New-Object Microsoft.Exchange.WebServices.Data.ItemView(100, 0, [Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning)
$view.PropertySet = New-Object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
$view.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Associated

# Create the object to return
$Output = New-Object System.Object
$Output | Add-Member -type NoteProperty -name "DisplayName" -value $Mailbox.DisplayName
$Output | Add-Member -type NoteProperty -name "HasCustomVoicemailGreeting" -value $False
$Output | Add-Member -type NoteProperty -name "VoicemailGreetingModifiedDate" -value $Null
$Output | Add-Member -type NoteProperty -name "SampleRateHz" -value $Null
$Output | Add-Member -type NoteProperty -name "Channels" -value $Null
$Output | Add-Member -type NoteProperty -name "FileSize" -value $Null

# Do the search and enumerate the results
If ($results = $service.FindItems( $FolderId, $searchFilter, $view )) {

 # Check if the greeting message exists; create it if not
 If ($results.Items.Count -eq 0) {
  # Create an EmailMessage object
  $UmGreetingObject = New-Object Microsoft.Exchange.WebServices.Data.EmailMessage -ArgumentList $Service
  
  # Set the message class to the correct type  
  $UmGreetingObject.ItemClass = "IPM.Configuration.Um.CustomGreetings.External"  
  
  # Mark the item as associated to put it in the hidden message table
  $UmGreetingObject.IsAssociated = $True
  
  # Set the PidTagRoamingDataTypes extended property on the new item
  $PidTagRoamingDatatypes = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x7C06,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer);  
  $UmGreetingObject.SetExtendedProperty($PidTagRoamingDatatypes,1)
  
  # Save the new object
  $UmGreetingObject.Save($FolderId)  
  
  # Repeat the search to refresh the results
  $results = $service.FindItems( $FolderId, $searchFilter, $view )
 }
 
 # Define the property set required to get the binary audio data
 $psPropset = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
 
 # Add the binary audio data property to the property set
 $PidTagRoamingBinary = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x7C09,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary);  
 $psPropset.Add($PidTagRoamingBinary)  

 # Load the new properties
 [Void]$service.LoadPropertiesForItems($results,$psPropset)

 # Loop through the search results to find the UM greeting message
 $Item = $Null
 ForEach ($Item in $Results.Items) {
  If ($Item.ItemClass -eq "IPM.Configuration.Um.CustomGreetings.External" ) {
   # Write properties of the audio file into the output object
   $Output.FileSize = [bitconverter]::ToInt32([byte[]]($AudioData[4],$AudioData[5],$AudioData[6],$AudioData[7]),0)
   $Output.Channels = [bitconverter]::ToInt16([byte[]]($AudioData[22],$AudioData[23]),0)
   $Output.SampleRateHz = [bitconverter]::ToInt16([byte[]]($AudioData[24],$AudioData[25],$AudioData[26],$AudioData[27]),0)
   
   # Set the MAPI item properties with the new audio data
   $Item.SetExtendedProperty($PidTagRoamingBinary,$AudioData)
   
   # Update the item in the mailbox
   $Item.Update([Microsoft.Exchange.WebServices.Data.ConflictResolutionMode]::AlwaysOverwrite)

   # Update the output object
   $Output.HasCustomVoicemailGreeting = $True
   $Output.VoicemailGreetingModifiedDate = $Item.LastModifiedTime
  }
 }
}

# Write the output to screen
$Output
Download Script

I strongly encourage you to try this out with a test mailbox first.

- Ben

14 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Hey Cool Script! I have an issue i was hoping you can help on line 90
    Exception calling "LoadPropertiesForItems" with "2" argument(s): "The collection is empty.
    Parameter name: items"
    At C:\root\VM\UM.ps1:90 char:5

    Cheers,
    RS

    ReplyDelete
  3. Hey RS. My guess is that the mailbox you are trying to upload to doesn't have a recorded greeting at all yet. The greeting audio is stored as data in a special message which the script looks for. If there is no recording, there is no message. I'll see if there is a way I can create the message if it doesn't exist yet, but in the meantime you could try recording a greeting (any greeting) by dialling in to the mailbox, then using the script to upload the new recording.

    - Ben

    ReplyDelete
  4. OK, I figured out how to create the message which stores the UM greeting. It was a little tricky as there was a MAPI property that needed to be set or Exchange just deleted the message whenever I called the voicemail box. Anyway I've updated the script to create it if it does not already exist in the mailbox (lines 83-102).

    Thanks for pointing out the problem.

    - Ben

    ReplyDelete
  5. This comment has been removed by the author.

    ReplyDelete
  6. This comment has been removed by the author.

    ReplyDelete
  7. This comment has been removed by a blog administrator.

    ReplyDelete
  8. Hi. Great script. But I get an error. Can you help me?
    Exception calling "Bind" with "2" argument(s): "The request failed. Unable to connect to the remote server"
    At C:\Users\adm_al\Desktop\Set-UMGreeting.ps1:56 char:1

    ReplyDelete
  9. Sounds like the client where you are running the script cannot connect to the Exchange Web Services URL. You might like to check that the auto-discovered URL is correct and accessible, or try specifying the URL manually.

    - Ben

    ReplyDelete
    Replies
    1. Thanks for answer. The Autodiscover is accessible thou. How do I set it set it manuelly?
      btw, the full error is:
      Exception calling "Bind" with "2" argument(s): "The request failed. Unable to connect to the remote server"
      At C:\Set-UMGreeting.ps1:56 char:1
      + $Folder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service,$FolderId)
      + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
      + FullyQualifiedErrorId : ServiceRequestException

      Couldn not bind to mailbox root folder.
      At C:\Set-UMGreeting.ps1:59 char:2
      + throw "Couldn not bind to mailbox root folder."
      + CategoryInfo : OperationStopped: (Couldn not bind to mailbox root folder.:String) [], RuntimeException
      + FullyQualifiedErrorId : Couldn not bind to mailbox root folder.

      Delete
  10. When I try to run this script on my environment I get:

    Exception calling "Bind" with "2" argument(s): "Exchange Server doesn't
    support the requested version."
    At D:\Telecom\Scripts\Powershell\Set-UMGreeting.ps1:57 char:1
    + $Folder =
    [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service,$FolderId)
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    ~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : ServiceVersionException

    Couldn not bind to mailbox root folder.
    At D:\Telecom\Scripts\Powershell\Set-UMGreeting.ps1:60 char:2
    + throw "Couldn not bind to mailbox root folder."
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : OperationStopped: (Couldn not bind to mailbox ro
    ot folder.:String) [], RuntimeException
    + FullyQualifiedErrorId : Couldn not bind to mailbox root folder.

    ReplyDelete
  11. Hi, I have never worked with scripts before. I downloaded the script and when I opened it it opened in Notepad. How do I run it?

    ReplyDelete
  12. Great script here. Thanks for posting.

    I ran this script in my lab which is in a 2010,2013 coexistence. The script doesn't appear to work with 2010 mailboxes but works great with 2013 mailboxes. I tried to run this script on a 2013 mailbox in my production environment but I receive the following error.

    Exception calling "Bind" with "2" argument(s): "Exchange Server doesn't support the requested version."
    At C:\Scripts\Set-UMGreeting.ps1:56 char:61
    + $Folder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind <<<< ($Service,$FolderId)
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

    Couldn not bind to mailbox root folder.
    At C:\Scripts\Set-UMGreeting.ps1:59 char:7
    + throw <<<< "Couldn not bind to mailbox root folder."
    + CategoryInfo : OperationStopped: (Couldn not bind to mailbox root folder.:String) [], RuntimeException
    + FullyQualifiedErrorId : Couldn not bind to mailbox root folder.

    The only other difference between my lab and production is that prod is also in a hybrid config with Office 365. Could this be causing the issue?

    ReplyDelete
  13. Is it any possibility for this script to work on exchange 2016?
    I dont have EWS API installed on server as I'm not sure it is supported. As I see the latest version is 2.2, you are using 2.1.
    Can be used dll located at "C:\Program Files\Microsoft\Exchange Server\V15\Bin\Microsoft.Exchange.WebServices.dll" instead?

    I'm sorry for potentialy stupid questions, but I don't know anything about EWS API.
    Thank you for your help!

    ReplyDelete