1/24/17 Update: The script has been updated to v1.3, fixing support for mailboxes on Exchange 2010. There are also some minor fixes, such as correctly using autodiscover when neither an EWS URL nor the Exchange Online switch is used.
4/18/16 Edit: I was reviewing this script for an unrelated reason when I discovered that I had used incorrect construction in the begin block since you cannot access parameter values in it. I have updated the script in the download and the inline code at the end of the post that any any code that references parameters has been moved to the process block.
Exchange has the ability to send text messages to specific carriers in a few countries, and is enabled by default. This allows users to configure calendar notifications (such as changes to meetings that are occurring in the next three days) and rules to forward email as a text message. Users have to use OWA (or if you prefer the new name, Outlook on the web) to configure this. But what if your users do this before you realize it is enabled by default and now you want to disable it?
If you modify the role assignment policy to remove MyTextMessaging or modify OWA Mailbox policy to remove Text Messaging, it hides this feature from users, but it doesn’t disable anything already in place. You then decide to use PowerShell to run Clear-TextMessagingAccount for someone, but it says the user cannot be read. You can run it for your own account, but nobody else, even as an admin. This is because the write scope of the role that contains the cmdlet is Self. So how to remove the settings for another user?
I wrote a script that uses the EWS Managed API modify the hidden messages that contain the settings and delete any inbox rules that are forwarding to a mobile device. I should point out that doing it this way is unsupported, but I have used it successfully for mailboxes on Exchange 2013 and in Exchange Online.
The calendar notification settings and text messaging configuration are stored in folder associated items (FAI) in the root folder of the mailbox, in the roaming XML property of a user configuration message. Because of this, you can use the Microsoft.Exchange.WebServices.Data.UserConfiguration class to easily get messages with a specific subclass and retrieve this property without having to define a property set with the extended MAPI property. The subclass for the calendar notification settings is CalendarNotification.001 and text messaging configuration is TextMessaging.001. If you already have a service object created, you can get the message for calendar notification with these two lines:
1 2 |
$folderId = New-Object -TypeName Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Root,'alias@company.com') $calNotify = [Microsoft.Exchange.WebServices.Data.UserConfiguration]::Bind($exchangeService, 'CalendarNotifcation.001', $folderId, [Microsoft.Exchange.WebServices.Data.UserConfigurationProperties]::All) |
The roaming properties of a user configuration message are stored in the Dictionary, XmlData, and BinaryData properties of the search result object. The property for the calendar notification settings (PR_ROAMING_XMLSTREAM as the XmlData property) is a binary value returned as a byte array, so it needs to be converted to a string cast as an XML object so it can be manipulated with XML methods:
1 |
[xml]$calStream = [System.Text.Encoding]::ASCII.GetString($calNotify.XmlData) |
The three notification types have their own node and contains an element whose value indicates whether it is enabled. Since I don’t care what the other options are, only that they are disabled, this can be done by directly setting the value for the element:
1 2 3 |
$calStream.CalendarNotificationSettings.UpdateSettings.Enabled = 'false' $calStream.CalendarNotificationSettings.ReminderSettings.Enabled = 'false' $calStream.CalendarNotificationSettings.SummarySettings.Enabled = 'false' |
To write the data back to the XmlData property and save it in the mailbox, it needs to be converted back to a byte array. This isn’t done with a one-liner like converting from a byte array. The XML data is converted to a string, which is then converted to a byte array. There could be a more efficient way of doing this, but I don’t know it at the time of this writing. The first line is the one-liner to take the XML data and store it as a byte array in the property, the second saves the message back to the mailbox, and the two functions that convert XML to a string and a string to a byte array follow:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
$calNotify.xmlData = Convert-StringToByteArray -string (Convert-XmlToString -xml $calStream) $calNotify.Update() function Convert-XmlToString ($xml) { $sw = New-Object -TypeName System.IO.StringWriter $xmlSettings = New-Object -TypeName System.Xml.XmlWriterSettings $xmlSettings.ConformanceLevel = [System.Xml.ConformanceLevel]::Fragment $xmlSettings.Indent = $true $xw = [System.Xml.XmlWriter]::Create($sw, $xmlSettings) $xml.WriteTo($xw) $xw.Close() $sw.ToString() } function Convert-StringToByteArray ($string) { $byteArray = New-Object -TypeName Byte[] -ArgumentList $string.Length $i = 0 foreach ($char in $string.ToCharArray()) { $byteArray[$i] = [byte]$char $i++ } ,$byteArray } |
For the text messaging configuration, it is in the same property of its message. Once converted to XML, devices are stored in the MachineToPersonMessagingPolicies node, with a PossibleRecipient node for each device that has ever been configured. To simply delete any devices, you can remove all sub-nodes since there aren’t any others:
1 |
$textStream.SelectSingleNode('//MachineToPersonMessagingPolicies').RemoveAll() |
Then convert the XML data back to a byte array and save the message the same as before.
What remains are any inbox rules that may have been created that forward to a text messaging device. As an admin, you can use PowerShell to get rules, but you won’t see any rules that have been disabled in Outlook. Even if a rule is visible because it is enabled or has been disabled via OWA, and so you are able to see if a given rule is forwarding to a text messaging device, if you delete the rule, you will also delete any rules that are currently disabled via Outlook. What’s worse, you won’t even know if there are disabled rules that will be deleted because the warning is presented for every mailbox regardless of the existence of any applicable rules.
So the script will get all FAI messages that are rules and delete any that are forwarding to a device configured via the text messaging feature. The first step is to get the rules by searching for all FAIs in the inbox whose class is that of a rule:
1 2 3 4 5 6 7 8 9 |
$folderId = New-Object -TypeName Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,'alias@company.com') $searchFilter1 = New-Object -TypeName Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::ItemClass, 'IPM.Rule.Version2.Message') $searchFilter2 = New-Object -TypeName Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::ItemClass, 'IPM.Rule.Message') $searchFilterCollection = New-Object -TypeName Microsoft.Exchange.WebServices.Data.SearchFilter+SearchFilterCollection([Microsoft.Exchange.WebServices.Data.LogicalOperator]::Or) $searchFilterCollection.Add($searchFilter1) $searchFilterCollection.Add($searchFilter2) $itemView = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ItemView(100) $itemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Associated $inboxRules = $exchangeService.FindItems($folderId, $searchFilterCollection, $itemView) |
After getting the rules, we need to retrieve the property that contains a rule’s actions, which is PR_EXTENDED_RULE_ACTIONS (0x0E990102) when on Exchange 2013+ or 0x65EF0102 when on Exchange 2010, a binary property:
1 2 3 4 |
$propExtRuleActionsV2 = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x0E99,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary) $propExtRuleActionsV1 = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x65EF,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary) $propertySet = New-Object -TypeName Microsoft.Exchange.WebServices.Data.PropertySet(@($propExtRuleActionsV2,$propExtRuleActionsV1)) [void]$exchangeService.LoadPropertiesForItems($inboxRules, $propertySet) |
Parsing the binary data is not easy (for me) because it includes pieces of variable-length information. If the entire value is converted to a string, however, an action that forwards to a configured text messaging device contains the string MOBILE: followed by the E.164-formatted phone number. So, all that needs to be done is to get the rule’s actions, convert it to string, check for MOBILE, and delete the rule:
1 2 3 4 5 6 7 8 9 10 11 |
foreach ($rule in $inboxRules.Items) { $ruleActionsV2 = $null if ($rule.TryGetProperty($propExtRuleActionsV2,[ref]$ruleActionsV2)) { if ([System.Text.Encoding]::ASCII.GetString($ruleActionsV2) -like '*MOBILE:*') { $rule.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete) } } } |
The script supports on-premises and Exchange Online, autodiscover or specified URL, pipelining mailboxes into it, impersonation and specifying credentials. The output will contain what actions it took on a mailbox, including whether any of the features were not configured in the first place. You can run it multiple times against a mailbox without it having an issue that any or all features are not configured. The full script can be expanded below, and it can also be downloaded via the following link:
Remove-TextMessagingConfiguration.zip (3.1 KiB)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
<# .Synopsis Remove text messaging configuration and inbox rules .Description Disable calendar notification, remove mobile devices added as a text messaging device and delete inbox rules that forward to a text messaging device. Works with Exchange 2010/2013/2016/EXO mailboxes. .Parameter EmailAddress Email address of the mailbox. Accepts pipeline input from Get-Mailbox. .Parameter EWSUrl To not use autodiscover, specify the URL to use for EWS. .Parameter Credential Provide credentials to use instead of the current user. .Parameter EWSApiPath Explicit path to EWS API DLL if it has not been installed via setup routine. .Parameter UseImpersonation Switch to specify connection to the mailbox via impersonation instead of full mailbox access. .Parameter UseExchangeOnline Switch to use the hard-coded EWS URL for Exchange Online. Cannot be used with the EWSUrl parameter. .Example Remove-TextMessagingConfiguration.ps1 -EmailAddress johndoe@company.com -Credential (get-credential) .Example Get-Mailbox johndoe | Remove-TextMessagingConfiguration -EWSUrl -UseImpersonation .Notes Version: 1.3 Date: 1/24/17 #> #requires -version 3 [CmdletBinding(DefaultParameterSetName='autod')] param ( [parameter(Mandatory=$true,Position=0,ValueFromPipelinebyPropertyName=$true)][Alias('PrimarySMTPAddress')]$EmailAddress, [parameter(Mandatory=$false,ParameterSetName='ews')][string]$EWSUrl, [parameter(Mandatory=$false,ParameterSetName='exo')][switch]$UseExchangeOnline, [parameter(Mandatory=$false)][pscredential]$Credential, [parameter(Mandatory=$false)][string]$EWSApiPath, [switch]$UseImpersonation ) begin { $firstRun = $true function Get-UserConfigurationMessage ($targetAddress, $className, $impersonate) { if ($impersonate) { $exchangeService.ImpersonatedUserId = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $targetAddress) } #Bind to root of mailbox and return FAI with configuration class of specified name $folderId = New-Object -TypeName Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Root,$targetAddress) [Microsoft.Exchange.WebServices.Data.UserConfiguration]::Bind($exchangeService, $className, $folderId, [Microsoft.Exchange.WebServices.Data.UserConfigurationProperties]::All) } function Get-Rules ($targetAddress, $impersonate) { if ($impersonate) { $exchangeService.ImpersonatedUserId = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $targetAddress) } #Search inbox for rule messages $folderId = New-Object -TypeName Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$targetAddress) #Message class on 2013+ $searchFilter1 = New-Object -TypeName Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::ItemClass, 'IPM.Rule.Version2.Message') #Message class on 2010 $searchFilter2 = New-Object -TypeName Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::ItemClass, 'IPM.Rule.Message') $searchFilterCollection = New-Object -TypeName Microsoft.Exchange.WebServices.Data.SearchFilter+SearchFilterCollection([Microsoft.Exchange.WebServices.Data.LogicalOperator]::Or) $searchFilterCollection.Add($searchFilter1) $searchFilterCollection.Add($searchFilter2) $itemView = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ItemView(100) $itemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Associated ,$exchangeService.FindItems($folderId, $searchFilterCollection, $itemView) } function Convert-XmlToString ($xml) { $sw = New-Object -TypeName System.IO.StringWriter $xmlSettings = New-Object -TypeName System.Xml.XmlWriterSettings $xmlSettings.ConformanceLevel = [System.Xml.ConformanceLevel]::Fragment $xmlSettings.Indent = $true $xw = [System.Xml.XmlWriter]::Create($sw, $xmlSettings) $xml.WriteTo($xw) $xw.Close() $sw.ToString() } function Convert-StringToByteArray ($string) { $byteArray = New-Object -TypeName Byte[] -ArgumentList $string.Length $i = 0 foreach ($char in $string.ToCharArray()) { $byteArray[$i] = [byte]$char $i++ } ,$byteArray } } process { if ($firstRun) { #Test if any version of API is installed before continuing if ($EWSApiPath) {$apiPath = $EWSApiPath} else { $apiPath = (($(Get-ItemProperty -ErrorAction SilentlyContinue -Path Registry::$(Get-ChildItem -ErrorAction SilentlyContinue -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Exchange\Web Services' | Sort-Object Name -Descending | Select-Object -First 1 -ExpandProperty Name)).'Install Directory') + 'Microsoft.Exchange.WebServices.dll') } if (Test-Path $apiPath) { Add-Type -Path $apiPath } else { Write-Error "The Exchange Web Services Managed API is required to use this script." -Category NotInstalled break } $exchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2 $exchangeService = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ExchangeService($exchangeVersion) if ($Credential) { $exchangeService.Credentials = New-Object -TypeName Microsoft.Exchange.WebServices.Data.WebCredentials($Credential) } $firstRun = $false } if ($EWSUrl) { $exchangeService.Url = $EWSUrl } elseif ($UseExchangeOnline) { $exchangeService.Url = 'https://outlook.office365.com/ews/Exchange.asmx' } else { $exchangeService.AutodiscoverUrl($EmailAddress, {$true}) } #Create custom object to hold results $output = "" | Select-Object 'EmailAddress','CalendarNotify','TextConfiguration','InboxRules' $output.EmailAddress = $EmailAddress #Get calendar notification settings try { $calNotify = Get-UserConfigurationMessage -targetAddress $EmailAddress -className 'CalendarNotification.001' -impersonate $UseImpersonation #Convert binary property to XML [xml]$calStream = [System.Text.Encoding]::ASCII.GetString($calNotify.XmlData) #Disable the three notification types $notifyEnabled = $false if ($calStream.CalendarNotificationSettings.UpdateSettings.Enabled -eq 'true') { $calStream.CalendarNotificationSettings.UpdateSettings.Enabled = 'false' $notifyEnabled = $true } if ($calStream.CalendarNotificationSettings.ReminderSettings.Enabled -eq 'true') { $calStream.CalendarNotificationSettings.ReminderSettings.Enabled = 'false' $notifyEnabled = $true } if ($calStream.CalendarNotificationSettings.SummarySettings.Enabled -eq 'true') { $calStream.CalendarNotificationSettings.SummarySettings.Enabled = 'false' $notifyEnabled = $true } if ($notifyEnabled) { #Convert XML back to binary and save $calNotify.xmlData = Convert-StringToByteArray -string (Convert-XmlToString -xml $calStream) $calNotify.Update() $output.CalendarNotify = 'Deleted' } else { $output.CalendarNotify = 'NotConfigured' } } catch { if ($error[0].Exception -like '*The specified object was not found in the store.*') { $output.CalendarNotify = 'NotFound' } else { $output.CalendarNotify = 'Error' } } #Get text messaging settings try { $textConfig = Get-UserConfigurationMessage -targetAddress $EmailAddress -className 'TextMessaging.001' -impersonate $UseImpersonation #Convert binary property to XML [xml]$textStream = [System.Text.Encoding]::ASCII.GetString($textConfig.xmldata) if ($textStream.TextMessagingSettings.MachineToPersonMessagingPolicies.PossibleRecipient) { $xpath = '//MachineToPersonMessagingPolicies' #Node name that contains devices #Remove any defined mobile devices $textStream.SelectSingleNode($xpath).RemoveAll() #Convert XML back to binary and save $textConfig.xmlData = Convert-StringToByteArray -string (Convert-XmlToString -xml $textStream) $textConfig.Update() $output.TextConfiguration = 'Deleted' } else { $output.TextConfiguration = 'NotConfigured' } } catch { if ($error[0].Exception -like '*The specified object was not found in the store.*') { $output.TextConfiguration = 'NotFound' } else { $output.TextConfiguration = 'Error' } } #Check for inbox rules that forward to mobile device try { $inboxRules = Get-Rules -targetAddress $EmailAddress -impersonate $UseImpersonation if ($inboxRules.Count -gt 0) { #Get property for 2013+ version rules that contains rule actions $propExtRuleActionsV2 = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x0E99,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary) #Get property for 2010 version rules that contains rule actions $propExtRuleActionsV1 = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x65EF,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary) $propertySet = New-Object -TypeName Microsoft.Exchange.WebServices.Data.PropertySet(@($propExtRuleActionsV2,$propExtRuleActionsV1)) [void]$exchangeService.LoadPropertiesForItems($inboxRules, $propertySet) $matchingRule = $false foreach ($rule in $inboxRules.Items) { $ruleActionsV2 = $null $ruleActionsV1 = $null if ($rule.TryGetProperty($propExtRuleActionsV2,[ref]$ruleActionsV2)) { #Convert from binary and look for string that indicates forwarding to device if ([System.Text.Encoding]::ASCII.GetString($ruleActionsV2) -like '*MOBILE:*') { $rule.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete) $matchingRule = $true $output.InboxRules = 'Deleted' continue } } elseif ($rule.TryGetProperty($propExtRuleActionsV1,[ref]$ruleActionsV1)) { #Convert from binary and look for string that indicates forwarding to device if ([System.Text.Encoding]::ASCII.GetString($ruleActionsV1) -like '*MOBILE:*') { $rule.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete) $matchingRule = $true $output.InboxRules = 'Deleted' } } } if (-not($matchingRule)) { $output.InboxRules = 'NotConfigured' } } else { $output.InboxRules = 'NotConfigured' } } catch { $output.InboxRules = 'Error' } $output } |
Hey. This script is exactly what we’re looking for but unfortunately it isn’t working for me. It seems to error-out when getting the Inbox Rules. I looked at one of the test mailboxes I ran it against and there’s no rule with ItemClass of ‘IPM.Rule.Version2.Message’ in the FAI. The only item I see in the FAI for this mailbox is ‘IPM.Rule.Message’ and it doesn’t seem to have the same properties as the ‘IPM.Rule.Version2.Message’. Do you have any idea what properties you need to review on ‘IPM.Rule.Message’ items to get the same information? This test mailbox does have an inbox rule that forwards emails to the configured mobile phone. Many thank, Stuart
IPM.Rule.Message is when the mailbox is on Exchange 2010. The script has been updated (v1.3) to accommodate mailboxes on Exchange 2010. It is not necessary to do anything differently for mailboxes on 2010 or 2013+; the script will check for both rule classes.