SMS / MMS / Voice Notifications with PowerShell, System Center Operations Manager and Twilio


Send inexpensive SMS text or MMS messages and voice phone calls to notification subscribers in Operations Manager 2012 using PowerShell and Twilio


Solution Guide

What It Does

This script allows System Center Operations Manager 2012 notifications to be sent via SMS and MMS messages or voice calls by integrating with the internet-based Twilio messaging service (no modem or mobile service required). It can send the dynamic content of an alert either as text in an SMS or MMS message, or machine-read as speech in a voice call. The script integrates as a Notification Channel in Operations Manager, making it easy to use alongside email and instant messaging notifications.

Note: MMS usage is not shown in the video tutorials, but is included.

SMS / Voice Notifications with PowerShell, System Center Operations Manager and Twilio
Even the best admins can't put out fires in their sleep

Usage Scenario

Most Operations Manager deployments use email for notifications of alerts. Sometimes additional methods of contact may be helpful, e.g. having critical alerts trigger a phone call to operations staff after-hours to maximize the chance of notification being delivered (who doesn’t like being woken up at 3 AM?). This script can provide a supplement to email notifications by providing automated SMS text or MMS messages and voice phone calls as alternative communication channels.

This method provides arguably the least expensive means to implement SMS and voice integration. It requires only an Internet connection and a Twilio account, which can be operated in trial mode for free, or for very low cost with a full-featured account (less than $0.01 per message). If you don’t require the robust features of third party notification add-ons like those from Derdack, this is a simple and cheap option.

The script respects the schedules configured for subscribers, so you can limit messages from being sent outside allowed notification windows.

How it Works

The script is invoked as a notification command in Operations Manager, as triggered by a subscription. It is added as a Channel alongside the built-in channels like Email and Instant Message. You can then add it to any Notification Subscription as a channel for delivery. When the subscription is triggered by an alert, the script will be executed and the specified message will be sent to each of the subscribers who has an SMS address (phone number) configured and whose notification schedules allow delivery at the time of the alert.

Twilio, PowerShell, Operations Manager notification architecture
How the notification magic happens

Twilio Integration

Twilio provides the message delivery. The script uses the Twilio API to connect to the service via HTTP and request delivery of the message to a given recipient. Therefore, a Twilio account must be available and configured for the script to use. It’s worth noting that this means an Internet connection is required for message delivery. Therefore, rest assured that this script will never notify you about your Internet connection being down.

Delivery Methods

Messages can be delivered by SMS text message or voice phone call. The method is determined by the MessageType parameter used when invoking the script (“SMS”, "MMS", or “Voice”). SMS messages are truncated to fit within the SMS 160 character limit, and MMS messages to fit within the MMS 1600 character limit. For voice calls, the message text is read aloud by a robot voice.

Configuration

There are two aspects of configuring the script. First, an XML configuration file is used to provide Twilio account details. Because the account credentials can be used to manipulate the account (whether accidentally or maliciously), ensure the configuration file is protected by reasonable means to be accessible only to the Operations Manager service account and trusted administrators.

Second, the script command-line parameters are used to specify the runtime options. These are documented in the script comments.

MMS vs SMS Message Types

You can choose between SMS and MMS when sending text messages. There are advantages and disadvantages of each:

SMS (text messages)

MMS ("picture messages")

To use MMS, just use "-MessageType MMS" in your command. To include an image, you can provide a URL to any publicly-accessible image on a website. Example command line parameters:

 -NoProfile -File Send-TwilioMessage.ps1 -MessageType MMS -MessageText "Ops Alert: $Data[Default='Not Present']/Context/DataItem/AlertName$" -SubscriptionID "$MPElement$" -EnableTraceLogging -MMSImageURL "https://automys.blob.core.windows.net/static/automys-logo.png"

 

MMS notification with logo image and multiple paragraphs
MMS notification with logo image and multiple paragraphs

 

Implementing In Your Environment

This section describes how to get the script working. A step-by-step installation video is also included.

Prerequisites

We’ll assume the following is in place before getting started:

Twilio Setup

First you’ll want to set up a Twilio account. We won’t go into all of the details of this awesome service here, as there is plenty of information on the web about that. For now, getting a free trial account created is sufficient.

  1. Go to Twilio.com and follow the signup process. You’ll choose a phone number here.

  2. When signup is done, you should find yourself on the getting started page. Here, locate your Twilio number and the API credentials for the account.

  3. Trial accounts can only contact phone numbers that are pre-verified in your account. To add numbers you want to call or text while testing, add them under Numbers > Verified Caller IDs.

That’s it. Leave the page up to grab the account info in the next step.

Script Setup

Next, you’ll get the script set up on the Operations Manager management server. This is where it lives, optionally alongside the configuration, trace log, and API library files.

  1. Unzip the downloaded solution files into a directory of your choice that is accessible to the Operations Manager service account. For example: C:\OpsMgr_Custom_Scripts\Notifications

  2. Open the XML configuration file with notepad. Fill in the AccountSID, AuthToken, and SenderPhoneNumber elements from your Twilio account page. When you finish, it should look something like the below. For the APIFilesPath, leave as “.” unless moving the API files to another folder (in which case, change it to that path). (See example below)

  3. Ensure the API library files are present: Twilio.Api.dll and RestSharp.dll.

  4. Optional: if wanting to obtain the above files independently for security or to use the latest version, perform the following steps to download. A zip utility like 7-zip must be installed.

    1. Open https://www.nuget.org/api/v2/package/Twilio in a browser. This should trigger the download of a “nupkg” file, which is a Nuget package for the .Net Twilio API.

    2. Open the file with your zip utility and locate Twilio.Api.dll within lib\3.5. Copy this to the script folder configured above.

    3. Open https://www.nuget.org/api/v2/package/RestSharp in a browser. This should trigger the download of a “nupkg” file, which is a Nuget package for the .Net REST API.

    4. Open the file with your zip utility and locate RestSharp.dll within lib\net4. Copy this to the script folder configured above.

  5. Ensure PowerShell script execution is allowed on the server

    1. Open a PowerShell prompt as administrator

    2. Check the current execution policy. Verify result is RemoteSigned, Unrestricted, or Bypass.

      PS >Get-ExecutionPolicy
      
    3. If current policy is none of the above, update the policy. Confirm any prompts.

      PS >Set-ExecutionPolicy RemoteSigned

Example configuration file:

<?xml version="1.0" encoding="utf-8"?>
<!-- This information can be used to modify or use your Twilio account. Ensure it is appropriately protected -->
<Settings>
  <Twilio>
    <AccountSID>AC0d1dfd9fdb4d14f3f0f8e7eb36a8</AccountSID>
    <AuthToken>bcf68188673581adf3569bd4c52f</AuthToken>
    <SenderPhoneNumber>+12244042990</SenderPhoneNumber>
    <APIFilesPath>.</APIFilesPath> 
  </Twilio>
</Settings>

 

The script is now ready to go. Next, we configure Operations Manager to use it.

Operations Manager Configuration

To integrate the script into Operations Manager, we’ll create a new Channel and then use the channel in a subscription.

  1. Open the Operations Console.

  2. Go to Notifications > Channels in the Administration workspace.

  3. From the tasks pane, select New > Command.

  4. Give the new notification channel a name and description, e.g. “Twilio Messaging” / “Sends SMS or voice calls via Twilio service”. Next.

  5. The Settings area is where the integration is configured. Configure as follows (see video and script comments for more explanation), and Finish.

    Setting

    Example Value

    Full path of the command file

    C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe

    Command line parameters

    -NoProfile -File Send-TwilioMessage.ps1 -MessageType SMS -MessageText "Ops Alert: $Data[Default='Not Present']/Context/DataItem/AlertName$" -SubscriptionID "$MPElement$" -EnableTraceLogging

    Startup folder

    C:\OpsMgr_Custom_Scripts\Notifications

  6. Create or open one or more subscribers for testing from Notifications > Subscribers.

  7. In the Addresses area, add a new address of type Text Message (SMS). Include a valid phone number as the delivery address (E.164 format recommended). Schedules can be left at default for testing.

  8. Create a placeholder subscriber to use for subscription triggering. This can be any non-human account (e.g. an Operations Manager service account), and is used only to cause the subscription to trigger our script command channel. This subscriber will not have an address or receive notifications.

    1. Create new subscriber, selecting the domain account to use

    2. For schedule, select Always send notifications.

    3. In the addresses area, add a new address. Name the address “Twilio Command Placeholder”.

    4. For the address channel, select the Command channel type and the new command channel created above. No address is configured here.

    5. For address schedule, select Always send notifications.

  9. Under Notifications > Subscriptions, create a New test subscription from the task pane.

  10. Provide name and description, .e.g. “SMS Alerts”.

  11. In the Criteria area, specify any criteria that will match the alert you will use for testing purposes.

  12. In Subscribers, add the SMS-enabled subscribers you configured above as well as the placeholder account.

  13. In Channels, add the new channel created above.

  14. Finish the wizard.

At this point, Operations Manager is configured via our new subscription to watch for certain alerts that will cause it to trigger SMS notification via our fancy script command channel.

Using Multiple Message Types

In the Channel setup above, we specified a MessageType in the script parameters. This determines whether the message is sent via SMS, MMS or voice call. What if we want to have multiple delivery types, perhaps in different subscriptions or circumstances? For example, perhaps you want a subscription for critical off-hours alerts that uses voice call notifications, and a different subscription for lesser severity alerts that uses SMS and Email only.

To accomplish this, set up two notification channels each with a different MessageType parameter, for example one with “–MessageType SMS” and one with “–MessageType Voice”, leaving the other settings the same. Then, you can choose which to use in a given subscription.

Notification channels for SMS text and voice call messages
Use separate channel for each message type

Kicking the Tires

The moment of glory is near. Before testing from Operations Manager, it is easiest to test from the command line and ensure the script runs successfully. Once that is confirmed, we’ll move on to get an Operations Manager alert to trigger via subscription.

Manual Test

To manually test, open a PowerShell prompt and change to your script directory:

PS C:\Users\Noah> cd C:\OpsMgr_Custom_Scripts\Notifications

 

Now, call the script using a test message and the name of the test subscription. It’s also helpful to include the EnableTraceLogging switch parameter to see a detailed log of everything that happens:

.\Send-TwilioMessage.ps1 -MessageText "This is ground control to Major Tom" -MessageType SMS -SubscriptionID "SMS Alerts" –EnableTraceLogging

 

With any luck, you’ll receive a text message within a few seconds. Open the trace.log file in the script directory to see details, including any error messages that may have occurred.

If SMS isn’t exciting enough, also test a voice call:

.\Send-TwilioMessage.ps1 -MessageText "I’m stepping through the door and floating in a most peculiar way" -MessageType Voice -SubscriptionID "SMS Alerts" –EnableTraceLogging

 

And a robotic reading should greet your ears in a moment.

Note that for trial accounts, messages will include an addition about trial status.  This is removed when upgrading to a paid account.

Subscription Test

To test the full integration, all that’s needed now is to trigger an alert that is configured in the test subscription. I like to use a custom task and rule that creates a synthetic/fake event in the event log in order to trigger an alert just for testing. It’s also helpful to add email as a channel to the test subscription such that an email is sent at the same time you expect a notification via SMS or voice. This way, you have an easy way to know the subscription triggered if you don’t receive a corresponding SMS or call.

Schedule Testing

You may also want to test any schedules that you implement on the subscriptions to limit when SMS / voice notifications are used. The script respects the schedules configured on both subscribers and their individual addresses, so you could limit SMS to be used only after business hours, for example, or calls to happen only on weekends and lunch hours, or any other time people would rather not be bothered.

Readying for Production

After testing demonstrates everything is working, remember a couple considerations before going live.

First, consider removing the EnableTraceLogging switch from the command parameters for the Command Channel. This performs detailed logging that may grow into a large file in production. If leaving it on, make sure Operations Manager is monitoring your disk space.

Second, it is probably not advisable to try using a Twilio trial account past the testing phase. It’s cheap and they provide an excellent service, so pony up.

As always, please reach out to me if you found this helpful, have questions, or are interested in working together on something more advanced. Enjoy!

Script

<#
    .SYNOPSIS
        Sends a message via SMS or voice call using the Twilio messaging service to 
        recipients defined in the specified System Center Operations Manager subscription.

    .DESCRIPTION
        This script is intended to be invoked as a notification command in System Center 
        Operations Manager 2012/R2. It accepts a message in the form of a string and sends
        to a list of phone numbers retrieved from the recipients defined in the specified 
        subscription. The message is truncated to fit with the 160 character SMS limit and
        delivered via the Twilio messaging service (see www.twilio.com). 
        
        A valid Twilio account must be provided to the script via an associated configuration file.
        Additionally, the .Net library for the Twilio API must be available in a configured path
        on the system.
        
        The recipients must be Subscribers defined in Operations Manager with a "Text Message (SMS)"
        address defined, which is a phone number. The number can be in one of several formats, though
        Twilio prefers E.164, e.g.(for US) +15551234567. 

    .PARAMETER  MessageText
        The text of the message to send as a string. Will be truncated to fit within the 160
        character SMS limit. For voice calls, Twilio will perform text-to-speech conversion.
    
    .PARAMETER  MessageType
        Choice of SMS, MMS or Voice. SMS will be delivered as a normal text message with max length of 160 characters. 
        MMS will be delivered as MMS message with max length 1600 characters. Limited to US/Canada only by Twilio as of April 2015.
        Voice will be delivered as a voice phone call with machine reading of the MessageText.

    .PARAMETER  SubscriptionID
        The ID property of the Operations Manager subscription that defines recipients for this
        message. Preferred format is "{GUID}" and is provided in notifcation channel command parameters using the
        "$MPElement$" substituion variable. Alternatively, script will attempt to use as DisplayName
        to find subscription by name rather  than ID.

    .PARAMETER  ConfigurationFilePath
        The directory containing the XML configuration file which defines the Twilio account SID, 
        auth token, sending phone number, and API files path. 

    .PARAMETER  EnableTraceLogging
        Switch to enable trace logging to a file in the same directory as the script. Unless included, 
        no trace data will be logged.

    .PARAMETER  MMSImageURL
        Optional URL of a publicly-accessible image to include in the MMS message. This is read by Twilio
        and inserted as image content in the message.

    .EXAMPLE
        To manually test with trace logging from a PowerShell prompt using a subscription ID obtained from Operations Manager:
        
        .\Send-TwilioMessage.ps1 -MessageText "London Bridge is falling down!" -MessageType SMS -SubscriptionID "{5B2E1566-39E8-DB71-4A19-2C55FEF4829A}" -EnableTraceLogging

    .EXAMPLE
        To manually test a voice call from a PowerShell prompt using a subscription ID obtained from Operations Manager:
        
        .\Send-TwilioMessage.ps1 -MessageText "London Bridge is falling down!" -MessageType Voice -SubscriptionID "{5B2E1566-39E8-DB71-4A19-2C55FEF4829A}"

    .EXAMPLE
        Configured as command parameters to powershell.exe in Operations Manager notification channel command,
        sending the alert name as an SMS message:
        
        -File Send-TwilioMessage.ps1 -MessageType SMS -MessageText "OpsMgr Alert: $Data[Default='Not Present']/Context/DataItem/AlertName$" -SubscriptionID "$MPElement$"

    .INPUTS
        None.

    .OUTPUTS
        No objects returned.

    .NOTES
        For more details and implementation guidance, see the associated documentation at https://automys.com
#>

[CmdletBinding()]
Param(
    [Parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [string]$MessageText,

    [ValidateSet("SMS","MMS","Voice")]
    [string]$MessageType = "SMS",

    [Parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [string]$SubscriptionID,

    [string]$ConfigurationFilePath,

    [switch]$EnableTraceLogging,

    [string[]]$MMSImageURL
)


# Define function to add entry to trace log located in same folder as script
function AppendLog ([string]$Message)
{
    if($EnableTraceLogging -eq $true)
    {
        Add-Content -Path $logPath -Value ((Get-Date).ToString() + "`t" + $Message)
    }
}

# Define function to check the master and address schedules for an individual subscriber
# Returns true if current time conforms to schedules, false if not
function CheckSubscriberSchedules ($Subscriber, $Address)
{
    $scheduleValidated = $true

    # Check against subscriber master schedule(s). If any violated, overall check not satisifed.
    foreach($scheduleEntry in $Subscriber.ScheduleEntries)
    {
        if((CheckSchedule -Schedule $scheduleEntry) -eq $false)
        {
            $scheduleValidated = $false
        }
    }
            
    # Check against subscriber address (phone number) schedule(s). If any violated, overall check not satisifed.
    foreach($scheduleEntry in $Address.ScheduleEntries)
    {
        if((CheckSchedule -Schedule $scheduleEntry) -eq $false)
        {
            $scheduleValidated = $false
        }
    }

    return $scheduleValidated
}

# Define function to convert a NotificationRecipientScheduleEntry time range object to a standard DateTime format in the specified time zone
function ConvertTimeRange ($EntryTime, $TimeZone)
{
    $convertedTime = Get-Date -Hour $EntryTime.Hour -Minute $EntryTime.Minute -Second 0
    return [System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId($convertedTime, $scheduleTimeZone)
}

# Define function to check a single schedule
# Returns true if current time conforms to schedule, false if not
function CheckSchedule ($Schedule)
{
    if($Schedule -eq $null)
    {
        return $true
    }

    # Begin with no violations, setting if found
    $scheduleViolated = $false
            
    # Get current time in target time zone
    $scheduleTimeZone = $Schedule.TimeZone.Substring($Schedule.TimeZone.IndexOf("|") + 1)
    $now = [System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId([DateTime]::UtcNow, $scheduleTimeZone)

    # Date range check
    # If date range defined and current time is outside the range, record violation. Otherwise, check passes.
    if($Schedule.ScheduledStartDate -ne $null -and $Schedule.ScheduledEndDate -ne $null)
    {
        if($now -lt $Schedule.ScheduledStartDate -or $now -gt $Schedule.ScheduledEndDate)
        {
            $scheduleViolated = $true
        }
    }

    # Daily time range check
    # If current time is outside the daily time range, record violation. Otherwise, check passes.
    if($now -lt (ConvertTimeRange -EntryTime $Schedule.DailyStartTime -TimeZone $scheduleTimeZone) -or $now -gt (ConvertTimeRange -EntryTime $Schedule.DailyEndTime -TimeZone $scheduleTimeZone))
    {
        $scheduleViolated = $true
    }

    # Day of week test
    $allowedDays = @()
    $scheduleDaysString = $Schedule.ScheduledDays.ToString()
    $scheduleDaysString = $scheduleDaysString -replace "Weekdays","Monday,Tuesday,Wednesday,Thursday,Friday"
    $scheduleDaysString = $scheduleDaysString -replace "WeekendDays","Saturday,Sunday"
    switch ($scheduleDaysString)
    {
        "None" {
            # No days allowed
        }
        "All" {
            $allowedDays += "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"
        }
        default {
            # One or more days by name, comma separated
            $allowedDays += $scheduleDaysString -replace " ","" -split "," 
        }
    }
            
    # If today is not in the list of allowed days, record violation
    if(($allowedDays -contains $now.DayOfWeek) -eq $false)
    {
        $scheduleViolated = $true
    }

    # Determine overall result
    # If now is outside the schedule and we wanted to be inside, return false to indicate schedule not satisifed
    if ($Schedule.ScheduleEntryType -eq "Inclusion" -and $scheduleViolated -eq $true)
    {
        return $false
    }
    # If now is within the schedule but we wanted to exclude these times, return false to indicate schedule not satisifed
    elseif ($Schedule.ScheduleEntryType -eq "Exclusion" -and $scheduleViolated -eq $false)
    {
        return $false
    }
    # Otherwise, the schedule was satisifed
    else
    {
        return $true
    }
}

function TruncateMessage ($MessageText, $CharacterLimit)
{
    $ellipses = "..."
    $contentLimit = $CharacterLimit - $ellipses.Length
    if($MessageText.Length -gt $contentLimit)
    {
        $MessageText = $MessageText.Substring(0, $contentLimit) + $ellipses
    }
    return $MessageText
}

# Test access to log file, create new name if denied (likely created by another user or process)
$logPath = $PSScriptRoot + "\trace.log"
try 
{ 
    [IO.File]::OpenWrite($logPath).Close() 
}
catch 
{
    $logSuffix = Get-Date -Format "yyyyMMddhhMMss"
    $logPath = "$PSScriptRoot\trace-$logSuffix.log"
}

if($ConfigurationFilePath.Length -eq 0)
{
    $ConfigurationFilePath = $PSScriptRoot + "\Send-TwilioMessage_config.xml"
}

AppendLog -Message "Script started"
AppendLog -Message "Running as user [$([Environment]::UserDomainName)\$([Environment]::UserName)]"
AppendLog -Message "MessageText=[$MessageText]; MessageType=[$MessageType]; SubscriptionID=[$SubscriptionID]; ConfigurationFilePath=[$ConfigurationFilePath]"

try 
{
    AppendLog -Message "Reading configuration file"
    # Check for the expected configuration file
    if((Test-Path $ConfigurationFilePath) -eq $false)
    {
        throw "Configuration file not found at expected path: $ConfigurationFilePath"
    }

    # Read and validate configuration parameters from file
    [xml]$configFile = Get-Content $ConfigurationFilePath

    if($configFile -eq $null -or $configFile.Settings -eq $null -or $configFile.Settings.Twilio -eq $null)
    {
        throw "Error reading the configuration file $ConfigurationFilePath. Verify the format is correct."
    }

    if($configFile.Settings.Twilio.AccountSID.Length -eq 0)
    {
        throw "No value defined for AccountSID in configuration file"
    }

    if($configFile.Settings.Twilio.AuthToken.Length -eq 0)
    {
        throw "No value defined for AuthToken in configuration file"
    }

    if($configFile.Settings.Twilio.SenderPhoneNumber.Length -eq 0)
    {
        throw "No value defined for SenderPhoneNumber in configuration file"
    }

    if($configFile.Settings.Twilio.APIFilesPath.Length -eq 0)
    {
        $configFile.Settings.Twilio.APIFilesPath = "."
    }

    # Check for Twilio API library files
    AppendLog -Message "Loading Twilio API library"
    $libraryFilesPath = $configFile.Settings.Twilio.APIFilesPath.TrimEnd('\')
    if($libraryFilesPath -eq ".")
    {
        # If "." path configured, expect files in the same folder as script
        $libraryFilesPath = $PSScriptRoot
    }

    $libraryFileList = "Twilio.Api.dll","RestSharp.dll"
    foreach($fileName in $libraryFileList)
    {
        $filePath = $libraryFilesPath + "\" + $fileName
        if((Test-Path $filePath) -eq $false)
        {
            throw "Required API file $fileName not found at expected path $libraryFilesPath"
        }
    }

    # Load Twilio .NET API library
    Add-Type -Path ($libraryFilesPath + "\" + "Twilio.Api.dll")

    # Create Twilio client object
    $twilioClient = New-Object Twilio.TwilioRestClient($configFile.Settings.Twilio.AccountSID, $configFile.Settings.Twilio.AuthToken)

    # Verify access to account
    AppendLog -Message "Validating Twilio account"
    $accountTest = $twilioClient.GetAccount()
    if($accountTest -eq  $null -or $accountTest.Sid.Length -eq 0)
    {
        $errorMessage = "Failed to access Twilio account. Validate the account SID and auth token in the configuration file match the API Credentials shown at https://www.twilio.com/user/account."
        if($accountTest.RestException -ne $null -and $accountTest.RestException.Message.Length -gt 0)
        {
            $errorMessage += " Details: " + $accountTest.RestException.Message
        }
        throw $errorMessage
    }

    # Load Operations Manager PowerShell module
    AppendLog -Message "Loading Operations Manager PowerShell module"
    Import-Module OperationsManager
    if((Get-Module OperationsManager) -eq $null)
    {
        # If not loaded by name, try the path to module from Registry
        $modulePath = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\System Center Operations Manager\12\Setup\Powershell\V2" | select -ExpandProperty InstallDirectory
        $modulePath += "\OperationsManager\OperationsManager.psd1"
        Import-Module $modulePath
    }
    if((Get-Module OperationsManager) -eq $null)
    {
        # Not found by either method. Throw error and exit.
        throw "Failed to load PowerShell module for System Center Operations Manager" 
    }

    # Get the notifcation subscription specified as parameter. Try both name and GUID.
    AppendLog -Message "Retrieving subscription and recipient details"
    $subscription = Get-SCOMNotificationSubscription | where {$_.Id -eq $SubscriptionID -or $_.DisplayName -eq $SubscriptionID}

    # Validate subscription
    if($subscription -eq $null)
    {
        throw "No notification subscription found with ID [$SubscriptionID]"
    }
    AppendLog -Message "Found subscription [$($subscription.DisplayName)]"

    # Get list of recipients to subscription
    $recipientList = $subscription.ToRecipients + $subscription.CcRecipients + $subscription.BccRecipients

    # Validate recipient list
    if($recipientList -eq $null -or $recipientList.Count -eq 0)
    {
        throw "No recipients found for subscription with ID [$SubscriptionID]"
    }
    AppendLog -Message "Found recipients: [$(($recipientList | select -ExpandProperty Name) -join ",")]"

    # Get phone number list for recipients
    $phoneList = @()
    foreach($recipient in $recipientList)
    {
        $smsAddress = $recipient.Devices | where Protocol -eq SMS
        if($smsAddress -ne $null -and (CheckSubscriberSchedules -Subscriber $recipient -Address $smsAddress) -eq $false)
        {
            AppendLog -Message "Schedules for $($recipient.Name) do not allow SMS/Voice notifications at current time"
        }

        $phoneNumber = $smsAddress.Address
        if($phoneNumber.Length -gt 0)
        {
            $phoneList += $phoneNumber
        }
        else
        {
            AppendLog -Message "No phone number configured for recipient: $($recipient.Name)"
        }
    }

    # Validate phone list
    if($phoneList.Count -eq 0)
    {
        throw "No phone numbers found for recipients of subscription with ID [$SubscriptionID]"
    }
    AppendLog -Message "Found phone numbers: [$($phoneList -join ",")]"

    # Replace any line breaks to be parsed correctly
    $MessageText = $MessageText -replace "\\n","`n"

    # Send message via the specified method to each recipient
    AppendLog -Message "Sending messages"
    $errorList = @()
    foreach($recipient in $phoneList)
    {
        switch($MessageType)
        {
            "SMS" {
                # Truncate message to SMS limit
                $SMS_CHARACTER_LIMIT = 160
                $MessageText = TruncateMessage -MessageText $MessageText -CharacterLimit $SMS_CHARACTER_LIMIT

                # Send SMS message using supplied message text
                $sendResult = $twilioClient.SendSmsMessage($configFile.Settings.Twilio.SenderPhoneNumber, $recipient, $MessageText)
            }

            "MMS" {
                # Truncate message to MMS limit
                $MMS_CHARACTER_LIMIT = 1600
                $MessageText = TruncateMessage -MessageText $MessageText -CharacterLimit $MMS_CHARACTER_LIMIT

                # Send MMS message using supplied message text including image if specified
                if($MMSImageURL.Length -gt 0)
                {
                    $sendResult = $twilioClient.SendMessage($configFile.Settings.Twilio.SenderPhoneNumber, $recipient, $MessageText, $MMSImageURL)
                }
                else
                {
                    $sendResult = $twilioClient.SendMessage($configFile.Settings.Twilio.SenderPhoneNumber, $recipient, $MessageText)
                }
                
            }
    
            "Voice" {
                # Build voice message URL
                [Reflection.Assembly]::LoadWithPartialName("System.Web") | Out-Null
                $encodedText = [System.Web.HttpUtility]::UrlEncode($MessageText)
                $messageURL = "http://twimlets.com/message?Message%5B0%5D=" + $encodedText

                # Initiate the voice phone call
                $sendResult = $twilioClient.InitiateOutboundCall($configFile.Settings.Twilio.SenderPhoneNumber, $recipient, $messageURL)
            }
        }

        # Check results
        if($sendResult -eq $null -or $sendResult.Status.Length -eq 0)
        {
            $errorMessage = "Failed to send message."
            if($sendResult.RestException -ne $null -and $sendResult.RestException.Message.Length -gt 0)
            {
                $errorMessage += " Exception details: Status=[" + $sendResult.RestException.Status + "], Message=[" + $sendResult.RestException.Message + "]"
            }
            $errorList += $errorMessage
        }
        else
        {
            AppendLog -Message "Successfully sent message to [$recipient]"
        }
    }

    # Validate results
    if($errorList.Count -gt 0)
    {
       $errorString = $errorList -join ";"
       throw "Encountered $($errorList.Count) failures while sending messages. Details: [$errorString]"
    }
}
catch 
{
    AppendLog -Message "ERROR: $($error[0].Exception.Message)"
}
finally
{
    AppendLog -Message "Script finished"
}

Discuss