├── .gitattributes ├── .gitignore ├── LICENSE ├── Legacy ├── Archive │ └── Search-MailboxForMessageClass.ps1 ├── Build │ ├── EWSOAuth.ps1 │ ├── Logging.ps1 │ ├── Test-EWSScripts.ps1 │ └── Update-CommonCode.ps1 ├── Check-NamedProps.ps1 ├── Convert-EWSId.ps1 ├── Create-MailboxBatches.ps1 ├── Delete-ByEntryId.ps1 ├── Delete-ByInternetMessageId.ps1 ├── Fix-DuplicateMailboxFolders.ps1 ├── Get-EmptyFolders.ps1 ├── Get-Office365ServiceStatus.ps1 ├── Import-CalendarCSV.ps1 ├── Merge-MailboxFolder.ps1 ├── Microsoft.Exchange.WebServices.dll ├── RecoverDeletedItems.ps1 ├── Remove-DuplicateItems.ps1 ├── Search-Appointments.ps1 ├── Search-MailboxItems.ps1 ├── Send-Again.ps1 ├── Update-FolderItems.ps1 └── Update-Folders.ps1 └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Legacy/.vscode/launch.json 2 | *.dll 3 | *.lnk 4 | Legacy/Microsoft.Identity.Client.xml 5 | Legacy/Microsoft.IdentityModel.Abstractions.xml 6 | Legacy/Backup 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 David Barrett 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Legacy/Build/Logging.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Logging.ps1 3 | # 4 | # By David Barrett, Microsoft Ltd. 2016-2023. Use at your own risk. No warranties are given. 5 | # 6 | # DISCLAIMER: 7 | # THIS CODE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 8 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 9 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 10 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 11 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 12 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 13 | # THE SOFTWARE. 14 | 15 | 16 | param ( 17 | 18 | #>** LOGGING PARAMETERS START **# 19 | [Parameter(Mandatory=$False,HelpMessage="Log file - activity is logged to this file if specified.")] 20 | [string]$LogFile = "", 21 | 22 | [Parameter(Mandatory=$False,HelpMessage="Enable verbose log file. Verbose logging is written to the log whether -Verbose is enabled or not.")] 23 | [switch]$VerboseLogFile, 24 | 25 | [Parameter(Mandatory=$False,HelpMessage="Enable debug log file. Debug logging is written to the log whether -Debug is enabled or not.")] 26 | [switch]$DebugLogFile, 27 | 28 | [Parameter(Mandatory=$False,HelpMessage="If selected, an optimised log file creator is used that should be signficantly faster (but may leave file lock applied if script is cancelled).")] 29 | [switch]$FastFileLogging, 30 | #>** LOGGING PARAMETERS END **# 31 | 32 | [Parameter(Mandatory=$False,HelpMessage="This is a dummy parameter to avoid syntax error.")] 33 | $Dummy = $null 34 | ) 35 | 36 | 37 | 38 | #>** LOGGING FUNCTIONS START **# 39 | $scriptStartTime = [DateTime]::Now 40 | 41 | Function LogToFile([string]$Details) 42 | { 43 | if ( [String]::IsNullOrEmpty($LogFile) ) { return } 44 | "$([DateTime]::Now.ToShortDateString()) $([DateTime]::Now.ToLongTimeString()) $Details" | Out-File $LogFile -Append 45 | } 46 | 47 | Function UpdateDetailsWithCallingMethod([string]$Details) 48 | { 49 | # Update the log message with details of the function that logged it 50 | $timeInfo = "$([DateTime]::Now.ToShortDateString()) $([DateTime]::Now.ToLongTimeString())" 51 | $callingFunction = (Get-PSCallStack)[2].Command # The function we are interested in will always be frame 2 on the stack 52 | if (![String]::IsNullOrEmpty($callingFunction)) 53 | { 54 | return "$timeInfo [$callingFunction] $Details" 55 | } 56 | return "$timeInfo $Details" 57 | } 58 | 59 | Function LogToFile([string]$logInfo) 60 | { 61 | if ( [String]::IsNullOrEmpty($LogFile) ) { return } 62 | 63 | if ($FastFileLogging) 64 | { 65 | # Writing the log file using a FileStream (that we keep open) is significantly faster than using out-file (which opens, writes, then closes the file each time it is called) 66 | $fastFileLogError = $Error[0] 67 | if (!$script:logFileStream) 68 | { 69 | # Open a filestream to write to our log 70 | Write-Verbose "Opening/creating log file: $LogFile" 71 | $script:logFileStream = New-Object IO.FileStream($LogFile, ([System.IO.FileMode]::Append), ([IO.FileAccess]::Write), ([IO.FileShare]::Read) ) 72 | if ( $Error[0] -ne $fastFileLogError ) 73 | { 74 | $FastFileLogging = $false 75 | Write-Host "Fast file logging disabled due to error: $Error[0]" -ForegroundColor Red 76 | $script:logFileStream = $null 77 | } 78 | } 79 | if ($script:logFileStream) 80 | { 81 | if (!$script:logFileStreamWriter) 82 | { 83 | $script:logFileStreamWriter = New-Object System.IO.StreamWriter($script:logFileStream) 84 | } 85 | $script:logFileStreamWriter.WriteLine($logInfo) 86 | $script:logFileStreamWriter.Flush() 87 | if ( $Error[0] -ne $fastFileLogError ) 88 | { 89 | $FastFileLogging = $false 90 | Write-Host "Fast file logging disabled due to error: $Error[0]" -ForegroundColor Red 91 | } 92 | else 93 | { 94 | return 95 | } 96 | } 97 | } 98 | 99 | $logInfo | Out-File $LogFile -Append 100 | } 101 | 102 | Function Log([string]$Details, [ConsoleColor]$Colour) 103 | { 104 | if ($Colour -eq $null) 105 | { 106 | $Colour = [ConsoleColor]::White 107 | } 108 | $Details = UpdateDetailsWithCallingMethod( $Details ) 109 | Write-Host $Details -ForegroundColor $Colour 110 | LogToFile $Details 111 | } 112 | Log "$($MyInvocation.MyCommand.Name) version $($script:ScriptVersion) starting" Green 113 | 114 | Function LogVerbose([string]$Details) 115 | { 116 | $Details = UpdateDetailsWithCallingMethod( $Details ) 117 | Write-Verbose $Details 118 | if ( !$VerboseLogFile -and !$DebugLogFile -and ($VerbosePreference -eq "SilentlyContinue") ) { return } 119 | LogToFile $Details 120 | } 121 | 122 | Function LogDebug([string]$Details) 123 | { 124 | $Details = UpdateDetailsWithCallingMethod( $Details ) 125 | Write-Debug $Details 126 | if (!$DebugLogFile -and ($DebugPreference -eq "SilentlyContinue") ) { return } 127 | LogToFile $Details 128 | } 129 | 130 | $script:LastError = $Error[0] 131 | Function ErrorReported($Context) 132 | { 133 | # Check for any error, and return the result ($true means a new error has been detected) 134 | 135 | # We check for errors using $Error variable, as try...catch isn't reliable when remoting 136 | if ([String]::IsNullOrEmpty($Error[0])) { return $false } 137 | 138 | # We have an error, have we already reported it? 139 | if ($Error[0] -eq $script:LastError) { return $false } 140 | 141 | # New error, so log it and return $true 142 | $script:LastError = $Error[0] 143 | if ($Context) 144 | { 145 | Log "ERROR ($Context): $($Error[0])" Red 146 | } 147 | else 148 | { 149 | $log = UpdateDetailsWithCallingMethod("ERROR: $($Error[0])") 150 | Log $log Red 151 | } 152 | return $true 153 | } 154 | 155 | Function ReportError($Context) 156 | { 157 | # Reports error without returning the result 158 | ErrorReported $Context | Out-Null 159 | } 160 | #>** LOGGING FUNCTIONS END **# -------------------------------------------------------------------------------- /Legacy/Build/Test-EWSScripts.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Test-EWSScripts.ps1 3 | # 4 | # By David Barrett, Microsoft Ltd. 2023. Use at your own risk. No warranties are given. 5 | # 6 | # DISCLAIMER: 7 | # THIS CODE IS SAMPLE CODE. THESE SAMPLES ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. 8 | # MICROSOFT FURTHER DISCLAIMS ALL IMPLIED WARRANTIES INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR OF FITNESS FOR 9 | # A PARTICULAR PURPOSE. THE ENTIRE RISK ARISING OUT OF THE USE OR PERFORMANCE OF THE SAMPLES REMAINS WITH YOU. IN NO EVENT SHALL 10 | # MICROSOFT OR ITS SUPPLIERS BE LIABLE FOR ANY DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, 11 | # BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE 12 | # SAMPLES, EVEN IF MICROSOFT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. BECAUSE SOME STATES DO NOT ALLOW THE EXCLUSION OR LIMITATION 13 | # OF LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE LIMITATION MAY NOT APPLY TO YOU. 14 | 15 | 16 | param ( 17 | [Parameter(Mandatory=$False,HelpMessage="The folder where the scripts are located.")] 18 | [string]$ScriptFolder = "C:\Tools\PowerShell-EWS-Scripts\PowerShell-EWS-Scripts\Legacy", 19 | 20 | [Parameter(Mandatory=$False,HelpMessage="The folder in which logs will be created.")] 21 | [string]$LogFolder = "c:\Temp\Logs", 22 | 23 | [Parameter(Mandatory=$False,HelpMessage="The folder in which traces will be created.")] 24 | [string]$TraceFolder = "c:\Temp\Traces", 25 | 26 | [Parameter(Mandatory=$False,HelpMessage="If set, the OAuth variables will be assigned to global variables to be available outside the script.")] 27 | [switch]$SetGlobalOAuth 28 | ) 29 | 30 | $TestRecoverDeletedItems = $false 31 | $TestUpdateFolderItems = $false 32 | $TestSearchMailboxItems = $false 33 | $TestMergeMailboxFolders = $false 34 | $TestRemoveDuplicateItems = $true 35 | 36 | # Tenant 37 | $tenantId = "fc69f6a8-90cd-4047-977d-0c768925b8ec" 38 | 39 | # App permissions client info 40 | $clientIdAppPermissions = "f61d7821-7aaf-4d24-b34f-ca50528bcc7b" # App Id for app granted full_access_as_app 41 | # Secret key and/or certificate needed (depending which tests enabled). Set prior to calling script. $secretKey for secret key, and $certificate for certificate 42 | # e.g. $certificate = Get-Item Cert:\CurrentUser\My\AD76407DCDC4E966A3F39F44954E2E9701D6083B 43 | # or $secretKey = "xxx" 44 | 45 | # Create self-signed certificate: 46 | # $certname = "Test-EWSScripts.ps1" 47 | # $cert = New-SelfSignedCertificate -Subject "CN=$certname" -CertStoreLocation "Cert:\CurrentUser\My" -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256 48 | # Export-Certificate -Cert $cert -FilePath "C:\Certificates\$certname.cer" 49 | 50 | # Delegated permissions client info 51 | $clientIdDelegatePermissions = "42eb458d-96d4-4a5b-9d0c-2467e1cf2e59" # App Id for app granted EWS.AccessAsUser.All 52 | 53 | # Mailboxes 54 | $Mailbox = "dave@demonmaths.co.uk" # Primary mailbox 55 | $DelegatedMailbox = "1@demonmaths.co.uk" # Mailbox to which Primary mailbox has FullAccess permission 56 | $InaccessibleMailbox = "100@demonmaths.co.uk" # Mailbox to which Primary mailbox has no permissions 57 | 58 | if ($SetGlobalOAuth) 59 | { 60 | $global:tenantId = $tenantId 61 | $global:clientIdDelegatePermissions = $clientIdDelegatePermissions 62 | $global:clientIdAppPermissions = $clientIdAppPermissions 63 | $global:Mailbox = $Mailbox 64 | $global:DelegatedMailbox = $DelegatedMailbox 65 | $global:InaccessibleMailbox = $InaccessibleMailbox 66 | } 67 | 68 | # Store current path and then change path to script folder 69 | $currentPath = (Get-Location).Path 70 | cd $ScriptFolder 71 | 72 | $runAppPermissionTests = $true 73 | $runDelegatePermissionTests = ![String]::IsNullOrEmpty($clientIdDelegatePermissions) 74 | $skipOAuthDebug = $true 75 | 76 | # Use Azure Key Vault to store sensitive info (secret key) - not yet implemented 77 | function CreateAzureKeyVault() 78 | { 79 | Connect-AzAccount -TenantId $tenantId 80 | New-AzResourceGroup -Name "ScriptData" -Location 81 | New-AzKeyVault -Name "Test-EWSScripts" -ResourceGroupName "ScriptData" 82 | } 83 | 84 | # Check that we have valid parameters for app permission 85 | function AppPermissionsCheck() 86 | { 87 | if ([String]::IsNullOrEmpty($secretKey) -and $certificate -eq $null) 88 | { 89 | Write-Host "Application permission tests will not be run as neither `$secretKey nor `$certificate are set" -ForegroundColor Yellow 90 | $runAppPermissionTests = $false 91 | } 92 | } 93 | 94 | 95 | 96 | # Test RecoverDeletedItems with delegated permissions to delegated archive mailbox 97 | function TestRecoverDeletedItems1() 98 | { 99 | $global:testDescriptions.Add("TestRecoverDeletedItems1", "RecoverDeletedItems.ps1: attempt to access delegated archive mailbox using delegated permissions and show which items would be restored from ArchiveRecoverableItemsDeletions.") 100 | if (!$runDelegatePermissionTests) { return "Skipped as delegate configuration incomplete/disabled" } 101 | 102 | $Error.Clear() 103 | trap {} 104 | .\RecoverDeletedItems.ps1 -Mailbox $DelegatedMailbox -Archive -RestoreFromFolder "WellKnownFolderName.ArchiveRecoverableItemsDeletions" -Office365 -OAuthTenantId $tenantId -OAuthClientId $clientIdDelegatePermissions -GlobalTokenStorage -WhatIf 105 | if ($Error.Count -gt 0) 106 | { 107 | return "Failed, error when accessing $DelegatedMailbox as $Mailbox" 108 | } 109 | return "Succeeded, $DelegatedMailbox accessible to $Mailbox" 110 | } 111 | 112 | 113 | # Test RecoverDeletedItems with delegated permissions to inaccessible mailbox 114 | function TestRecoverDeletedItems2() 115 | { 116 | $global:testDescriptions.Add("TestRecoverDeletedItems2", "RecoverDeletedItems.ps1: attempt to access other (inaccessible) archive mailbox using delegated permissions and show which items would be restored from ArchiveRecoverableItemsDeletions.") 117 | if (!$runDelegatePermissionTests) { return "Skipped as delegate configuration incomplete/disabled" } 118 | 119 | $Error.Clear() 120 | trap {} 121 | .\RecoverDeletedItems.ps1 -Mailbox $InaccessibleMailbox -Archive -RestoreFromFolder "WellKnownFolderName.ArchiveRecoverableItemsDeletions" -Office365 -OAuthTenantId $tenantId -OAuthClientId $clientIdDelegatePermissions -GlobalTokenStorage -WhatIf 122 | if ($Error.Count -eq 1 -and $Error[0].Exception.Message.Contains("The specified folder could not be found in the store.")) 123 | { 124 | return "Succeeded, $InaccessibleMailbox not accessible when accessing as $Mailbox" 125 | } 126 | else 127 | { 128 | if ($Error.Count -eq 0) 129 | { 130 | return "Failed, $InaccessibleMailbox was accessible (expected to be inaccessible to $Mailbox)" 131 | } 132 | else 133 | { 134 | return "Failed, unexpected error when accessing $InaccessibleMailbox" 135 | } 136 | } 137 | } 138 | 139 | 140 | # Test OAuth token renewal with delegated permissions to delegated mailbox 141 | function TestRecoverDeletedItems3() 142 | { 143 | $global:testDescriptions.Add("TestRecoverDeletedItems3", "RecoverDeletedItems.ps1: access delegated archive mailbox using delegated permissions and show which items would be restored from ArchiveRecoverableItemsDeletions.") 144 | if ($skipOAuthDebug) { return "Skipped as OAuth debugging disabled" } 145 | if (!$runDelegatePermissionTests) { return "Skipped as delegate configuration incomplete/disabled" } 146 | 147 | $Error.Clear() 148 | trap {} 149 | .\RecoverDeletedItems.ps1 -Mailbox $DelegatedMailbox -Archive -RestoreFromFolder "WellKnownFolderName.ArchiveRecoverableItemsDeletions" -Office365 -OAuth -OAuthTenantId $tenantId -OAuthClientId $clientIdDelegatePermissions -GlobalTokenStorage -WhatIf -OAuthDebug -DebugTokenRenewal 1 150 | if ($Error.Count -gt 0) 151 | { 152 | return "Failed, error when accessing $DelegatedMailbox as $Mailbox" 153 | } 154 | return "Succeeded, $DelegatedMailbox accessible (token renewal succeeded) when accessing as $Mailbox" 155 | } 156 | 157 | 158 | # Test UpdateFolderItems with delegated permissions to primary mailbox 159 | function TestUpdateFolderItems1() 160 | { 161 | $global:testDescriptions.Add("TestUpdateFolderItems1", "Update-FolderItems.ps1: access primary mailbox using delegated permissions and set isRead for first 5 items in Inbox to true.") 162 | if (!$runDelegatePermissionTests) { return "Skipped as delegate configuration incomplete/disabled" } 163 | 164 | $Error.Clear() 165 | trap {} 166 | .\Update-FolderItems.ps1 -Mailbox $Mailbox -FolderPath "WellKnownFolderName.Inbox" -MarkAsRead -Office365 -OAuth -OAuthTenantId $tenantId -OAuthClientId $clientIdDelegatePermissions -GlobalTokenStorage -MaximumNumberOfItemsToProcess 5 -WhatIf 167 | if ($Error.Count -gt 0) 168 | { 169 | return "Failed, error when accessing $Mailbox" 170 | } 171 | return "Succeeded, $Mailbox accessible and no errors reported" 172 | } 173 | 174 | 175 | # Test UpdateFolderItems with application permissions to primary mailbox 176 | function TestUpdateFolderItems2() 177 | { 178 | $global:testDescriptions.Add("TestUpdateFolderItems2", "Update-FolderItems.ps1: access primary mailbox using application permissions and set isRead for first 5 items in Inbox to true.") 179 | if (!$runAppPermissionTests) { return "Skipped as app configuration incomplete" } 180 | 181 | $Error.Clear() 182 | trap {} 183 | .\Update-FolderItems.ps1 -Mailbox $Mailbox -FolderPath "WellKnownFolderName.Inbox" -MarkAsRead -Office365 -OAuth -OAuthTenantId $tenantId -OAuthClientId $clientIdAppPermissions -OAuthSecretKey $secretKey -MaximumNumberOfItemsToProcess 5 -StopAfterMaximumNumberOfItemsProcessed 184 | if ($Error.Count -gt 0) 185 | { 186 | return "Failed, error when accessing $Mailbox" 187 | } 188 | return "Succeeded, $Mailbox accessible" 189 | } 190 | 191 | 192 | # Test UpdateFolderItems with application permissions to primary mailbox forcing testing of token renewal 193 | function TestUpdateFolderItems3() 194 | { 195 | $global:testDescriptions.Add("TestUpdateFolderItems3", "Update-FolderItems.ps1: debug OAuth token renewal while accessing primary mailbox using application permissions and set isRead for first 5 items in Inbox to true.") 196 | if ($skipOAuthDebug) { return "Skipped as OAuth debugging disabled" } 197 | if (!$runAppPermissionTests) { return "Skipped as app configuration incomplete" } 198 | 199 | $Error.Clear() 200 | trap {} 201 | .\Update-FolderItems.ps1 -Mailbox $Mailbox -FolderPath "WellKnownFolderName.Inbox" -MarkAsRead -Office365 -OAuth -OAuthTenantId $tenantId -OAuthClientId $clientIdAppPermissions -OAuthSecretKey $secretKey -MaximumNumberOfItemsToProcess 5 -StopAfterMaximumNumberOfItemsProcessed -OAuthDebug -DebugTokenRenewal 1 202 | if ($Error.Count -gt 0) 203 | { 204 | return "Failed, error when accessing $Mailbox" 205 | } 206 | return "Succeeded, $Mailbox accessible" 207 | } 208 | 209 | 210 | # Test Search-MailboxItems with application permissions to primary mailbox (search for all IPM.Note items) 211 | function TestSearchMailboxItems1() 212 | { 213 | $global:testDescriptions.Add("TestSearchMailboxItems1", "TestSearchMailboxItems.ps1: access primary mailbox using application permissions and search for all IPM.Note items.") 214 | if (!$runAppPermissionTests) { return "Skipped as app configuration incomplete" } 215 | 216 | $Error.Clear() 217 | trap {} 218 | $matches = .\Search-MailboxItems.ps1 -Mailbox $Mailbox -MessageClass "IPM.Note" -Office365 -OAuth -OAuthTenantId $tenantId -OAuthClientId $clientIdAppPermissions -OAuthSecretKey $secretKey 219 | if ($Error.Count -gt 0) 220 | { 221 | return "Failed, error when accessing $Mailbox" 222 | } 223 | if ($matches.Count -eq 0) 224 | { 225 | return "Failed, access to $Mailbox succeeded, but no IPM.Note items found" 226 | } 227 | return "Succeeded, $Mailbox accessible and $($matches.Count) IPM.Note item(s) found" 228 | } 229 | 230 | # Test Search-MailboxItems with delegated permissions to primary mailbox (search for all IPM.Note items from $Mailbox) 231 | function TestSearchMailboxItems2() 232 | { 233 | $global:testDescriptions.Add("TestSearchMailboxItems2", "TestSearchMailboxItems.ps1: access $DelegatedMailbox mailbox using delegate flow and search for all IPM.Note items received from $Mailbox.") 234 | if (!$runDelegatePermissionTests) { return "Skipped as delegate configuration incomplete/disabled" } 235 | 236 | $Error.Clear() 237 | trap {} 238 | $matches = .\Search-MailboxItems.ps1 -Mailbox $DelegatedMailbox -MessageClass "IPM.Note" -Sender $Mailbox -Office365 -OAuth -OAuthTenantId $tenantId -OAuthClientId $clientIdDelegatePermissions 239 | if ($Error.Count -gt 0) 240 | { 241 | return "Failed, error when accessing $Mailbox" 242 | } 243 | if ($matches.Count -eq 0) 244 | { 245 | return "Failed, access to $DelegatedMailbox succeeded, but no IPM.Note items found from $Mailbox" 246 | } 247 | return "Succeeded, $DelegatedMailbox accessible and $($matches.Count) IPM.Note item(s) found from $Mailbox" 248 | } 249 | 250 | # Test Search-MailboxItems with application permissions to primary mailbox (search for all IPM.Note items sent to $Mailbox) 251 | function TestSearchMailboxItems3() 252 | { 253 | $global:testDescriptions.Add("TestSearchMailboxItems3", "TestSearchMailboxItems.ps1: access primary mailbox using application permissions and search for all IPM.Note items sent to $Mailbox.") 254 | if (!$runAppPermissionTests) { return "Skipped as app configuration incomplete" } 255 | 256 | $Error.Clear() 257 | trap {} 258 | $matches = .\Search-MailboxItems.ps1 -Mailbox $Mailbox -MessageClass "IPM.Note" -Office365 -OAuth -OAuthTenantId $tenantId -OAuthClientId $clientIdAppPermissions -OAuthSecretKey $secretKey 259 | if ($Error.Count -gt 0) 260 | { 261 | return "Failed, error when accessing $Mailbox" 262 | } 263 | if ($matches.Count -eq 0) 264 | { 265 | return "Failed, access to $Mailbox succeeded, but no IPM.Note items found where $Mailbox is a recipient" 266 | } 267 | return "Succeeded, $Mailbox accessible and $($matches.Count) IPM.Note item(s) found where $Mailbox is a recipient" 268 | } 269 | 270 | # Test MergeMailboxFolder with application permissions to primary mailbox 271 | function TestMergeMailboxFolder1() 272 | { 273 | $global:testDescriptions.Add("TestMergeMailboxFolder1", "Merge-MailboxItems.ps1: access primary mailbox using application permissions and show what would be copied from Inbox to InboxCopy folder.") 274 | if (!$runAppPermissionTests) { return "Skipped as app configuration incomplete" } 275 | 276 | $Error.Clear() 277 | trap {} 278 | $mmresult = .\Merge-MailboxFolder.ps1 -SourceMailbox $Mailbox -MergeFolderList @{"InboxCopy" = "WellKnownFolderName.Inbox"} -WhatIf -ReturnTotalItemsAffected -Office365 -OAuth -OAuthTenantId $tenantId -OAuthClientId $clientIdAppPermissions -OAuthSecretKey $secretKey 279 | if ($Error.Count -gt 0) 280 | { 281 | return "Failed, error when accessing $Mailbox" 282 | } 283 | if ($mmresult -gt 0) 284 | { 285 | return "Succeeded, $Mailbox accessible and $mmresult items found to copy" 286 | } 287 | return "Check mailbox contents - no error reported, but no items found to copy (is Inbox empty?)" 288 | } 289 | 290 | # Test throttling for MergeMailboxFolder with application permissions to primary mailbox 291 | function TestMergeMailboxFolder2() 292 | { 293 | $global:testDescriptions.Add("TestMergeMailboxFolder2", "Merge-MailboxItems.ps1: test throttling, accessing primary mailbox using application permissions and show what would be copied from Inbox to InboxCopy folder.") 294 | if (!$runAppPermissionTests) { return "Skipped as app configuration incomplete" } 295 | 296 | $Error.Clear() 297 | trap {} 298 | 299 | # To test throttling, we need to generate a large number of requests - so we keep rerunning the script 300 | $mmresult = .\Merge-MailboxFolder.ps1 -SourceMailbox $Mailbox -MergeFolderList @{"InboxCopy" = "WellKnownFolderName.Inbox"} -WhatIf -ReturnTotalItemsAffected -Office365 -OAuth -OAuthTenantId $tenantId -OAuthClientId $clientIdAppPermissions -OAuthSecretKey $secretKey 301 | if ($Error.Count -gt 0) 302 | { 303 | return "Failed, error when accessing $Mailbox" 304 | } 305 | if ($mmresult -gt 0) 306 | { 307 | return "Succeeded, $Mailbox accessible and $mmresult items found to copy" 308 | } 309 | return "Check mailbox contents - no error reported, but no items found to copy (is Inbox empty?)" 310 | } 311 | 312 | # Test MergeMailboxFolder with application permissions with certificate auth to primary mailbox 313 | function TestMergeMailboxFolder3() 314 | { 315 | $global:testDescriptions.Add("TestMergeMailboxFolder3", "Merge-MailboxItems.ps1: access primary mailbox using application permissions with certificate auth and show what would be copied from Inbox to InboxCopy folder.") 316 | if (!$runAppPermissionTests) { return "Skipped as app configuration incomplete" } 317 | 318 | $Error.Clear() 319 | trap {} 320 | $mmresult = .\Merge-MailboxFolder.ps1 -SourceMailbox $Mailbox -MergeFolderList @{"InboxCopy" = "WellKnownFolderName.Inbox"} -WhatIf -ReturnTotalItemsAffected -Office365 -OAuth -OAuthTenantId $tenantId -OAuthClientId $clientIdAppPermissions -OAuthCertificate $certificate -OAuthDebug 321 | if ($Error.Count -gt 0) 322 | { 323 | return "Failed, error when accessing $Mailbox" 324 | } 325 | if ($mmresult -gt 0) 326 | { 327 | return "Succeeded, $Mailbox accessible and $mmresult items found to copy" 328 | } 329 | return "Check mailbox contents - no error reported, but no items found to copy (is Inbox empty?)" 330 | } 331 | 332 | # Test RemoveDuplicateItems with application permissions to primary mailbox 333 | function TestRemoveDuplicateItems1() 334 | { 335 | $global:testDescriptions.Add("TestRemoveDuplicateItems1", "Remove-DuplicateItems.ps1: access primary mailbox using application permissions and list all duplicate items within entire mailbox.") 336 | if (!$runAppPermissionTests) { return "Skipped as app configuration incomplete" } 337 | 338 | $Error.Clear() 339 | trap {} 340 | if (![String]::IsNullOrEmpty(($secretKey))) 341 | { 342 | $duplicateItems = .\Remove-DuplicateItems.ps1 -Mailbox $Mailbox -RecurseFolders -MatchEntireMailbox -ReturnDuplicateCount -WhatIf -Office365 -OAuth -OAuthTenantId $tenantId -OAuthClientId $clientIdAppPermissions -OAuthSecretKey $secretKey 343 | } 344 | elseif ($certificate -ne $null) 345 | { 346 | $duplicateItems = .\Remove-DuplicateItems.ps1 -Mailbox $Mailbox -RecurseFolders -MatchEntireMailbox -ReturnDuplicateCount -WhatIf -Office365 -OAuth -OAuthTenantId $tenantId -OAuthClientId $clientIdAppPermissions -OAuthCertificate $certificate 347 | } 348 | else 349 | { 350 | return "No valid app auth information provided (need secret key or certificate)" 351 | } 352 | 353 | if ($Error.Count -gt 0) 354 | { 355 | return "Failed, error while processing $Mailbox" 356 | } 357 | if ($duplicateItems -gt 0) 358 | { 359 | return "Succeeded, $Mailbox accessible and $duplicateItems duplicates found" 360 | } 361 | return "Check mailbox contents - no error reported, but no duplicates found" 362 | } 363 | 364 | # Test RemoveDuplicateItems with application permissions to primary mailbox 365 | function TestRemoveDuplicateItems2() 366 | { 367 | $global:testDescriptions.Add("TestRemoveDuplicateItems2", "Remove-DuplicateItems.ps1: access primary mailbox using application permissions and list all duplicate items within entire mailbox, only matching duplicate items with a creation date of today") 368 | if (!$runAppPermissionTests) { return "Skipped as app configuration incomplete" } 369 | 370 | $Error.Clear() 371 | trap {} 372 | if (![String]::IsNullOrEmpty(($secretKey))) 373 | { 374 | $duplicateItems = .\Remove-DuplicateItems.ps1 -Mailbox $Mailbox -RecurseFolders -MatchEntireMailbox -ReturnDuplicateCount -WhatIf -Office365 -OAuth -OAuthTenantId $tenantId -OAuthClientId $clientIdAppPermissions -OAuthSecretKey $secretKey 375 | } 376 | elseif ($certificate -ne $null) 377 | { 378 | $duplicateItems = .\Remove-DuplicateItems.ps1 -Mailbox $Mailbox -RecurseFolders -MatchEntireMailbox -ReturnDuplicateCount -WhatIf -Office365 -OAuth -OAuthTenantId $tenantId -OAuthClientId $clientIdAppPermissions -OAuthCertificate $certificate 379 | } 380 | else 381 | { 382 | return "No valid app auth information provided (need secret key or certificate)" 383 | } 384 | 385 | if ($Error.Count -gt 0) 386 | { 387 | return "Failed, error while processing $Mailbox" 388 | } 389 | if ($duplicateItems -gt 0) 390 | { 391 | return "Succeeded, $Mailbox accessible and $duplicateItems duplicates found" 392 | } 393 | return "Check mailbox contents - no error reported, but no duplicates found" 394 | } 395 | 396 | # Run tests and collate results 397 | AppPermissionsCheck 398 | $global:testDescriptions = @{} 399 | $results = @{} 400 | 401 | if ($TestRecoverDeletedItems) 402 | { 403 | $results.Add("TestRecoverDeletedItems1", "$(TestRecoverDeletedItems1)") 404 | $results.Add("TestRecoverDeletedItems2", "$(TestRecoverDeletedItems2)") 405 | $results.Add("TestRecoverDeletedItems3", "$(TestRecoverDeletedItems3)") 406 | } 407 | if ($TestUpdateFolderItems) 408 | { 409 | $results.Add("TestUpdateFolderItems1", "$(TestUpdateFolderItems1)") 410 | $results.Add("TestUpdateFolderItems2", "$(TestUpdateFolderItems2)") 411 | $results.Add("TestUpdateFolderItems3", "$(TestUpdateFolderItems3)") 412 | } 413 | if ($TestSearchMailboxItems) 414 | { 415 | $results.Add("TestSearchMailboxItems1", "$(TestSearchMailboxItems1)") 416 | $results.Add("TestSearchMailboxItems2", "$(TestSearchMailboxItems2)") 417 | $results.Add("TestSearchMailboxItems3", "$(TestSearchMailboxItems3)") 418 | } 419 | if ($TestMergeMailboxFolders) 420 | { 421 | #$results.Add("TestMergeMailboxFolder1", "$(TestMergeMailboxFolder1)") 422 | $results.Add("TestMergeMailboxFolder3", "$(TestMergeMailboxFolder3)") 423 | } 424 | if ($TestRemoveDuplicateItems) 425 | { 426 | $results.Add("TestRemoveDuplicateItems1", "$(TestRemoveDuplicateItems1)") 427 | } 428 | $global:testResults = $results 429 | 430 | 431 | # Output results 432 | Write-Host 433 | Write-Host "Test Results (available in `$testResults)" 434 | Write-Host 435 | 436 | foreach ($testName in $results.Keys) 437 | { 438 | Write-Host "$($testName): " -NoNewline 439 | if ($results[$testName].StartsWith("Succeeded")) 440 | { 441 | Write-Host "$($results[$testName])" -ForegroundColor Green 442 | } 443 | elseif ($results[$testName].StartsWith("Failed")) 444 | { 445 | Write-Host "$($results[$testName])" -ForegroundColor Red 446 | } 447 | else 448 | { 449 | Write-Host "$($results[$testName])" -ForegroundColor Yellow 450 | } 451 | } 452 | 453 | Write-Host 454 | Write-Host "Test descriptions available in `$testDescriptions" 455 | 456 | 457 | # Restore original path 458 | cd $currentPath -------------------------------------------------------------------------------- /Legacy/Build/Update-CommonCode.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Update-CommonCode.ps1 3 | # 4 | # By David Barrett, Microsoft Ltd. 2023. Use at your own risk. No warranties are given. 5 | # 6 | # DISCLAIMER: 7 | # THIS CODE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 8 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 9 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 10 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 11 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 12 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 13 | # THE SOFTWARE. 14 | 15 | 16 | # $ScriptFolder = "C:\Tools\PowerShell-EWS-Scripts\PowerShell-EWS-Scripts\Legacy" 17 | # $SharedFolder = "C:\Tools\PowerShell-EWS-Scripts\PowerShell-EWS-Scripts\Legacy\Build" 18 | # $BackupFolder = "C:\Tools\PowerShell-EWS-Scripts\PowerShell-EWS-Scripts\Legacy\Backup" 19 | # .\Update-CommonCode.ps1 -ScriptFolder $ScriptFolder -SharedCode @("EWSOAuth.ps1", "Logging.ps1") -BackupFolder $BackupFolder -SharedCodeFolder $SharedFolder 20 | # .\Update-CommonCode.ps1 -ScriptFolder "C:\Tools\PowerShell-EWS-Scripts\PowerShell-EWS-Scripts\Legacy" -SharedCode @("EWSOAuth.ps1", "Logging.ps1") -BackupFolder "C:\Tools\PowerShell-EWS-Scripts\PowerShell-EWS-Scripts\Legacy\Backup" -SharedCodeFolder "C:\Tools\PowerShell-EWS-Scripts\PowerShell-EWS-Scripts\Legacy\Build" 21 | # .\Update-CommonCode.ps1 -ScriptFolder "C:\Tools\PowerShell-EWS-Scripts\PowerShell-EWS-Scripts\Legacy" -SharedCode @("EWSOAuth.ps1", "Logging.ps1") -BackupFolder "C:\Tools\PowerShell-EWS-Scripts\PowerShell-EWS-Scripts\Legacy\Backup" -SharedCodeFolder "C:\Tools\PowerShell-EWS-Scripts\PowerShell-EWS-Scripts\Legacy\Build" -StripSharedCode 22 | 23 | 24 | param ( 25 | 26 | [Parameter(Mandatory=$True,HelpMessage="Source folder containing scripts to be processed.")] 27 | $ScriptFolder, 28 | 29 | [Parameter(Mandatory=$True,HelpMessage="Shared code file(s) to be injected.")] 30 | $SharedCode, 31 | 32 | [Parameter(Mandatory=$False,HelpMessage="Folder where shared code files are located.")] 33 | $SharedCodeFolder, 34 | 35 | [Parameter(Mandatory=$False,HelpMessage="Folder where files will be backed up prior to update (if not specified, .ccbak file will be created)")] 36 | $BackupFolder, 37 | 38 | [Parameter(Mandatory=$False,HelpMessage="If specified, removes the shared code modules from the target PowerShell scripts.")] 39 | [switch]$StripSharedContent 40 | ) 41 | 42 | 43 | $psSourceFiles = Get-ChildItem -Path $ScriptFolder -Include *.ps1 -Name 44 | 45 | function ReplaceSharedCode() 46 | { 47 | param ( 48 | $sourceCode, 49 | [String]$SharedCodeTemplate 50 | ) 51 | 52 | $sharedCode = Get-Content $SharedCodeTemplate 53 | $sharedCodeIndex = 0 54 | $sourceCodeIndex = 0 55 | $updatedCode = @() 56 | $moduleApplied = $false 57 | 58 | Write-Verbose "Checking $($sourceCode.Length) lines of code" 59 | Write-Verbose "Using template: $SharedCodeTemplate" 60 | 61 | while ($sharedCodeIndex -lt $sharedCode.Length -and $sourceCodeIndex -lt $sourceCode.Length) 62 | { 63 | while ($sharedCodeIndex -lt $sharedCode.Length -and -not $sharedCode[$sharedCodeIndex].StartsWith("#>**")) 64 | { $sharedCodeIndex++ } 65 | 66 | if ($sharedCode[$sharedCodeIndex].StartsWith("#>**")) 67 | { 68 | if ($sharedCode[$sharedCodeIndex].EndsWith("START **#")) 69 | { 70 | while ($sourceCodeIndex -lt $sourceCode.Length) 71 | { 72 | if ($sourceCode[$sourceCodeIndex].Equals($sharedCode[$sharedCodeIndex])) 73 | { 74 | # Start of share code injection 75 | Write-Verbose $sharedCode[$sharedCodeIndex] 76 | $script:codeUpdated = $true 77 | $moduleApplied = $true 78 | do 79 | { 80 | if (!$StripSharedContent) { $updatedCode += $sharedCode[$sharedCodeIndex++] } 81 | } while ($sharedCodeIndex -lt $sharedCode.Length -and -not $sharedCode[$sharedCodeIndex].StartsWith("#>**")) 82 | 83 | do 84 | { 85 | $sourceCodeIndex++ 86 | } while ($sourceCodeIndex -lt $sourceCode.Length -and -not $sourceCode[$sourceCodeIndex].Equals($sharedCode[$sharedCodeIndex])) 87 | Write-Verbose $sharedCode[$sharedCodeIndex] 88 | $sharedCodeIndex++ 89 | break 90 | } 91 | else 92 | { 93 | $updatedCode += $sourceCode[$SourceCodeIndex] 94 | } 95 | $sourceCodeIndex++ 96 | } 97 | } 98 | } 99 | } 100 | 101 | if (!$moduleApplied) 102 | { 103 | return $sourceCode 104 | } 105 | 106 | Write-Host "Applied code from template: $SharedCodeTemplate" 107 | while ($sourceCodeIndex -lt $sourceCode.Length) 108 | { 109 | $updatedCode += $sourceCode[$SourceCodeIndex++] 110 | } 111 | return $updatedCode 112 | } 113 | 114 | 115 | foreach ($psSourceFile in $psSourceFiles) 116 | { 117 | Write-Host "Processing $psSourceFile" 118 | $psCode = Get-Content "$ScriptFolder\$psSourceFile" 119 | $updatedCode = $psCode 120 | 121 | $script:codeUpdated = $false 122 | $SharedCode | foreach { 123 | $sharedCodeModule = $_ 124 | if (-not (Test-Path $sharedCodeModule)) 125 | { 126 | $sharedCodeModule = "$SharedCodeFolder\$sharedCodeModule" 127 | } 128 | if (Test-Path $sharedCodeModule) 129 | { 130 | $updatedCode = ReplaceSharedCode $updatedCode $sharedCodeModule 131 | } 132 | } 133 | 134 | $backupFileName = "$ScriptFolder\$psSourceFile.ccbak" 135 | if (![String]::IsNullOrEmpty($BackupFolder)) 136 | { 137 | $backupFileName = "$BackupFolder\$psSourceFile" 138 | } 139 | 140 | if ($script:codeUpdated) 141 | { 142 | if (Test-Path $backupFileName) 143 | { 144 | Remove-Item $backupFileName 145 | } 146 | Copy-Item -Path "$ScriptFolder\$psSourceFile" -Destination $backupFileName 147 | if (Test-Path $backupFileName) 148 | { 149 | Write-Host "Original backed up to: $backupFileName" 150 | $updatedCode | Out-File "$ScriptFolder\$psSourceFile" -Encoding utf8 151 | Write-Host "Updated $psSourceFile" -ForegroundColor Green 152 | } 153 | else 154 | { 155 | Write-Host "Failed to backup $psSourceFile - changes not written" -ForegroundColor Red 156 | } 157 | } 158 | else 159 | { 160 | Write-Host "No change to $psSourceFile" -ForegroundColor Yellow 161 | } 162 | } -------------------------------------------------------------------------------- /Legacy/Check-NamedProps.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Check-NamedProps.ps1 3 | # 4 | # By David Barrett, Microsoft Ltd. 2019. Use at your own risk. No warranties are given. 5 | # 6 | # DISCLAIMER: 7 | # THIS CODE IS SAMPLE CODE. THESE SAMPLES ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. 8 | # MICROSOFT FURTHER DISCLAIMS ALL IMPLIED WARRANTIES INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR OF FITNESS FOR 9 | # A PARTICULAR PURPOSE. THE ENTIRE RISK ARISING OUT OF THE USE OR PERFORMANCE OF THE SAMPLES REMAINS WITH YOU. IN NO EVENT SHALL 10 | # MICROSOFT OR ITS SUPPLIERS BE LIABLE FOR ANY DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, 11 | # BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE 12 | # SAMPLES, EVEN IF MICROSOFT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. BECAUSE SOME STATES DO NOT ALLOW THE EXCLUSION OR LIMITATION 13 | # OF LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE LIMITATION MAY NOT APPLY TO YOU. 14 | 15 | $version = "1.1.6" 16 | 17 | Function Log([string]$Details, [ConsoleColor]$Colour) 18 | { 19 | if ($Colour -eq $null) 20 | { 21 | $Colour = [ConsoleColor]::White 22 | } 23 | Write-Host $Details -ForegroundColor $Colour 24 | if ( $LogFile -eq "" ) { return } 25 | "$([DateTime]::Now.ToShortDateString()) $([DateTime]::Now.ToLongTimeString()) $Details" | Out-File $LogFile -Append 26 | } 27 | 28 | Function LogVerbose([string]$Details) 29 | { 30 | Write-Verbose $Details 31 | if ( $LogFile -eq "" ) { return } 32 | "$([DateTime]::Now.ToShortDateString()) $([DateTime]::Now.ToLongTimeString()) $Details" | Out-File $LogFile -Append 33 | } 34 | 35 | $script:LastError = $Error[0] 36 | Function ErrorReported($Context) 37 | { 38 | # Check for any error, and return the result ($true means a new error has been detected) 39 | 40 | # We check for errors using $Error variable, as try...catch isn't reliable when remoting 41 | if ([String]::IsNullOrEmpty($Error[0])) { return $false } 42 | 43 | # We have an error, have we already reported it? 44 | if ($Error[0] -eq $script:LastError) { return $false } 45 | 46 | # New error, so log it and return $true 47 | $script:LastError = $Error[0] 48 | if ($Context) 49 | { 50 | Log "Error ($Context): $($Error[0])" Red 51 | } 52 | else 53 | { 54 | Log "Error: $($Error[0])" Red 55 | } 56 | return $true 57 | } 58 | 59 | Function ReportError($Context) 60 | { 61 | # Reports error without returning the result 62 | ErrorReported $Context | Out-Null 63 | } 64 | 65 | Function CmdletsAvailable() 66 | { 67 | param ( 68 | $RequiredCmdlets, 69 | $Silent = $False, 70 | $PSSession = $null 71 | ) 72 | 73 | $cmdletsAvailable = $True 74 | foreach ($cmdlet in $RequiredCmdlets) 75 | { 76 | $cmdletExists = $false 77 | if ($PSSession) 78 | { 79 | $cmdletExists = $(Invoke-Command -Session $PSSession -ScriptBlock { Get-Command $Using:cmdlet -ErrorAction Ignore }) 80 | } 81 | else 82 | { 83 | $cmdletExists = $(Get-Command $cmdlet -ErrorAction Ignore) 84 | } 85 | if (!$cmdletExists) 86 | { 87 | if (!$Silent) { Log "Required cmdlet $cmdlet is not available" Red } 88 | $cmdletsAvailable = $False 89 | } 90 | } 91 | 92 | return $cmdletsAvailable 93 | } 94 | 95 | Function CheckEnvironment($PowerShellUrl) 96 | { 97 | # Now check that we have the required Exchange cmdlets available 98 | if ( !$(CmdletsAvailable @("Get-Mailbox") $True ) ) 99 | { 100 | if ( ![String]::IsNullOrEmpty($PowerShellUrl) ) 101 | { 102 | # Try to connect and import a session 103 | Log "Connecting to Exchange using PowerShell Url: $PowerShellUrl" 104 | $script:ExchangeSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri $PowerShellUrl -Credential $Credentials #-Authentication Kerberos -WarningAction 'SilentlyContinue' 105 | ReportError "New-PSSession" 106 | $script:ExchangeSession | Export-Clixml -Path "$($FilePath)exchange.session.xml" 107 | 108 | # If we don't have all the cmdlets available, we can't go any further 109 | if ( !$(CmdletsAvailable @("Get-Mailbox") $False $script:ExchangeSession) ) 110 | { 111 | Log "Required Exchange cmdlet(s) are missing, cannot continue" Red 112 | exit 113 | } 114 | 115 | # Import the session 116 | Import-PSSession $script:ExchangeSession -AllowClobber -WarningAction 'SilentlyContinue' -CommandType All -DisableNameChecking 117 | ReportError "Import-PSSession" 118 | } 119 | } 120 | } 121 | 122 | Function GetMailboxSmtpAddress() 123 | { 124 | param ($id, $archive, $organisation) 125 | 126 | if ( ![String]::IsNullOrEmpty($script:MailboxId) ) 127 | { 128 | return $script:MailboxId 129 | } 130 | $mb = $null 131 | 132 | if ( ![String]::IsNullOrEmpty($organisation) ) 133 | { 134 | LogVerbose "Get-Mailbox $id -Organization $organisation" 135 | $mb = Get-Mailbox $id -Organization $organisation 136 | } 137 | else 138 | { 139 | LogVerbose "Get-Mailbox $id" 140 | $mb = Get-Mailbox $id 141 | } 142 | if ($mb -eq $null) 143 | { 144 | $script:MailboxId = "Unknown: $id" 145 | } 146 | else 147 | { 148 | if ( ![String]::IsNullOrEmpty($organisation) ) 149 | { 150 | $script:MailboxId = $mb.PrimarySmtpAddress.Address 151 | } 152 | else 153 | { 154 | $script:MailboxId = $mb.PrimarySmtpAddress 155 | } 156 | if ($archive) 157 | { 158 | $script:MailboxId = "[ARCHIVE]$($script:MailboxId)" 159 | } 160 | } 161 | LogVerbose "Mailbox Id: $($script:MailboxId)" 162 | } 163 | 164 | $script:dumpEntryIdsCounter = 0 165 | Function DumpEntryIdList() 166 | { 167 | param ( 168 | $dumpEntryIdsFileName, 169 | $propIds, 170 | $force = $false 171 | ) 172 | 173 | # Dump all found EntryIds to file - we only do this once every $SaveRestartInfoInterval times requested, as dumping to text file is very expensive in the context of this script 174 | # (if a message has lots of named properties on it, we don't really want to dump the whole file for every named prop) 175 | if (!$force) 176 | { 177 | $script:dumpEntryIdsCounter++ 178 | if ($script:dumpEntryIdsCounter -lt $SaveRestartInfoInterval) { return } 179 | $script:dumpEntryIdsCounter = 0 180 | } 181 | 182 | LogVerbose "Dumping complete entry Id list for $($script:MailboxId)" 183 | 184 | $mailboxSmtpAddress = $script:MailboxId 185 | if ( ![String]::IsNullOrEmpty($script:MailboxId.Local) ) 186 | { 187 | # Our mailbox Id is not a string, so we'll piece together the SMTP address 188 | $mailboxSmtpAddress = "$($script:MailboxId.Local)@$($script:MailboxId.Domain)" 189 | } 190 | $mailboxSmtpAddress | out-file $dumpEntryIdsFileName 191 | foreach ($entryId in $propIds.Keys) 192 | { 193 | "$entryId;$($propIds[$entryId])" | out-file $dumpEntryIdsFileName -Append 194 | } 195 | } 196 | 197 | Function Check-NamedProps 198 | { 199 | param ( 200 | [Parameter(Position=0,Mandatory=$False,HelpMessage="Specifies the mailbox server (only databases from this server will be queried)")] 201 | [ValidateNotNullOrEmpty()] 202 | $MailboxServer, 203 | 204 | [Parameter(Position=1,Mandatory=$False,HelpMessage="Specifies the mailbox database to analyse")] 205 | [ValidateNotNullOrEmpty()] 206 | $MailboxDatabase, 207 | 208 | [Parameter(Position=2,Mandatory=$False,HelpMessage="Specifies the mailbox to analyse")] 209 | [ValidateNotNullOrEmpty()] 210 | $Mailbox, 211 | 212 | [Parameter(Mandatory=$False,HelpMessage="Specifies the organization to which the mailbox belong (Microsoft internal use only)")] 213 | [ValidateNotNullOrEmpty()] 214 | $Organization, 215 | 216 | [Parameter(Mandatory=$False,HelpMessage="Processes the archive mailbox instead of the primary")] 217 | [ValidateNotNullOrEmpty()] 218 | [switch]$Archive, 219 | 220 | [Parameter(Mandatory=$False,HelpMessage="If set, the script keeps track of where it is so that if interrupted it continues where it left off")] 221 | [alias("Restartable")] 222 | [switch]$Restart, 223 | 224 | [Parameter(Mandatory=$False,HelpMessage="If set, the script keeps track of where it is so that if interrupted it continues where it left off")] 225 | [ValidateNotNullOrEmpty()] 226 | [int]$SaveRestartInfoInterval = 50, 227 | 228 | [Parameter(Mandatory=$False,HelpMessage="If a mailbox has more than the specified number of properties, then the properties are dumped to a file. Setting this to -1 disables property dumps.")] 229 | [ValidateNotNullOrEmpty()] 230 | $DumpPropsIfTotalExceeds = 0, 231 | 232 | [Parameter(Mandatory=$False,HelpMessage="If specifed, only properties that have this name will be searched.")] 233 | [ValidateNotNullOrEmpty()] 234 | $SearchNamedProp, 235 | 236 | [Parameter(Mandatory=$False,HelpMessage="If specifed, only properties that have this GUID will be searched.")] 237 | [ValidateNotNullOrEmpty()] 238 | $SearchGuid, 239 | 240 | [Parameter(Mandatory=$False,HelpMessage="Type of named property being searched (defaults to PT_STRING). Only required for a message search of found props.")] 241 | [ValidateNotNullOrEmpty()] 242 | $NamedPropType = "001f", 243 | 244 | [Parameter(Mandatory=$False,HelpMessage="Where to dump the properties when needed. If missing, current directory will be used.")] 245 | [ValidateNotNullOrEmpty()] 246 | [String]$DumpPropsPath = "", 247 | 248 | [Parameter(Mandatory=$False,HelpMessage="If specified, the entry Ids of messages with matched named properties will be retrieved and saved (requires -SearchNamedProp)")] 249 | [ValidateNotNullOrEmpty()] 250 | [switch]$DumpEntryIds, 251 | 252 | [Parameter(Mandatory=$false,HelpMessage="PowerShell Url")] 253 | [string]$PowerShellUrl, 254 | 255 | [Parameter(Mandatory=$False,HelpMessage="Log file - activity is logged to this file if specified")] 256 | [string]$LogFile = "" 257 | ) 258 | 259 | Begin 260 | { 261 | # Check we have Exchange cmdlets available 262 | CheckEnvironment $PowerShellUrl 263 | 264 | # Need to load ManagedStoreDiagnostics.ps1 for Get-StoreQuery 265 | $originalPath = $(Get-Location).Path 266 | if ($(Test-Path -Path "$($env:exchangeinstallpath)Scripts\ManagedStoreDiagnosticFunctions.ps1")) 267 | { 268 | cd "$($env:exchangeinstallpath)Scripts" 269 | } 270 | else 271 | { 272 | # ManagedStoreDiagnosticFunctions not found. 273 | if (!$(Test-Path -Path "ManagedStoreDiagnosticFunctions.ps1")) 274 | { 275 | # We can't continue without this 276 | Log "Could not locate ManagedStoreDiagnosticFunctions.ps1 - this is available from your Exchange server Scripts folder, and should be placed in the same location as this script." Red 277 | exit 278 | } 279 | } 280 | . .\ManagedStoreDiagnosticFunctions.ps1 281 | cd $originalPath 282 | } 283 | 284 | Process 285 | { 286 | Log "$($MyInvocation.MyCommand.Name) version $version" 287 | 288 | # Check parameters 289 | if (![String]::IsNullOrEmpty($DumpPropsPath)) 290 | { 291 | if (!$DumpPropsPath.EndsWith("\")) 292 | { $DumpPropsPath = "$DumpPropsPath\" } 293 | } 294 | 295 | # As this process can take a very long time, we show a progress bar 296 | Write-Progress -Activity "Reading mailboxes" -Status "Reading database(s)" -PercentComplete 0 297 | 298 | # Get our list of databases 299 | $script:MailboxGuid = "" 300 | if ($MailboxServer) 301 | { 302 | $databases = Get-MailboxDatabase -Server $MailboxServer 303 | } 304 | elseif ($MailboxDatabase) 305 | { 306 | $databases = Get-MailboxDatabase -Identity $MailboxDatabase 307 | } 308 | elseif ($Mailbox) 309 | { 310 | # We are checking a single mailbox, so we locate the database first 311 | $mb = $null 312 | if ( ![String]::IsNullOrEmpty($Organization) ) 313 | { 314 | LogVerbose "Get-Mailbox $Mailbox -Organization $Organization" 315 | $mb = Get-Mailbox $Mailbox -Organization $Organization 316 | } 317 | else 318 | { 319 | LogVerbose "Get-Mailbox $Mailbox" 320 | $mb = Get-Mailbox $Mailbox 321 | } 322 | if ( $mb -eq $null ) 323 | { 324 | Log "Failed to retrieve mailbox $Mailbox" Red 325 | Exit 326 | } 327 | if ( ![String]::IsNullOrEmpty($Organization) ) 328 | { 329 | $script:MailboxId = $mb.PrimarySmtpAddress.Address 330 | } 331 | else 332 | { 333 | $script:MailboxId = $mb.PrimarySmtpAddress 334 | } 335 | if ($Archive) 336 | { 337 | Log "Checking archive mailbox $Mailbox, which is located in database $($mb.ArchiveDatabase)" 338 | $databases = Get-MailboxDatabase -Identity $mb.ArchiveDatabase 339 | $script:mailboxGuid = $mb.ArchiveGuid 340 | } 341 | else 342 | { 343 | Log "Checking mailbox $Mailbox, which is located in database $($mb.Database)" 344 | $databases = Get-MailboxDatabase -Identity $mb.Database 345 | $script:mailboxGuid = $mb.ExchangeGuid 346 | } 347 | } 348 | else 349 | { 350 | $databases = Get-MailboxDatabase 351 | } 352 | $currentDb = 0 353 | $dbCount = $databases.Count 354 | if (!$dbCount) { $dbCount = 1 } 355 | 356 | $wildcardSearch = $false 357 | if ( ![String]::IsNullOrEmpty($SearchNamedProp) ) 358 | { 359 | $wildcardSearch = $SearchNamedProp.Contains("*") 360 | } 361 | LogVerbose "Wildcard search: $wildcardSearch" 362 | 363 | foreach ($database in $databases) 364 | { 365 | Log "Processing database $($database.Name) ($($database.AdminDisplayVersion))" 366 | Write-Progress -Activity "Reading mailboxes" -Status "Reading mailboxes in database $database.Name ($currentDb of $dbCount)" -PercentComplete (($currentDb++/$dbCount)*100) 367 | # Get list of mailboxes and their numbers 368 | $mailboxQuery = "SELECT MailboxGuid,MailboxInstanceGuid,DisplayName,MailboxNumber FROM Mailbox" 369 | if ( ![String]::IsNullOrEmpty($MailboxGuid) ) 370 | { 371 | $mailboxQuery = "$mailboxQuery WHERE MailboxGuid=`"$($MailboxGuid)`"" 372 | } 373 | LogVerbose "Mailbox query: $mailboxQuery" 374 | $mbxs = Get-StoreQuery -Database $database.Name -Query $mailboxQuery -Unlimited 375 | $mbxCount = $mbxs.Count 376 | if ( ($mbxCount -eq $null) -and $mbxs) 377 | { 378 | # We don't have Count property returned when we only have one mailbox 379 | $mbxCount = 1 380 | } 381 | 382 | # For each mailbox, get the named prop count 383 | $currentMbx = 0 384 | if ($mbxCount -lt 1) 385 | { 386 | # We didn't find any mailboxes 387 | Log "No mailboxes to check" 388 | } 389 | else 390 | { 391 | foreach ($mbx in $mbxs) 392 | { 393 | GetMailboxSmtpAddress $mbx.MailboxGuid.ToString() $Archive $Organization 394 | $entryIdsDumpFile = "$DumpPropsPath$($mbx.MailboxGuid).EntryIds.txt" 395 | $propIds = @{} 396 | $alreadySearchedProps = @() 397 | if ( $Restart ) 398 | { 399 | # In restartable mode, so we check for an already existing file, and if it is present, load it 400 | if ( $(Test-Path $entryIdsDumpFile) ) 401 | { 402 | # Import the EntryIds from a text file 403 | Log "EntryId export already exists for mailbox, reading existing export from file: $entryIdsDumpFile" 404 | $exportedEntryIds = Get-Content -Path $entryIdsDumpFile 405 | if ( $exportedEntryIds ) 406 | { 407 | if ($exportedEntryIds[0] -ne $script:MailboxId) 408 | { 409 | Log "Existing mailbox export does not match, restart not possible" Red 410 | $entryIdsDumpFile = "$DumpPropsPath$($mbx.MailboxGuid).EntryIds.New.txt" 411 | Log "Will export to file: $entryIdsDumpFile" 412 | } 413 | else 414 | { 415 | for ( $i=1; $i -lt $exportedEntryIds.Length; $i++ ) 416 | { 417 | $importedEntryId, $exportedNamedProps = $exportedEntryIds[$i] -split ";" 418 | foreach ($exportedNamedProp in $exportedNamedProps) 419 | { 420 | $alreadySearchedProps += $exportedNamedProp 421 | } 422 | $propIds.Add( $importedEntryId, $($exportedNamedProps -join ";") ) 423 | 424 | } 425 | } 426 | } 427 | } 428 | else 429 | { 430 | # EntryIds export file doesn't exist, so create one with the mailbox Id as the first line 431 | $mailboxSmtpAddress = $script:MailboxId 432 | if ( ![String]::IsNullOrEmpty($script:MailboxId.Local) ) 433 | { 434 | # Our mailbox Id is not a string, so we'll piece together the SMTP address 435 | $mailboxSmtpAddress = "$($script:MailboxId.Local)@$($script:MailboxId.Domain)" 436 | } 437 | $mailboxSmtpAddress | out-file $entryIdsDumpFile 438 | } 439 | } 440 | 441 | $currentMbx++ 442 | Write-Progress -Activity "Retrieving named props" -Status "Retrieving named properties in mailbox $($mbx.DisplayName)" -PercentComplete (($currentMbx/$mbxCount)*100) 443 | $skip = $false 444 | # We ignore system and health mailboxes 445 | if ($mbx.DisplayName.StartsWith("SystemMailbox") -or $mbx.DisplayName.Contains("HealthMailbox") -or $mbx.DisplayName.Equals("Microsoft Exchange")) 446 | { $skip = $true } 447 | 448 | if (!$skip) 449 | { 450 | LogVerbose "Retrieving properties from mailbox $($mbx.DisplayName)" 451 | $propQuery = "SELECT PropNumber,PropGuid,PropName,PropDispId FROM ExtendedPropertyNameMapping WHERE MailboxPartitionNumber=$($mbx.MailboxNumber)" 452 | if (![String]::IsNullOrEmpty($SearchNamedProp)) 453 | { 454 | # If the name contains wildcard, we don't limit our query (we filter in the script instead) 455 | if ( !$WildCardSearch ) 456 | { 457 | $propQuery = "$propQuery AND PropName=`"$SearchNamedProp`"" 458 | } 459 | } 460 | if (![String]::IsNullOrEmpty($SearchGuid)) 461 | { 462 | $propQuery = "$propQuery AND PropGuid=`"$SearchGuid`"" 463 | } 464 | LogVerbose "Prop query: $propQuery" 465 | 466 | $namedProps = Get-StoreQuery -Database $database.Name -Query $propQuery -Unlimited 467 | $namedPropCount = $namedProps.Count 468 | if (!$namedPropCount) { $namedPropCount = 0 } 469 | LogVerbose "Number of properties returned: $($namedPropCount)" 470 | 471 | # We create a new PsObject to hold this data as then we can pipe to other cmdlets (e.g. Export-CSV) 472 | $output = New-Object PsObject 473 | if ( ![String]::IsNullOrEmpty($script:MailboxGuid) ) 474 | { 475 | $output | Add-Member -MemberType NoteProperty -Name "MailboxGuid" -Value $script:MailboxGuid 476 | } 477 | else 478 | { 479 | $output | Add-Member -MemberType NoteProperty -Name "MailboxGuid" -Value $mbx.MailboxGuid 480 | } 481 | $output | Add-Member -MemberType NoteProperty -Name "DisplayName" -Value $mbx.DisplayName 482 | $output | Add-Member -MemberType NoteProperty -Name "NamedPropCount" -Value $namedPropCount 483 | $output 484 | 485 | if ( ($namedPropCount -gt 0) -and ($DumpPropsIfTotalExceeds -ge 0) ) 486 | { 487 | if ( ($DumpPropsIfTotalExceeds -le $namedPropCount) -or $SearchNamedProp -or $SearchGuid ) 488 | { 489 | # This mailbox exceeds our property limit (or contains the specific named property we are looking for), so we dump all the properties to a file for further investigation 490 | $dumpPropsFileName = "$DumpPropsPath$($mbx.MailboxGuid).namedprops.xml" 491 | LogVerbose "Dumping property list to: $dumpPropsFileName" 492 | $namedProps | Export-Clixml $dumpPropsFileName 493 | 494 | if ($DumpEntryIds) 495 | { 496 | # Now we perform a search for all messages with these properties 497 | $mailboxInstanceGuid = [BitConverter]::ToString($mbx.MailboxInstanceGuid.ToByteArray()).Replace("-","") # Required to create EntryId 498 | $dumpEntryIdsDebugFileName = "$DumpPropsPath$($mbx.MailboxGuid).EntryIds.Debug.{n}.txt" 499 | $dumpEntryIdsDebugIndex = 1 500 | $currentProp = 1 501 | $namedPropCount = $namedProps.Count 502 | $smtpWritten = $false 503 | 504 | DumpEntryIdList $entryIdsDumpFile $propIds # This ensures we have any restartable info, and stamps the mailbox name at the top of the file 505 | ForEach ($namedProp in $namedProps) 506 | { 507 | $ewsPropId = "$($namedProp.PropGuid)/$($namedProp.PropName)/$NamedPropType" 508 | $nameMatch = !$wildcardSearch 509 | if ( $wildcardSearch ) 510 | { 511 | if ( $namedProp.PropName -like $SearchNamedProp ) 512 | { 513 | LogVerbose "$($namedProp.PropName) matches $SearchNamedProp" 514 | $nameMatch = $true 515 | } 516 | else 517 | { 518 | LogVerbose "$($namedProp.PropName) does NOT match $SearchNamedProp" 519 | } 520 | } 521 | 522 | if ( $Restart ) 523 | { 524 | # If we've restarted, check we haven't already searched for this prop 525 | if ( $alreadySearchedProps.Contains($ewsPropId) ) 526 | { 527 | $nameMatch = $false 528 | LogVerbose "Already searched for property $ewsPropId, skipping" 529 | } 530 | } 531 | 532 | if ( $nameMatch ) 533 | { 534 | Write-Progress -Activity "Searching for properties" -Status "Searching for property $($namedProp.PropNumber)" -PercentComplete (($currentProp++/$namedPropCount)*100) 535 | $propId = "p$('{0:x}' -f $namedProp.PropNumber)$NamedPropType" 536 | 537 | $messageQuery = "SELECT MessageId, FolderId FROM Message WHERE MailboxPartitionNumber=$($mbx.MailboxNumber) AND $propId != null" 538 | LogVerbose "Message query: $messageQuery" 539 | $messages = Get-StoreQuery -Database $database.Name -Query $messageQuery -Unlimited 540 | 541 | # Get a valid message count. Store query returns a blank record rather than no records when no data is found 542 | $messageCount = $messages.Count 543 | if (!$messageCount) 544 | { 545 | $messageCount = 0 546 | if (![String]::IsNullOrEmpty($messages.FolderId) -and ![String]::IsNullOrEmpty($messages.MessageId)) 547 | { 548 | $messageCount = 1 549 | } 550 | } 551 | LogVerbose "Number of messages returned: $($messageCount)" 552 | 553 | if ($messageCount -gt 0) 554 | { 555 | 556 | $eidType = "0700" # eitLTPrivateMessage 557 | ForEach ($message in $messages) 558 | { 559 | if ( ($message.FolderId.Length -lt 50) -or ($message.MessageId.Length -lt 50) ) 560 | { 561 | Log "Invalid message details returned: FolderId = $($message.FolderId), MessageId = $($message.MessageId)" Red 562 | } 563 | else 564 | { 565 | $entryId = "00000000$($mailboxInstanceGuid)$eidType$($message.FolderId.Substring(2,48))$($message.MessageId.Substring(2,48))" 566 | Log "$propId found on item: $entryId" 567 | if (!$propIds.ContainsKey($entryId)) # Deduplication (for messages with more than one property) 568 | { 569 | $propIds.Add($entryId, $ewsPropId) 570 | if ($Restart) 571 | { 572 | "$entryId;$ewsPropId" | out-file $entryIdsDumpFile -Append 573 | } 574 | } 575 | else 576 | { 577 | # Here we have more than one named prop on a single item, so we need to dump the whole 578 | # EntryId list again 579 | $newProps = "$($propIds[$entryId]);$ewsPropId" 580 | $propIds.Remove($entryId) 581 | $propIds.Add($entryId, $newProps) 582 | if ($Restart) 583 | { 584 | DumpEntryIdList $entryIdsDumpFile $propIds 585 | } 586 | } 587 | } 588 | } 589 | } 590 | } 591 | } 592 | Write-Progress -Activity "Searching for properties" -Completed 593 | DumpEntryIdList $entryIdsDumpFile $propIds $true 594 | } 595 | } 596 | } 597 | } 598 | else 599 | { 600 | LogVerbose "Skipping $mbx.DisplayName" 601 | } 602 | } 603 | Write-Progress -Activity "Retrieving named props" -Completed 604 | } 605 | } 606 | Write-Progress -Activity "Reading mailboxes" -Completed 607 | Log "Mailbox processing finished" Green 608 | } 609 | 610 | End 611 | { 612 | # Just to ensure that we don't change the path... I hate it when a script changes a path and doesn't set it back! 613 | cd $originalPath 614 | } 615 | } -------------------------------------------------------------------------------- /Legacy/Create-MailboxBatches.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Create-MailboxBatches.ps1 3 | # 4 | # By David Barrett, Microsoft Ltd. Use at your own risk. No warranties are given. 5 | # 6 | # DISCLAIMER: 7 | # THIS CODE IS SAMPLE CODE. THESE SAMPLES ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. 8 | # MICROSOFT FURTHER DISCLAIMS ALL IMPLIED WARRANTIES INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR OF FITNESS FOR 9 | # A PARTICULAR PURPOSE. THE ENTIRE RISK ARISING OUT OF THE USE OR PERFORMANCE OF THE SAMPLES REMAINS WITH YOU. IN NO EVENT SHALL 10 | # MICROSOFT OR ITS SUPPLIERS BE LIABLE FOR ANY DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, 11 | # BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE 12 | # SAMPLES, EVEN IF MICROSOFT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. BECAUSE SOME STATES DO NOT ALLOW THE EXCLUSION OR LIMITATION 13 | # OF LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE LIMITATION MAY NOT APPLY TO YOU. 14 | 15 | 16 | param ( 17 | [Parameter(Mandatory=$False,HelpMessage="Use Exchange Online PowerShell module to connect (must be installed)")] 18 | [switch] $ExchangeOnline, 19 | 20 | [Parameter(Mandatory=$False,HelpMessage="Credentials used to authenticate with Exchange PowerShell for on-premises")] 21 | [alias("Credentials")] 22 | [System.Management.Automation.PSCredential]$Credential, 23 | 24 | [Parameter(Mandatory=$False,HelpMessage="On-premises PowerShell Url")] 25 | [String]$PowerShellUrl, 26 | 27 | [Parameter(Mandatory=$False,HelpMessage="Same as Get-Mailbox -Filter parameter, use for filtering")] 28 | $Filter = "", 29 | 30 | [Parameter(Mandatory=$False,HelpMessage="Same as Get-Mailbox -OrganizationalUnit parameter, use for filtering")] 31 | $OrganizationalUnit, 32 | 33 | [Parameter(Mandatory=$True,HelpMessage="Where the mailbox batch files will be created")] 34 | [String]$ExportBatchPath, 35 | 36 | [Parameter(Mandatory=$False,HelpMessage="Maximum number of mailboxes per batch")] 37 | [int]$BatchSize = 25 38 | ) 39 | 40 | 41 | Function CmdletsAvailable() 42 | { 43 | param ( 44 | $RequiredCmdlets, 45 | $Silent = $False 46 | ) 47 | 48 | $cmdletsAvailable = $True 49 | foreach ($cmdlet in $RequiredCmdlets) 50 | { 51 | if (Get-Command $cmdlet -ErrorAction SilentlyContinue) 52 | { 53 | } 54 | else 55 | { 56 | if (!$Silent) { Write-Host "Required cmdlet $cmdlet is not available" -ForegroundColor Red } 57 | $cmdletsAvailable = $False 58 | break 59 | } 60 | } 61 | 62 | return $cmdletsAvailable 63 | } 64 | 65 | Function ImportExchangeManagementSession() 66 | { 67 | param ( 68 | $RequiredCmdlets = "Get-Mailbox" 69 | ) 70 | 71 | # Check we have Exchange Management Session available. If not, we attempt to connect to and import one. 72 | if ( CmdletsAvailable $RequiredCmdlets $True ) 73 | { 74 | # Cmdlets we need are available, so no need to import any session 75 | return 76 | } 77 | 78 | if ($ExchangeOnline) 79 | { 80 | Connect-ExchangeOnline 81 | if ( CmdletsAvailable $RequiredCmdlets $True ) 82 | { 83 | return 84 | } 85 | Write-Host "Failed to connect to Exchange Online PowerShell" -ForegroundColor Red 86 | exit 87 | } 88 | 89 | if ([String]::IsNullOrEmpty($PowerShellUrl)) 90 | { 91 | Write-Host "PowerShell Url not specified and Exchange PowerShell session not available. Cannot continue." -ForegroundColor Red 92 | exit 93 | } 94 | 95 | Write-Host "Attempting to connect to and import Exchange Management session" -ForegroundColor Gray 96 | $global:session = $null 97 | if ($null -eq $Credentials) 98 | { 99 | # No credentials specified, so we attempt to connect without specifying them (which will attempt to authenticate as the logged on user) 100 | $global:session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri $PowerShellUrl -AllowRedirection 101 | } 102 | else 103 | { 104 | # We have credentials, so we use them - we only use basic auth if the Url is https 105 | if (!$PowerShellUrl.ToLower().StartsWith("https")) 106 | { 107 | $global:session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri $PowerShellUrl -Credential $Credentials -AllowRedirection 108 | } 109 | else 110 | { 111 | # With HTTPS we use basic auth, as this is required for Office 365 112 | $global:session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri $PowerShellUrl -Credential $Credentials -Authentication Basic -AllowRedirection 113 | } 114 | } 115 | 116 | if ($null -eq $global:session) 117 | { 118 | Write-Host "Failed to open Exchange Administration session, cannot continue" -ForegroundColor Red 119 | exit 120 | } 121 | Write-Host "Exchange PowerShell session successfully established" -ForegroundColor Green 122 | Import-PSSession $global:session 123 | 124 | # Now check that we have the cmdlets we need available 125 | if ( CmdletsAvailable($RequiredCmdlets) ) 126 | { 127 | return 128 | } 129 | 130 | exit 131 | } 132 | 133 | # Check export path 134 | if ( !(Test-Path -Path $ExportBatchPath -PathType Container) ) 135 | { 136 | # Doesn't exist, we'll try to create it 137 | New-Item -ItemType Directory $ExportBatchPath -Force 138 | if ( !(Test-Path -Path $ExportBatchPath -PathType Container) ) 139 | { 140 | Write-Host "Invalid export path: $ExportBatchPath" -ForegroundColor Red 141 | Exit 142 | } 143 | } 144 | if ( !($ExportBatchPath.EndsWith("\")) ) 145 | { $ExportBatchPath = "$ExportBatchPath\" } 146 | 147 | # Validate the availability of Get-Mailbox 148 | ImportExchangeManagementSession( @( "Get-Mailbox") ) 149 | 150 | $params = @{ 151 | ResultSize = "Unlimited" 152 | } 153 | if (![String]::IsNullOrEmpty($OrganizationalUnit)) 154 | { 155 | $params.OrganizationalUnit = $OrganizationalUnit 156 | } 157 | if (![String]::IsNullOrEmpty($Filter)) 158 | { 159 | $params.Filter = $Filter 160 | } 161 | 162 | 163 | # Retrieve all mailboxes 164 | if ($ExchangeOnline) 165 | { 166 | $params.PropertySets = "Minimum" 167 | $global:mailboxes = Get-EXOMailbox @params 168 | } 169 | else { 170 | $mailboxes = Get-Mailbox @params 171 | } 172 | 173 | # Now export the primary SMTP addresses of each mailbox to a file 174 | $fileNum = 1 175 | $userCount = 0 176 | foreach ($mailbox in $mailboxes) { 177 | $primarySmtpAddress = $mailbox.PrimarySmtpAddress 178 | if ([String]::IsNullOrEmpty($primarySmtpAddress)) 179 | { 180 | $primarySmtpAddress = $mailbox.WindowsEmailAddress 181 | } 182 | if ([String]::IsNullOrEmpty($primarySmtpAddress)) 183 | { 184 | Write-Host "No primary SMTP address found for mailbox $($mailbox.Name)" -ForegroundColor Yellow 185 | continue 186 | } 187 | Write-Verbose $primarySmtpAddress 188 | $primarySmtpAddress | Out-File "$ExportBatchPath\$fileNum.txt" -Append 189 | $userCount++ 190 | if ($userCount -ge $BatchSize) 191 | { 192 | $userCount = 0 193 | $fileNum++ 194 | } 195 | } -------------------------------------------------------------------------------- /Legacy/Delete-ByEntryId.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Delete-ByEntryId.ps1 3 | # 4 | # By David Barrett, Microsoft Ltd. 2016-2019. Use at your own risk. No warranties are given. 5 | # 6 | # DISCLAIMER: 7 | # THIS CODE IS SAMPLE CODE. THESE SAMPLES ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. 8 | # MICROSOFT FURTHER DISCLAIMS ALL IMPLIED WARRANTIES INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR OF FITNESS FOR 9 | # A PARTICULAR PURPOSE. THE ENTIRE RISK ARISING OUT OF THE USE OR PERFORMANCE OF THE SAMPLES REMAINS WITH YOU. IN NO EVENT SHALL 10 | # MICROSOFT OR ITS SUPPLIERS BE LIABLE FOR ANY DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, 11 | # BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE 12 | # SAMPLES, EVEN IF MICROSOFT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. BECAUSE SOME STATES DO NOT ALLOW THE EXCLUSION OR LIMITATION 13 | # OF LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE LIMITATION MAY NOT APPLY TO YOU. 14 | 15 | param ( 16 | [Parameter(Position=0,Mandatory=$False,HelpMessage="Specifies the mailbox to be accessed. If not present, then the first EntryId supplied must be the PrimarySmtpAddress of the mailbox.")] 17 | [ValidateNotNullOrEmpty()] 18 | [string]$Mailbox, 19 | 20 | [Parameter(Mandatory=$False,HelpMessage="When specified, the archive mailbox will be accessed (instead of the main mailbox)")] 21 | [switch]$Archive, 22 | 23 | [Parameter(Mandatory=$False,HelpMessage="List of EntryIds (optionally with named properties) to delete (or path to file containing this list)")] 24 | [ValidateNotNullOrEmpty()] 25 | $EntryIds, 26 | 27 | [Parameter(Mandatory=$False,HelpMessage="If this switch is set, the whole item will be deleted (otherwise, just the named property will be)")] 28 | [switch]$DeleteItems, 29 | 30 | [Parameter(Mandatory=$False,HelpMessage="If specified, items are deleted in batches. This is a lot quicker, but doesn't retrieve the item details prior to deletion.")] 31 | [switch]$Batch, 32 | 33 | [Parameter(Mandatory=$False,HelpMessage="Credentials used to authenticate with EWS")] 34 | [System.Management.Automation.PSCredential]$Credentials, 35 | 36 | [Parameter(Mandatory=$False,HelpMessage="If set, ApplicationImpersonation will not be used to access the mailbox (FullAccess rights will be required for the authenticating account)")] 37 | [switch]$DoNotImpersonate, 38 | 39 | [Parameter(Mandatory=$False,HelpMessage="EWS Url (if omitted, then autodiscover is used)")] 40 | [string]$EwsUrl, 41 | 42 | [Parameter(Mandatory=$False,HelpMessage="Path to managed API (if omitted, a search of standard paths is performed)")] 43 | [string]$EWSManagedApiPath = "", 44 | 45 | [Parameter(Mandatory=$False,HelpMessage="Whether to ignore any SSL errors (e.g. invalid certificate)")] 46 | [switch]$IgnoreSSLCertificate, 47 | 48 | [Parameter(Mandatory=$False,HelpMessage="Whether to allow insecure redirects when performing autodiscover")] 49 | [switch]$AllowInsecureRedirection, 50 | 51 | [Parameter(Mandatory=$False,HelpMessage="Log file - activity is logged to this file if specified")] 52 | [string]$LogFile = "", 53 | 54 | [Parameter(Mandatory=$False,HelpMessage="Trace file - if specified, EWS tracing information is written to this file")] 55 | [string]$TraceFile 56 | ) 57 | $script:ScriptVersion = "1.0.3" 58 | 59 | # Define our functions 60 | 61 | Function Log([string]$Details, [ConsoleColor]$Colour) 62 | { 63 | if ($Colour -eq $null) 64 | { 65 | $Colour = [ConsoleColor]::White 66 | } 67 | Write-Host $Details -ForegroundColor $Colour 68 | if ( $LogFile -eq "" ) { return } 69 | "$([DateTime]::Now.ToShortDateString()) $([DateTime]::Now.ToLongTimeString()) $Details" | Out-File $LogFile -Append 70 | } 71 | Log "$($MyInvocation.MyCommand.Name) version $($script:ScriptVersion) starting" Green 72 | 73 | Function LogVerbose([string]$Details) 74 | { 75 | Write-Verbose $Details 76 | if ( $LogFile -eq "" ) { return } 77 | "$([DateTime]::Now.ToShortDateString()) $([DateTime]::Now.ToLongTimeString()) $Details" | Out-File $LogFile -Append 78 | } 79 | 80 | Function LogDebug([string]$Details) 81 | { 82 | Write-Debug $Details 83 | if ( $LogFile -eq "" ) { return } 84 | "$([DateTime]::Now.ToShortDateString()) $([DateTime]::Now.ToLongTimeString()) $Details" | Out-File $LogFile -Append 85 | } 86 | 87 | $script:LastError = $Error[0] 88 | Function ErrorReported($Context) 89 | { 90 | # We check for errors here, using $Error variable, as try...catch isn't reliable when remoting 91 | if ([String]::IsNullOrEmpty($Error[0])) { return $false } 92 | 93 | # We have an error, have we already reported it? 94 | if ($Error[0] -eq $script:LastError) { return $false } 95 | 96 | # New error, so log it and return $true 97 | $script:LastError = $Error[0] 98 | if ($Context) 99 | { 100 | Log "Error ($Context): $($Error[0])" Red 101 | } 102 | else 103 | { 104 | Log "Error: $($Error[0])" Red 105 | } 106 | return $true 107 | } 108 | 109 | Function ReportError($Context) 110 | { 111 | # Reports error without returning the result 112 | ErrorReported $Context | Out-Null 113 | } 114 | 115 | Function LoadEWSManagedAPI() 116 | { 117 | # Find and load the managed API 118 | 119 | # Check if we've been given the path to the managed API 120 | if ( ![string]::IsNullOrEmpty($EWSManagedApiPath) ) 121 | { 122 | if ( Test-Path $EWSManagedApiPath ) 123 | { 124 | Add-Type -Path $EWSManagedApiPath 125 | return $true 126 | } 127 | Write-Host ( [string]::Format("Managed API not found at specified location: {0}", $EWSManagedApiPath) ) Yellow 128 | } 129 | 130 | # Search for the managed API 131 | $a = Get-ChildItem -ErrorAction SilentlyContinue | Where-Object { ($_.PSIsContainer -eq $false) -and ( $_.Name -eq "Microsoft.Exchange.WebServices.dll" ) } 132 | if (!$a) 133 | { 134 | $a = Get-ChildItem -Recurse "C:\Program Files\Microsoft\Exchange\Web Services" -ErrorAction Ignore | Where-Object { ($_.PSIsContainer -eq $false) -and ( $_.Name -eq "Microsoft.Exchange.WebServices.dll" ) } 135 | } 136 | if (!$a) 137 | { 138 | $a = Get-ChildItem -Recurse "C:\Program Files (x86)\Microsoft\Exchange\Web Services" -ErrorAction Ignore | Where-Object { ($_.PSIsContainer -eq $false) -and ( $_.Name -eq "Microsoft.Exchange.WebServices.dll" ) } 139 | } 140 | 141 | # If we've found it, we can load the managed API now 142 | if ($a) 143 | { 144 | Write-Host ([string]::Format("Using managed API {0} found at: {1}", $a.VersionInfo.FileVersion, $a.VersionInfo.FileName)) -ForegroundColor Gray 145 | Add-Type -Path $a.VersionInfo.FileName 146 | $script:EWSManagedApiPath = $a.VersionInfo.FileName 147 | return $true 148 | } 149 | return $false 150 | } 151 | 152 | Function TrustAllCerts() 153 | { 154 | # Implement call-back to override certificate handling (and accept all) 155 | $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider 156 | $Compiler=$Provider.CreateCompiler() 157 | ErrorReported "CreateCompiler" | Out-Null 158 | $Params=New-Object System.CodeDom.Compiler.CompilerParameters 159 | $Params.GenerateExecutable=$False 160 | $Params.GenerateInMemory=$True 161 | $Params.IncludeDebugInformation=$False 162 | $Params.ReferencedAssemblies.Add("System.DLL") | Out-Null 163 | 164 | $TASource=@' 165 | namespace Local.ToolkitExtensions.Net.CertificatePolicy { 166 | public class TrustAll : System.Net.ICertificatePolicy { 167 | public TrustAll() 168 | { 169 | } 170 | public bool CheckValidationResult(System.Net.ServicePoint sp, 171 | System.Security.Cryptography.X509Certificates.X509Certificate cert, 172 | System.Net.WebRequest req, int problem) 173 | { 174 | return true; 175 | } 176 | } 177 | } 178 | '@ 179 | $TAResults=$Provider.CompileAssemblyFromSource($Params,$TASource) 180 | ErrorReported "CompileAssembly" | Out-Null 181 | $TAAssembly=$TAResults.CompiledAssembly 182 | 183 | ## We now create an instance of the TrustAll and attach it to the ServicePointManager 184 | $TrustAll=$TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll") 185 | ErrorReported "CreateInstance" | Out-Null 186 | [System.Net.ServicePointManager]::CertificatePolicy=$TrustAll 187 | ErrorReported "Assign Policy" | Out-Null 188 | } 189 | 190 | Function CreateTraceListener($service) 191 | { 192 | # Create trace listener to capture EWS conversation (useful for debugging) 193 | if ($script:Tracer -eq $null) 194 | { 195 | $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider 196 | $Params=New-Object System.CodeDom.Compiler.CompilerParameters 197 | $Params.GenerateExecutable=$False 198 | $Params.GenerateInMemory=$True 199 | $Params.IncludeDebugInformation=$False 200 | $Params.ReferencedAssemblies.Add("System.dll") | Out-Null 201 | $Params.ReferencedAssemblies.Add($EWSManagedApiPath) | Out-Null 202 | 203 | $traceFileForCode = $traceFile.Replace("\", "\\") 204 | 205 | if (![String]::IsNullOrEmpty($TraceFile)) 206 | { 207 | LogVerbose "Tracing to: $TraceFile" 208 | } 209 | 210 | $TraceListenerClass = @" 211 | using System; 212 | using System.Text; 213 | using System.IO; 214 | using System.Threading; 215 | using Microsoft.Exchange.WebServices.Data; 216 | 217 | namespace TraceListener { 218 | class EWSTracer: Microsoft.Exchange.WebServices.Data.ITraceListener 219 | { 220 | private StreamWriter _traceStream = null; 221 | private string _lastResponse = String.Empty; 222 | 223 | public EWSTracer() 224 | { 225 | try 226 | { 227 | _traceStream = File.AppendText("$traceFileForCode"); 228 | } 229 | catch { } 230 | } 231 | 232 | ~EWSTracer() 233 | { 234 | Close(); 235 | } 236 | 237 | public void Close() 238 | { 239 | try 240 | { 241 | _traceStream.Flush(); 242 | _traceStream.Close(); 243 | } 244 | catch { } 245 | } 246 | 247 | 248 | public void Trace(string traceType, string traceMessage) 249 | { 250 | if ( traceType.Equals("EwsResponse") ) 251 | _lastResponse = traceMessage; 252 | 253 | if ( traceType.Equals("EwsRequest") ) 254 | _lastResponse = String.Empty; 255 | 256 | if (_traceStream == null) 257 | return; 258 | 259 | lock (this) 260 | { 261 | try 262 | { 263 | _traceStream.WriteLine(traceMessage); 264 | _traceStream.Flush(); 265 | } 266 | catch { } 267 | } 268 | } 269 | 270 | public string LastResponse 271 | { 272 | get { return _lastResponse; } 273 | } 274 | } 275 | } 276 | "@ 277 | 278 | $TraceCompilation=$Provider.CompileAssemblyFromSource($Params,$TraceListenerClass) 279 | $TraceAssembly=$TraceCompilation.CompiledAssembly 280 | $script:Tracer=$TraceAssembly.CreateInstance("TraceListener.EWSTracer") 281 | } 282 | 283 | # Attach the trace listener to the Exchange service 284 | $service.TraceListener = $script:Tracer 285 | } 286 | 287 | function CreateService($smtpAddress) 288 | { 289 | # Creates and returns an ExchangeService object to be used to access mailboxes 290 | 291 | # First of all check to see if we have a service object for this mailbox already 292 | 293 | # Create new service 294 | $exchangeService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013) 295 | 296 | # Set credentials if specified, or use logged on user. 297 | if ($Credentials -ne $Null) 298 | { 299 | LogVerbose "Applying given credentials: $($Credentials.UserName)" 300 | $exchangeService.Credentials = $Credentials.GetNetworkCredential() 301 | } 302 | else 303 | { 304 | LogVerbose "Using default credentials" 305 | $exchangeService.UseDefaultCredentials = $true 306 | } 307 | 308 | LogVerbose "Creating ExchangeService for: $smtpAddress" 309 | 310 | # Set EWS URL if specified, or use autodiscover if no URL specified. 311 | if ($EwsUrl) 312 | { 313 | LogVerbose "Using EWS Url: $EwsUrl" 314 | $exchangeService.URL = New-Object Uri($EwsUrl) 315 | } 316 | else 317 | { 318 | try 319 | { 320 | LogVerbose "Performing autodiscover for $smtpAddress" 321 | if ( $AllowInsecureRedirection ) 322 | { 323 | $exchangeService.AutodiscoverUrl($smtpAddress, {$True}) 324 | } 325 | else 326 | { 327 | $exchangeService.AutodiscoverUrl($smtpAddress) 328 | } 329 | if ([string]::IsNullOrEmpty($exchangeService.Url)) 330 | { 331 | Log "$smtpAddress : autodiscover failed" Red 332 | return $Null 333 | } 334 | LogVerbose "EWS Url found: $($exchangeService.Url)" 335 | } 336 | catch 337 | { 338 | Log "$smtpAddress : error occurred during autodiscover: $($Error[0])" Red 339 | return $null 340 | } 341 | } 342 | 343 | if ($exchangeService.URL.AbsoluteUri.ToLower().Equals("https://outlook.office365.com/ews/exchange.asmx")) 344 | { 345 | # This is Office 365, so we'll add a small delay to try and avoid throttling 346 | if ($script:currentThrottlingDelay -lt 100) 347 | { 348 | $script:currentThrottlingDelay = 100 349 | LogVerbose "Office 365 mailbox, throttling delay set to $($script:currentThrottlingDelay)ms" 350 | } 351 | } 352 | 353 | if (!$DoNotImpersonate) 354 | { 355 | # We are using ApplicationImpersonation to access the mailbox 356 | $exchangeService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $smtpAddress) 357 | } 358 | $exchangeService.HttpHeaders.Add("X-AnchorMailbox", $smtpAddress) 359 | 360 | # We enable tracing so that we can retrieve the last response (and read any throttling information from it - this isn't exposed in the EWS Managed API) 361 | CreateTraceListener $exchangeService 362 | $exchangeService.TraceFlags = [Microsoft.Exchange.WebServices.Data.TraceFlags]::All 363 | $exchangeService.TraceEnabled = $True 364 | 365 | # To test we have access to the mailbox, we bind to the Inbox. Any error here and we fail. 366 | $testBindFolder = $null 367 | try 368 | { 369 | if ($Archive) 370 | { 371 | $testBindFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchangeService, [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::ArchiveMsgFolderRoot, [Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly) 372 | } 373 | else 374 | { 375 | $testBindFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchangeService, [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox, [Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly) 376 | } 377 | } catch {} 378 | ReportError "Bind to folder in mailbox" 379 | if ($testBindFolder -eq $null) { return $null } 380 | 381 | return $exchangeService 382 | } 383 | 384 | function ConvertId($entryId) 385 | { 386 | # Use EWS ConvertId function to convert from EntryId to EWS Id 387 | 388 | $id = New-Object Microsoft.Exchange.WebServices.Data.AlternateId 389 | $id.Mailbox = $Mailbox 390 | $id.UniqueId = $entryId 391 | $id.IsArchive = $Archive 392 | $id.Format = [Microsoft.Exchange.WebServices.Data.IdFormat]::HexEntryId 393 | $ewsId = $Null 394 | try 395 | { 396 | $ewsId = $script:service.ConvertId($id, [Microsoft.Exchange.WebServices.Data.IdFormat]::EwsId) 397 | } 398 | catch {} 399 | ErrorReported | out-null 400 | LogVerbose "EWS Id: $($ewsId.UniqueId)" 401 | return $ewsId 402 | } 403 | 404 | Function RemoveProcessedItemsFromList() 405 | { 406 | # Process the results of a batch move/copy and remove any items that were successfully moved from our list of items to move 407 | param ( 408 | $requestedItems, 409 | $results, 410 | $Items 411 | ) 412 | 413 | $remainingItems = @() 414 | if ($results -ne $null) 415 | { 416 | $failed = 0 417 | for ($i = 0; $i -lt $requestedItems.Count; $i++) 418 | { 419 | if ($results[$i].ErrorCode -eq "NoError") 420 | { 421 | LogVerbose "Item successfully processed: $($requestedItems[$i])" 422 | } 423 | else 424 | { 425 | if ( ($results[$i].ErrorCode -eq "ErrorMoveCopyFailed") -or ($results[$i].ErrorCode -eq "ErrorInvalidOperation") -or ($results[$i].ErrorCode -eq "ErrorItemNotFound") ) 426 | { 427 | # This is a permanent error, so we remove the item from the list 428 | 429 | } 430 | else 431 | { 432 | $remainingItems += $requestedItems[$i] 433 | } 434 | LogVerbose("Error $($results[$i].ErrorCode) reported for item: $($requestedItems[$i].UniqueId)") 435 | $failed++ 436 | } 437 | } 438 | } 439 | if ( $failed -gt 0 ) 440 | { 441 | Log "$failed items reported error during batch request (if throttled, this is expected)" Yellow 442 | } 443 | return $remainingItems 444 | } 445 | 446 | Function BatchDelete() 447 | { 448 | # Send request to move/copy items, allowing for throttling (which in this case is likely to manifest as time-out errors) 449 | param ( 450 | $ItemsToDelete, 451 | $BatchSize = 200 452 | ) 453 | 454 | $progressActivity = "Deleting items" 455 | $itemId = New-Object Microsoft.Exchange.WebServices.Data.ItemId("xx") 456 | $itemIdType = [Type] $itemId.GetType() 457 | $genericItemIdList = [System.Collections.Generic.List``1].MakeGenericType(@($itemIdType)) 458 | 459 | $finished = $false 460 | $totalItems = $ItemsToDelete.Count 461 | Write-Progress -Activity $progressActivity -Status "0% complete" -PercentComplete 0 462 | 463 | $consecutiveErrors = 0 464 | 465 | while ( !$finished ) 466 | { 467 | $deleteIds = [Activator]::CreateInstance($genericItemIdList) 468 | 469 | for ([int]$i=0; $i -lt $BatchSize; $i++) 470 | { 471 | if ($ItemsToDelete[$i] -ne $null) 472 | { 473 | $deleteIds.Add($ItemsToDelete[$i]) 474 | } 475 | if ($i -ge $ItemsToDelete.Count) 476 | { break } 477 | } 478 | 479 | $results = $null 480 | try 481 | { 482 | LogVerbose "Sending batch request to delete $($deleteIds.Count) items ($($ItemsToDelete.Count) remaining)" 483 | $results = $script:Service.DeleteItems( $deleteIds, [Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete, [Microsoft.Exchange.WebServices.Data.SendCancellationsMode]::SendToNone, $null ) 484 | } 485 | catch 486 | { 487 | try 488 | { 489 | Log "Unexpected error: $($Error[0].Exception.InnerException.ToString())" Red 490 | } 491 | catch 492 | { 493 | Log "Unexpected error: $($Error[1])" Red 494 | } 495 | } 496 | 497 | $ItemsToDelete = RemoveProcessedItemsFromList $deleteIds $results $ItemsToDelete 498 | 499 | $percentComplete = ( ($totalItems - $ItemsToDelete.Count) / $totalItems ) * 100 500 | Write-Progress -Activity $progressActivity -Status "$percentComplete% complete" -PercentComplete $percentComplete 501 | 502 | if ($ItemsToDelete.Count -eq 0) 503 | { 504 | $finished = $True 505 | } 506 | } 507 | Write-Progress -Activity $progressActivity -Status "Complete" -Completed 508 | } 509 | 510 | Function EWSPropertyType($MAPIPropertyType) 511 | { 512 | # Return the EWS property type for the given MAPI Property value 513 | 514 | switch ([Convert]::ToInt32($MAPIPropertyType,16)) 515 | { 516 | 0x0 { return $Null } 517 | 0x1 { return $Null } 518 | 0x2 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Short } 519 | 0x1002 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::ShortArray } 520 | 0x3 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer } 521 | 0x1003 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::IntegerArray } 522 | 0x4 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Float } 523 | 0x1004 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::FloatArray } 524 | 0x5 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Double } 525 | 0x1005 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::DoubleArray } 526 | 0x6 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Currency } 527 | 0x1006 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::CurrencyArray } 528 | 0x7 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::ApplicationTime } 529 | 0x1007 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::ApplicationTimeArray } 530 | 0x0A { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Error } 531 | 0x0B { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Boolean } 532 | 0x0D { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Object } 533 | 0x100D { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::ObjectArray } 534 | 0x14 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Long } 535 | 0x1014 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::LongArray } 536 | 0x1E { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String } 537 | 0x101E { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::StringArray } 538 | 0x1F { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String } 539 | 0x101F { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::StringArray } 540 | 0x40 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::SystemTime } 541 | 0x1040 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::SystemTimeArray } 542 | 0x48 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::CLSID } 543 | 0x1048 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::CLSIDArray } 544 | 0x102 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary } 545 | 0x1102 { return [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::BinaryArray } 546 | } 547 | Write-Verbose "Couldn't match MAPI property type" 548 | return $null 549 | } 550 | 551 | function GetExtendedPropertyDefinition($guid, $name, $mapiType) 552 | { 553 | # Return an EWS ExtendedPropertyDefinition for the given MAPI property 554 | 555 | return new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition($( new-object System.Guid($guid) ), $name, $( EWSPropertyType $mapiType )) 556 | } 557 | 558 | function ProcessMailbox() 559 | { 560 | # Process the mailbox 561 | Write-Host ([string]::Format("Processing mailbox {0}", $Mailbox)) -ForegroundColor Gray 562 | 563 | # Bind to the mailbox 564 | $script:Service = CreateService($Mailbox) 565 | if ( $script:Service -eq $null ) 566 | { 567 | Write-Host "Failed to create ExchangeService" -ForegroundColor Red 568 | exit 569 | } 570 | $entryIdsBatch = @() 571 | $activity = "Deleting named properties from items" 572 | if ($DeleteItems) 573 | { 574 | if ($Batch) 575 | { 576 | $activity = "Collating items to delete" 577 | } 578 | else 579 | { 580 | $activity = "Deleting items" 581 | } 582 | } 583 | $itemsProcessed = 0 584 | 585 | # Now we process our list of EntryIds and delete the items 586 | ForEach ($entryIdElement in $EntryIds) 587 | { 588 | Write-Progress -Activity $activity -Status "Processing item" -PercentComplete (($itemsProcessed++/$($EntryIds.Count))*100) 589 | if (!$entryIdElement.Contains("@")) # A real EntryId does not contain @ 590 | { 591 | $entryId = $null 592 | $namedProps = $null 593 | if ($entryIdElement.Contains(";")) 594 | { 595 | # EntryId has named property information 596 | $entryId, $namedProps = $entryIdElement -split ";" 597 | } 598 | else 599 | { 600 | # Seems to be just an EntryId 601 | $entryId = $entryIdElement 602 | } 603 | 604 | if ($entryId) 605 | { 606 | Write-Progress -Activity $activity -Status "Processing item $entryId" -PercentComplete (($itemsProcessed/$($EntryIds.Count))*100) 607 | LogVerbose "Converting EntryId to EwsId: $entryId" 608 | $ewsId = ConvertId($entryId) 609 | $basePropset = New-Object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly, [Microsoft.Exchange.WebServices.Data.ItemSchema]::ItemClass, [Microsoft.Exchange.WebServices.Data.ItemSchema]::Subject) 610 | 611 | if ($ewsId) 612 | { 613 | if ($DeleteItems) 614 | { 615 | # We are deleting the items (not just the named properties) 616 | if ($Batch) 617 | { 618 | # We're batching the items to delete 619 | $entryIdsBatch += $ewsId.UniqueId 620 | Log "Adding item to delete list: $($ewsId.UniqueId)" 621 | } 622 | else 623 | { 624 | LogVerbose "Binding to item: $($ewsId.UniqueId)" 625 | $item = $null 626 | $item = [Microsoft.Exchange.WebServices.Data.Item]::Bind($script:Service, $ewsId.UniqueId, $basePropset) 627 | if ($item) 628 | { 629 | Log "Deleting item: $($item.Subject) ($($ewsId.UniqueId))" 630 | try 631 | { 632 | if ($item.ItemClass.StartsWith("IPM.Appointment")) 633 | { 634 | # This is an appointment, so we hard delete and suppress notifications to attendees 635 | [Microsoft.Exchange.WebServices.Data.Appointment]$apt = $item 636 | $apt.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete, [Microsoft.Exchange.WebServices.Data.SendCancellationsMode]::SendToNone) 637 | } 638 | else 639 | { 640 | $item.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete, $true) 641 | } 642 | } 643 | catch {} 644 | ReportError "Deleting item" 645 | } 646 | } 647 | } 648 | else 649 | { 650 | # We are just deleting the named properties (we do this one item at a time, not in batches [yet]) 651 | $item = $null 652 | $extendedProperties = @() 653 | $propset = $basePropset 654 | foreach ($namedProp in $namedProps) 655 | { 656 | try 657 | { 658 | LogVerbose "Parsing property: $namedProp" 659 | $namedPropElements = $namedProp -split "/" 660 | if ($namedPropElements.Count -eq 3) 661 | { 662 | # Named prop should be in format guid/name/type 663 | $ewsPropDef = $( GetExtendedPropertyDefinition $namedPropElements[0] $namedPropElements[1] $namedPropElements[2] ) 664 | $extendedProperties += $ewsPropDef 665 | $propset.Add($ewsPropDef) 666 | } 667 | } 668 | catch {} 669 | ReportError "Parsing property" 670 | } 671 | 672 | if ( $extendedProperties.Count -gt 0 ) 673 | { 674 | LogVerbose "Binding to item: $($ewsId.UniqueId)" 675 | $item = [Microsoft.Exchange.WebServices.Data.Item]::Bind($script:Service, $ewsId.UniqueId, $propset) 676 | 677 | if ($item) 678 | { 679 | Log "Deleting properties from item: $($item.Subject) ($($ewsId.UniqueId))" 680 | try 681 | { 682 | foreach ($extendedProperty in $extendedProperties) 683 | { 684 | [void]$item.RemoveExtendedProperty($extendedProperty) 685 | } 686 | [void]$item.Update([Microsoft.Exchange.WebServices.Data.ConflictResolutionMode]::AlwaysOverwrite, $true) 687 | } 688 | catch {} 689 | ReportError "Delete properties" 690 | } 691 | } 692 | } 693 | } 694 | else 695 | { 696 | Log "Failed to convert EntryId to EWS Id: $entryId" Red 697 | } 698 | } 699 | else 700 | { 701 | LogVerbose "Invalid EntryId ignored" 702 | } 703 | } 704 | } 705 | Write-Progress -Activity $activity -Completed 706 | 707 | if ($Batch) 708 | { 709 | if ($Force) 710 | { 711 | Log "Delete list contains $($entryIdsBatch.Count) items. Starting batch delete." Yellow 712 | BatchDelete $entryIdsBatch 713 | } 714 | else 715 | { 716 | Log "Delete list contains $($entryIdsBatch.Count) items. Batch delete not processed as -Force not specified" Green 717 | } 718 | } 719 | } 720 | 721 | # The following is the main script 722 | 723 | # Check if we need to ignore any certificate errors 724 | # This needs to be done *before* the managed API is loaded, otherwise it doesn't work consistently (i.e. usually doesn't!) 725 | if ($IgnoreSSLCertificate) 726 | { 727 | Log "WARNING: Ignoring any SSL certificate errors" Yellow 728 | TrustAllCerts 729 | ErrorReported | out-null 730 | } 731 | 732 | # Load EWS Managed API 733 | if (!(LoadEWSManagedAPI)) 734 | { 735 | Log "Failed to locate EWS Managed API, cannot continue" Red 736 | Exit 737 | } 738 | 739 | Write-Host "" 740 | 741 | # Check whether we have a file as input... 742 | $FileExists = Test-Path $EntryIds 743 | If ( $FileExists ) 744 | { 745 | # Import the EntryIds from a text file 746 | LogVerbose "Reading EntryIDs from file: $EntryIds" 747 | $EntryIds = Get-Content -Path $EntryIds 748 | } 749 | 750 | # Check we have a valid mailbox (either specified, or first entry of the EntryIds list) 751 | if ($EntryIds[0]) 752 | { 753 | if ($EntryIds[0].Contains("@")) 754 | { 755 | if ( [String]::IsNullOrEmpty($Mailbox) -or ($Mailbox.ToLower().Equals($EntryIds[0].ToLower())) ) 756 | { 757 | # Mailbox is first EntryId 758 | LogVerbose "Mailbox specified in file: $($EntryIds[0])" 759 | $Mailbox = $EntryIds[0] 760 | if ($EntryIds.Count -lt 2) 761 | { 762 | Log "EntryIds file found, but no EntryIds were listed" Yellow 763 | Exit 764 | } 765 | } 766 | else 767 | { 768 | # The mailbox specified in -Mailbox parameter does not match that in the EntryIds list, so we fail here 769 | Log "Mailbox specified by parameter: $Mailbox" White 770 | Log "Mailbox specified in EntryIds list: $($EntryIds[0])" White 771 | Log "Mailbox mismatch between parameter and item list" Red 772 | Exit 773 | } 774 | } 775 | } 776 | else 777 | { 778 | if ($Error[0]) 779 | { 780 | Log "Error: $($Error[0])" 781 | } 782 | Log "No EntryIds to process" 783 | } 784 | 785 | if ( [string]::IsNullOrEmpty($Mailbox) ) 786 | { 787 | Log "Mailbox is required (but not specified)." -ForegroundColor Red 788 | Exit 789 | } 790 | 791 | # Process as single mailbox 792 | ProcessMailbox 793 | 794 | 795 | if ($script:Tracer -ne $null) 796 | { 797 | $script:Tracer.Close() 798 | } -------------------------------------------------------------------------------- /Legacy/Delete-ByInternetMessageId.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Delete-ByInternetMessageId.ps1 3 | # 4 | # By David Barrett, Microsoft Ltd. 2022. Use at your own risk. No warranties are given. 5 | # 6 | # DISCLAIMER: 7 | # THIS CODE IS SAMPLE CODE. THESE SAMPLES ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. 8 | # MICROSOFT FURTHER DISCLAIMS ALL IMPLIED WARRANTIES INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR OF FITNESS FOR 9 | # A PARTICULAR PURPOSE. THE ENTIRE RISK ARISING OUT OF THE USE OR PERFORMANCE OF THE SAMPLES REMAINS WITH YOU. IN NO EVENT SHALL 10 | # MICROSOFT OR ITS SUPPLIERS BE LIABLE FOR ANY DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, 11 | # BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE 12 | # SAMPLES, EVEN IF MICROSOFT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. BECAUSE SOME STATES DO NOT ALLOW THE EXCLUSION OR LIMITATION 13 | # OF LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE LIMITATION MAY NOT APPLY TO YOU. 14 | 15 | param ( 16 | [Parameter(Position=0,Mandatory=$True,HelpMessage="Specifies the mailbox (or mailboxes) to be searched.")] 17 | [ValidateNotNullOrEmpty()] 18 | $Mailbox, 19 | 20 | [Parameter(Mandatory=$True,HelpMessage="InternetMessageId being searched for")] 21 | [string]$InternetMessageId, 22 | 23 | [Parameter(Mandatory=$False,HelpMessage="If this switch is set, any matching items will be deleted (otherwise, no changes are made)")] 24 | [switch]$Delete, 25 | 26 | [Parameter(Mandatory=$False,HelpMessage="Credentials used to authenticate with EWS")] 27 | [System.Management.Automation.PSCredential]$Credentials, 28 | 29 | [Parameter(Mandatory=$False,HelpMessage="If set, then we will use OAuth to access the mailbox (required for MFA enabled accounts).")] 30 | [switch]$OAuth, 31 | 32 | [Parameter(Mandatory=$False,HelpMessage="The client Id that this script will identify as. Must be registered in Azure AD.")] 33 | [string]$OAuthClientId = "", 34 | 35 | [Parameter(Mandatory=$False,HelpMessage="The tenant Id in which the application is registered. If missing, application is assumed to be multi-tenant and the common log-in URL will be used.")] 36 | [string]$OAuthTenantId = "", 37 | 38 | [Parameter(Mandatory=$False,HelpMessage="The redirect Uri of the Azure registered application.")] 39 | [string]$OAuthRedirectUri = "http://localhost/code", 40 | 41 | [Parameter(Mandatory=$False,HelpMessage="If using application permissions, specify the secret key OR certificate.")] 42 | [string]$OAuthSecretKey = "", 43 | 44 | [Parameter(Mandatory=$False,HelpMessage="If using application permissions, specify the secret key OR certificate. Please note that certificate auth requires the MSAL dll to be available.")] 45 | $OAuthCertificate = $null, 46 | 47 | [Parameter(Mandatory=$False,HelpMessage="Whether we are using impersonation to access the mailbox.")] 48 | [switch]$Impersonate, 49 | 50 | [Parameter(Mandatory=$False,HelpMessage="EWS Url (if omitted, then autodiscover is used)")] 51 | [string]$EwsUrl, 52 | 53 | [Parameter(Mandatory=$False,HelpMessage="If specified, requests are directed to Office 365 endpoint (this overrides -EwsUrl)")] 54 | [switch]$Office365, 55 | 56 | [Parameter(Mandatory=$False,HelpMessage="Path to managed API (if omitted, a search of standard paths is performed)")] 57 | [string]$EWSManagedApiPath = "", 58 | 59 | [Parameter(Mandatory=$False,HelpMessage="Whether to ignore any SSL errors (e.g. invalid certificate)")] 60 | [switch]$IgnoreSSLCertificate, 61 | 62 | [Parameter(Mandatory=$False,HelpMessage="Whether to allow insecure redirects when performing autodiscover")] 63 | [switch]$AllowInsecureRedirection, 64 | 65 | [Parameter(Mandatory=$False,HelpMessage="Trace file - if specified, EWS tracing information is written to this file")] 66 | [string]$TraceFile 67 | ) 68 | $script:ScriptVersion = "1.0.2" 69 | 70 | 71 | function LoadLibraries() 72 | { 73 | param ( 74 | [bool]$searchProgramFiles, 75 | $dllNames, 76 | [ref]$dllLocations = @() 77 | ) 78 | # Attempt to find and load the specified libraries 79 | 80 | foreach ($dllName in $dllNames) 81 | { 82 | # First check if the dll is in current directory 83 | Write-Verbose "Searching for DLL: $dllName" 84 | $dll = $null 85 | try 86 | { 87 | $dll = Get-ChildItem $dllName -ErrorAction SilentlyContinue 88 | } 89 | catch {} 90 | 91 | if ($searchProgramFiles) 92 | { 93 | if ($dll -eq $null) 94 | { 95 | $dll = Get-ChildItem -Recurse "C:\Program Files (x86)" -ErrorAction SilentlyContinue | Where-Object { ($_.PSIsContainer -eq $false) -and ( $_.Name -eq $dllName ) } 96 | if (!$dll) 97 | { 98 | $dll = Get-ChildItem -Recurse "C:\Program Files" -ErrorAction SilentlyContinue | Where-Object { ($_.PSIsContainer -eq $false) -and ( $_.Name -eq $dllName ) } 99 | } 100 | } 101 | } 102 | $script:LastError = $Error[0] # We do this to suppress any errors encountered during the search above 103 | 104 | if ($dll -eq $null) 105 | { 106 | Write-Host "Unable to load locate $dll" -ForegroundColor Red 107 | return $false 108 | } 109 | else 110 | { 111 | try 112 | { 113 | Write-Verbose ([string]::Format("Loading {2} v{0} found at: {1}", $dll.VersionInfo.FileVersion, $dll.VersionInfo.FileName, $dllName)) 114 | Add-Type -Path $dll.VersionInfo.FileName 115 | if ($dllLocations) 116 | { 117 | $dllLocations.value += $dll.VersionInfo.FileName 118 | } 119 | } 120 | catch 121 | { 122 | return $false 123 | } 124 | } 125 | } 126 | return $true 127 | } 128 | 129 | function GetTokenWithCertificate 130 | { 131 | # We use MSAL with certificate auth 132 | if (!script:msalApiLoaded) 133 | { 134 | $msalLocation = @() 135 | $script:msalApiLoaded = $(LoadLibraries -searchProgramFiles $false -dllNames @("Microsoft.Identity.Client.dll") -dllLocations ([ref]$msalLocation)) 136 | if (!$script:msalApiLoaded) 137 | { 138 | Write-Host "Failed to load MSAL. Cannot continue with certificate authentication." -ForegroundColor Red 139 | exit 140 | } 141 | } 142 | 143 | $cca1 = [Microsoft.Identity.Client.ConfidentialClientApplicationBuilder]::Create($OAuthClientId) 144 | $cca2 = $cca1.WithCertificate($OAuthCertificate) 145 | $cca3 = $cca2.WithTenantId($OAuthTenantId) 146 | $cca = $cca3.Build() 147 | 148 | $scopes = New-Object System.Collections.Generic.List[string] 149 | $scopes.Add("https://outlook.office365.com/.default") 150 | $acquire = $cca.AcquireTokenForClient($scopes) 151 | $authResult = $acquire.ExecuteAsync().Result 152 | $script:oauthToken = $authResult 153 | $script:oAuthAccessToken = $script:oAuthToken.AccessToken 154 | } 155 | 156 | function GetTokenViaCode 157 | { 158 | # Acquire auth code (needed to request token) 159 | $authUrl = "https://login.microsoftonline.com/$OAuthTenantId/oauth2/v2.0/authorize?client_id=$OAuthClientId&response_type=code&redirect_uri=$OAuthRedirectUri&response_mode=query&scope=openid%20profile%20email%20offline_access%20https://outlook.office365.com/.default" 160 | Write-Host "Please complete log-in via the web browser, and then paste the redirect URL (including auth code) here to continue" -ForegroundColor Green 161 | Start-Process $authUrl 162 | 163 | $authcode = Read-Host "Auth code" 164 | $codeStart = $authcode.IndexOf("?code=") 165 | if ($codeStart -gt 0) 166 | { 167 | $authcode = $authcode.Substring($codeStart+6) 168 | } 169 | $codeEnd = $authcode.IndexOf("&session_state=") 170 | if ($codeEnd -gt 0) 171 | { 172 | $authcode = $authcode.Substring(0, $codeEnd) 173 | } 174 | Write-Verbose "Using auth code: $authcode" 175 | 176 | # Acquire token (using the auth code) 177 | $body = @{grant_type="authorization_code";scope="https://outlook.office365.com/.default";client_id=$OAuthClientId;code=$authcode;redirect_uri=$OAuthRedirectUri} 178 | try 179 | { 180 | $script:oauthToken = Invoke-RestMethod -Method Post -Uri https://login.microsoftonline.com/$OAuthTenantId/oauth2/v2.0/token -Body $body 181 | $script:oAuthAccessToken = $script:oAuthToken.access_token 182 | $script:oauthTokenAcquireTime = [DateTime]::UtcNow 183 | } 184 | catch 185 | { 186 | Write-Host "Failed to obtain OAuth token" -ForegroundColor Red 187 | exit # Failed to obtain a token 188 | } 189 | } 190 | 191 | function GetTokenWithKey 192 | { 193 | $Body = @{ 194 | "grant_type" = "client_credentials"; 195 | "client_id" = "$OAuthClientId"; 196 | "client_secret" = "$OAuthSecretKey"; 197 | "scope" = "https://outlook.office365.com/.default" 198 | } 199 | 200 | try 201 | { 202 | $script:oAuthToken = Invoke-RestMethod -Method POST -uri "https://login.microsoftonline.com/$OAuthTenantId/oauth2/v2.0/token" -Body $body 203 | $script:oAuthAccessToken = $script:oAuthToken.access_token 204 | $script:oauthTokenAcquireTime = [DateTime]::UtcNow 205 | } 206 | catch 207 | { 208 | Write-Host "Failed to obtain OAuth token: $Error" -ForegroundColor Red 209 | exit # Failed to obtain a token 210 | } 211 | } 212 | 213 | function GetOAuthCredentials 214 | { 215 | # Obtain OAuth token for accessing mailbox 216 | param ( 217 | [switch]$RenewToken 218 | ) 219 | $exchangeCredentials = $null 220 | 221 | if ($script:oauthToken -ne $null) 222 | { 223 | # We already have a token 224 | if ($script:oauthTokenAcquireTime.AddSeconds($script:oauthToken.expires_in) -gt [DateTime]::UtcNow.AddMinutes(1)) 225 | { 226 | # Token still valid, so return that 227 | $exchangeCredentials = New-Object Microsoft.Exchange.WebServices.Data.OAuthCredentials($script:oAuthAccessToken) 228 | return $exchangeCredentials 229 | } 230 | 231 | # Token needs renewing 232 | 233 | } 234 | 235 | if (![String]::IsNullOrEmpty($OAuthSecretKey)) 236 | { 237 | GetTokenWithKey 238 | } 239 | elseif ($OAuthCertificate -ne $null) 240 | { 241 | GetTokenWithCertificate 242 | } 243 | else 244 | { 245 | GetTokenViaCode 246 | } 247 | 248 | # If we get here we have a valid token 249 | $exchangeCredentials = New-Object Microsoft.Exchange.WebServices.Data.OAuthCredentials($script:oAuthAccessToken) 250 | return $exchangeCredentials 251 | } 252 | 253 | function ApplyEWSOAuthCredentials 254 | { 255 | # Apply EWS OAuth credentials to all our service objects 256 | 257 | if ( -not $OAuth ) { return } 258 | if ( $script:services -eq $null ) { return } 259 | if ( $script:services.Count -lt 1 ) { return } 260 | if ( $script:oauthTokenAcquireTime.AddSeconds($script:oauthToken.expires_in) -gt [DateTime]::UtcNow.AddMinutes(1)) { return } 261 | 262 | # The token has expired and needs refreshing 263 | Write-Verbose "OAuth access token invalid, attempting to renew" 264 | $exchangeCredentials = GetOAuthCredentials -RenewToken 265 | if ($exchangeCredentials -eq $null) { return } 266 | if ( $script:oauthTokenAcquireTime.AddSeconds($script:oauthToken.expires_in) -le [DateTime]::Now ) 267 | { 268 | Write-Host "OAuth Token renewal failed" -ForegroundColor Red 269 | exit # We no longer have access to the mailbox, so we stop here 270 | } 271 | 272 | Write-Host "OAuth token successfully renewed; new expiry: $($script:oAuthToken.ExpiresOn)" -ForegroundColor Green 273 | if ($script:services.Count -gt 0) 274 | { 275 | foreach ($service in $script:services.Values) 276 | { 277 | $service.Credentials = New-Object Microsoft.Exchange.WebServices.Data.OAuthCredentials($exchangeCredentials) 278 | } 279 | Write-Verbose "Updated OAuth token for $($script.services.Count) ExchangeService object(s)" 280 | } 281 | } 282 | 283 | Function LoadEWSManagedAPI 284 | { 285 | # Find and load the managed API 286 | $ewsApiLocation = @() 287 | $ewsApiLoaded = $(LoadLibraries -searchProgramFiles $true -dllNames @("Microsoft.Exchange.WebServices.dll") -dllLocations ([ref]$ewsApiLocation)) 288 | 289 | if (!$ewsApiLoaded) 290 | { 291 | # Failed to load the EWS API, so try to install it from Nuget 292 | $ewsapi = Find-Package "Exchange.WebServices.Managed.Api" 293 | if ($ewsapi.Entities.Name.Equals("Microsoft")) 294 | { 295 | # We have found EWS API package, so install as current user (confirm with user first) 296 | Write-Host "EWS Managed API is not installed, but is available from Nuget. Install now for current user (required for this script to continue)? (Y/n)" -ForegroundColor Yellow 297 | $response = Read-Host 298 | if ( $response.ToLower().Equals("y") ) 299 | { 300 | Install-Package $ewsapi -Scope CurrentUser -Force 301 | $ewsApiLoaded = $(LoadLibraries -searchProgramFiles $true -dllNames @("Microsoft.Exchange.WebServices.dll") -dllLocations ([ref]$ewsApiLocation)) 302 | } 303 | } 304 | } 305 | 306 | if ($ewsApiLoaded) 307 | { 308 | if ($ewsApiLocation[0]) 309 | { 310 | Write-Host "Using EWS Managed API found at: $($ewsApiLocation[0])" -ForegroundColor Gray 311 | $script:EWSManagedApiPath = $ewsApiLocation[0] 312 | } 313 | else 314 | { 315 | Write-Host "Failed to read EWS API location: $ewsApiLocation" 316 | Exit 317 | } 318 | } 319 | 320 | return $ewsApiLoaded 321 | } 322 | 323 | Function CreateTraceListener($service) 324 | { 325 | # Create trace listener to capture EWS conversation (useful for debugging) 326 | 327 | if ([String]::IsNullOrEmpty($EWSManagedApiPath)) 328 | { 329 | Write-Host "Managed API path missing; unable to create tracer" -ForegroundColor Red 330 | Exit 331 | } 332 | 333 | if ($script:Tracer -eq $null) 334 | { 335 | $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider 336 | $Params=New-Object System.CodeDom.Compiler.CompilerParameters 337 | $Params.GenerateExecutable=$False 338 | $Params.GenerateInMemory=$True 339 | $Params.IncludeDebugInformation=$False 340 | $Params.ReferencedAssemblies.Add("System.dll") | Out-Null 341 | $Params.ReferencedAssemblies.Add($EWSManagedApiPath) | Out-Null 342 | 343 | $traceFileForCode = $traceFile.Replace("\", "\\") 344 | 345 | if (![String]::IsNullOrEmpty($TraceFile)) 346 | { 347 | Write-Host "Tracing to: $TraceFile" 348 | } 349 | 350 | $TraceListenerClass = @" 351 | using System; 352 | using System.Text; 353 | using System.IO; 354 | using System.Threading; 355 | using Microsoft.Exchange.WebServices.Data; 356 | 357 | namespace TraceListener { 358 | class EWSTracer: Microsoft.Exchange.WebServices.Data.ITraceListener 359 | { 360 | private StreamWriter _traceStream = null; 361 | private string _lastResponse = String.Empty; 362 | 363 | public EWSTracer() 364 | { 365 | try 366 | { 367 | _traceStream = File.AppendText("$traceFileForCode"); 368 | } 369 | catch { } 370 | } 371 | 372 | ~EWSTracer() 373 | { 374 | Close(); 375 | } 376 | 377 | public void Close() 378 | { 379 | try 380 | { 381 | _traceStream.Flush(); 382 | _traceStream.Close(); 383 | } 384 | catch { } 385 | } 386 | 387 | 388 | public void Trace(string traceType, string traceMessage) 389 | { 390 | if ( traceType.Equals("EwsResponse") ) 391 | _lastResponse = traceMessage; 392 | 393 | if ( traceType.Equals("EwsRequest") ) 394 | _lastResponse = String.Empty; 395 | 396 | if (_traceStream == null) 397 | return; 398 | 399 | lock (this) 400 | { 401 | try 402 | { 403 | _traceStream.WriteLine(traceMessage); 404 | _traceStream.Flush(); 405 | } 406 | catch { } 407 | } 408 | } 409 | 410 | public string LastResponse 411 | { 412 | get { return _lastResponse; } 413 | } 414 | } 415 | } 416 | "@ 417 | 418 | $script:Tracer = $null 419 | $TraceCompilation=$Provider.CompileAssemblyFromSource($Params,$TraceListenerClass) 420 | If ($TraceCompilation) 421 | { 422 | $TraceAssembly=$TraceCompilation.CompiledAssembly 423 | $script:Tracer=$TraceAssembly.CreateInstance("TraceListener.EWSTracer") 424 | # Attach the trace listener to the Exchange service 425 | $service.TraceListener = $script:Tracer 426 | } 427 | } 428 | } 429 | 430 | function CreateService($smtpAddress) 431 | { 432 | # Creates and returns an ExchangeService object to be used to access mailboxes 433 | 434 | # First of all check to see if we have a service object for this mailbox already 435 | if ($script:services -eq $null) 436 | { 437 | $script:services = @{} 438 | } 439 | if ($script:services.ContainsKey($smtpAddress)) 440 | { 441 | return $script:services[$smtpAddress] 442 | } 443 | 444 | # Create new service 445 | $exchangeService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013) 446 | 447 | # Do we need to use OAuth? 448 | if ($OAuth) 449 | { 450 | $exchangeService.Credentials = GetOAuthCredentials 451 | if ($exchangeService.Credentials -eq $null) 452 | { 453 | # OAuth failed 454 | return $null 455 | } 456 | } 457 | else 458 | { 459 | # Set credentials if specified, or use logged on user. 460 | if ($Credentials -ne $Null) 461 | { 462 | Write-Verbose "Applying given credentials: $($Credentials.UserName)" 463 | $exchangeService.Credentials = $Credentials.GetNetworkCredential() 464 | } 465 | else 466 | { 467 | Write-Verbose "Using default credentials" 468 | $exchangeService.UseDefaultCredentials = $true 469 | } 470 | } 471 | 472 | 473 | 474 | # Set EWS URL if specified, or use autodiscover if no URL specified. 475 | if ($EwsUrl -or $Office365) 476 | { 477 | if ($Office365) { $EwsUrl = "https://outlook.office365.com/EWS/Exchange.asmx" } 478 | $exchangeService.URL = New-Object Uri($EwsUrl) 479 | } 480 | else 481 | { 482 | try 483 | { 484 | Write-Verbose "Performing autodiscover for $smtpAddress" 485 | if ( $AllowInsecureRedirection ) 486 | { 487 | $exchangeService.AutodiscoverUrl($smtpAddress, {$True}) 488 | } 489 | else 490 | { 491 | $exchangeService.AutodiscoverUrl($smtpAddress) 492 | } 493 | if ([string]::IsNullOrEmpty($exchangeService.Url)) 494 | { 495 | Write-Host "$smtpAddress : autodiscover failed" -ForegroundColor Red 496 | return $Null 497 | } 498 | Write-Verbose "EWS Url found: $($exchangeService.Url)" 499 | } 500 | catch 501 | { 502 | Write-Host "$smtpAddress : error occurred during autodiscover: $($Error[0])" -ForegroundColor Red 503 | return $null 504 | } 505 | } 506 | 507 | $exchangeService.HttpHeaders.Add("X-AnchorMailbox", $smtpAddress) 508 | if ($Impersonate) 509 | { 510 | $exchangeService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $smtpAddress) 511 | } 512 | 513 | # We enable tracing so that we can retrieve the last response (and read any throttling information from it - this isn't exposed in the EWS Managed API) 514 | if (![String]::IsNullOrEmpty($EWSManagedApiPath)) 515 | { 516 | CreateTraceListener $exchangeService 517 | if ($script:Tracer) 518 | { 519 | $exchangeService.TraceFlags = [Microsoft.Exchange.WebServices.Data.TraceFlags]::All 520 | $exchangeService.TraceEnabled = $True 521 | } 522 | else 523 | { 524 | Write-Host "Failed to create EWS trace listener. Throttling back-off time won't be detected." -ForegroundColor Yellow 525 | } 526 | } 527 | 528 | $script:services.Add($smtpAddress, $exchangeService) 529 | Write-Verbose "Currently caching $($script:services.Count) ExchangeService objects" 530 | return $exchangeService 531 | } 532 | 533 | Function ProcessFolder() 534 | { 535 | # Process this folder 536 | 537 | $Folder = $args[0] 538 | if ($Folder -eq $null) 539 | { 540 | throw "No folder specified" 541 | } 542 | 543 | Write-Verbose "Reading subfolders of: $($Folder.DisplayName)" 544 | 545 | # Process any subfolders 546 | if ($Folder.ChildFolderCount -gt 0) 547 | { 548 | # We read the list of all folders first, so that we have the complete list before any processing 549 | $subfolders = @() 550 | $moreFolders = $True 551 | $FolderView = New-Object Microsoft.Exchange.WebServices.Data.FolderView(500) 552 | while ($moreFolders) 553 | { 554 | ApplyEWSOAuthCredentials 555 | $FindFoldersResults = $Folder.FindFolders($FolderView) 556 | $subfolders += $FindFoldersResults.Folders 557 | $moreFolders = $FindFoldersResults.MoreAvailable 558 | $FolderView.Offset += 500 559 | } 560 | # Process the subfolders 561 | if ($subfolders.Count -gt 0) 562 | { 563 | Write-Verbose "$($Folder.DisplayName) contains $($subfolders.Count) subfolders" 564 | ForEach ($subFolder in $subfolders) 565 | { 566 | ProcessFolder $subFolder 567 | } 568 | } 569 | } 570 | 571 | # Search for the item with given InternetMessageId 572 | Write-Verbose "Searching for message in: $($Folder.DisplayName)" 573 | $Offset=0 574 | $PageSize=1000 575 | $MoreItems=$true 576 | 577 | $SearchFilter = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::InternetMessageId, $InternetMessageId) 578 | $deletedFromFolder = 0 579 | 580 | while ($MoreItems) 581 | { 582 | $View = New-Object Microsoft.Exchange.WebServices.Data.ItemView($PageSize, $Offset, [Microsoft.Exchange.Webservices.Data.OffsetBasePoint]::Beginning) 583 | $View.PropertySet = [Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly 584 | 585 | try 586 | { 587 | $FindResults=$Folder.FindItems($SearchFilter, $View) 588 | $Offset+=$PageSize 589 | $MoreItems = $FindResults.MoreAvailable 590 | if ($FindResults) 591 | { 592 | foreach ($item in $FindResults.Items) 593 | { 594 | if ($Delete) 595 | { 596 | $item.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete) 597 | } 598 | $deletedFromFolder++ 599 | } 600 | } 601 | } 602 | catch 603 | { 604 | $MoreItems = $false 605 | } 606 | 607 | } 608 | if ($deletedFromFolder -gt 0) 609 | { 610 | if ($Delete) 611 | { 612 | Write-Host "$deletedFromFolder items matched and were deleted from folder $($Folder.DisplayName)" -ForegroundColor Green 613 | } 614 | else 615 | { 616 | Write-Host "$deletedFromFolder items matched in folder $($Folder.DisplayName)" -ForegroundColor Green 617 | } 618 | } 619 | else 620 | { 621 | Write-Verbose "0 items matched in folder $($Folder.DisplayName)" 622 | } 623 | $script:itemsDeleted += $deletedFromFolder 624 | } 625 | 626 | function ProcessMailbox($TargetMailbox) 627 | { 628 | # Process the mailbox 629 | 630 | Write-Host ([string]::Format("Processing mailbox {0}", $TargetMailbox)) -ForegroundColor Gray 631 | 632 | $script:Service = CreateService($TargetMailbox) 633 | if ($script:Service -eq $Null) 634 | { 635 | Write-Host "Failed to connect to mailbox $TargetMailbox" -ForegroundColor Red 636 | return 637 | } 638 | 639 | # Bind to mailbox root folder 640 | $mbx = New-Object Microsoft.Exchange.WebServices.Data.Mailbox( $TargetMailbox ) 641 | $folderId = New-Object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot, $mbx ) 642 | $mailboxRoot = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($script:service, $folderId) 643 | 644 | if ( $mailboxRoot -eq $null ) 645 | { 646 | Write-Host "Failed to open message store ($TargetMailbox)" -ForegroundColor Red 647 | if ($Impersonate) 648 | { 649 | Write-Host "Please check that you have impersonation permissions" -ForegroundColor Yellow 650 | } 651 | return 652 | } 653 | 654 | # To search the whole mailbox, we need to recurse each folder and perform the search. We recurse from the root folder to do this. 655 | $script:itemsDeleted = 0 656 | ProcessFolder $mailboxRoot 657 | 658 | if ($Delete) 659 | { 660 | Write-Host "$($script:itemsDeleted) items matched and were deleted from $TargetMailbox" -ForegroundColor Green 661 | } 662 | else 663 | { 664 | Write-Host "$($script:itemsDeleted) items matched (but were not deleted as -Delete not set) from $TargetMailbox" -ForegroundColor Yellow 665 | } 666 | } 667 | 668 | 669 | # The following is the main script 670 | 671 | 672 | # Check if we need to ignore any certificate errors 673 | # This needs to be done *before* the managed API is loaded, otherwise it doesn't work consistently (i.e. usually doesn't!) 674 | if ($IgnoreSSLCertificate) 675 | { 676 | Write-Host "WARNING: Ignoring any SSL certificate errors" -foregroundColor Yellow 677 | TrustAllCerts 678 | } 679 | 680 | # Load EWS Managed API 681 | if (!(LoadEWSManagedAPI)) 682 | { 683 | Write-Host "Failed to locate EWS Managed API, cannot continue" -ForegroundColor Red 684 | Exit 685 | } 686 | 687 | 688 | # Check whether we have a CSV file as input... 689 | $FileExists = Test-Path $Mailbox 690 | If ( $FileExists ) 691 | { 692 | # We have a CSV to process 693 | LogVerbose "Reading mailboxes from CSV file" 694 | $csv = Import-CSV $SourceMailbox -Header "PrimarySmtpAddress" 695 | foreach ($entry in $csv) 696 | { 697 | LogVerbose $entry.PrimarySmtpAddress 698 | if (![String]::IsNullOrEmpty($entry.PrimarySmtpAddress)) 699 | { 700 | if (!$entry.PrimarySmtpAddress.ToLower().Equals("primarysmtpaddress")) 701 | { 702 | $Mailbox = $entry.PrimarySmtpAddress 703 | ProcessMailbox $Mailbox 704 | } 705 | } 706 | } 707 | } 708 | Else 709 | { 710 | # Process as single mailbox 711 | ProcessMailbox $Mailbox 712 | } 713 | 714 | if ($script:Tracer -ne $null) 715 | { 716 | $script:Tracer.Close() 717 | } 718 | -------------------------------------------------------------------------------- /Legacy/Fix-DuplicateMailboxFolders.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Fix-DuplicateMailboxFolders.ps1 3 | # 4 | # By David Barrett, Microsoft Ltd. 2016. Use at your own risk. No warranties are given. 5 | # 6 | # DISCLAIMER: 7 | # THIS CODE IS SAMPLE CODE. THESE SAMPLES ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. 8 | # MICROSOFT FURTHER DISCLAIMS ALL IMPLIED WARRANTIES INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR OF FITNESS FOR 9 | # A PARTICULAR PURPOSE. THE ENTIRE RISK ARISING OUT OF THE USE OR PERFORMANCE OF THE SAMPLES REMAINS WITH YOU. IN NO EVENT SHALL 10 | # MICROSOFT OR ITS SUPPLIERS BE LIABLE FOR ANY DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, 11 | # BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE 12 | # SAMPLES, EVEN IF MICROSOFT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. BECAUSE SOME STATES DO NOT ALLOW THE EXCLUSION OR LIMITATION 13 | # OF LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE LIMITATION MAY NOT APPLY TO YOU. 14 | 15 | 16 | param ( 17 | [Parameter(Position=0,Mandatory=$False,HelpMessage="Specifies the mailbox to be accessed")] 18 | [ValidateNotNullOrEmpty()] 19 | [string]$Mailbox, 20 | 21 | [Parameter(Mandatory=$False,HelpMessage="Credentials used to authenticate with EWS (not required if -WhatIf is specified, or if default credentials are to be used). These credentials will also be used to import an Exchange PowerShell session, if necessary.")] 22 | [System.Management.Automation.PSCredential]$Credentials, 23 | 24 | [Parameter(Mandatory=$False,HelpMessage="This parameter can be used to control whether ApplicationImpersonation rights are needed to access the mailbox (default is TRUE)")] 25 | [bool]$Impersonate = $True, 26 | 27 | [Parameter(Mandatory=$False,HelpMessage="EWS Url (if blank, then autodiscover is used; if not specified then default Office 365 Url is used)")] 28 | [string]$EwsUrl = "https://outlook.office365.com/EWS/Exchange.asmx", 29 | 30 | [Parameter(Mandatory=$False,HelpMessage="PowerShell Url (default is Office 365 Url: https://ps.outlook.com/powershell/)")] 31 | [String]$PowerShellUrl = "https://ps.outlook.com/powershell/", 32 | 33 | [Parameter(Mandatory=$False,HelpMessage="Log file - activity is logged to this file if specified")] 34 | [string]$LogFile = "", 35 | 36 | [Parameter(Mandatory=$False,HelpMessage="If this parameter is present, then Merge-MailboxFolder.ps1 script will be called to attempt to eliminate the duplicate folder (by moving all items into the primary folder and then deleting the duplicate)")] 37 | [switch]$Repair 38 | ) 39 | 40 | Function Log([string]$Details, [ConsoleColor]$Colour) 41 | { 42 | if ($Colour -eq $null) 43 | { 44 | $Colour = [ConsoleColor]::White 45 | } 46 | Write-Host $Details -ForegroundColor $Colour 47 | if ( $LogFile -eq "" ) { return } 48 | $Details | Out-File $LogFile -Append 49 | } 50 | 51 | Function LogVerbose([string]$Details) 52 | { 53 | if ($VerbosePreference -eq "SilentlyContinue") { return } 54 | Write-Verbose $Details 55 | if ( $LogFile -eq "" ) { return } 56 | $Details | Out-File $LogFile -Append 57 | } 58 | 59 | Function CmdletsAvailable() 60 | { 61 | param ( 62 | $RequiredCmdlets, 63 | $Silent = $False 64 | ) 65 | 66 | $cmdletsAvailable = $True 67 | foreach ($cmdlet in $RequiredCmdlets) 68 | { 69 | if (Get-Command $cmdlet -ErrorAction SilentlyContinue) 70 | { 71 | } 72 | else 73 | { 74 | if (!$Silent) { Write-Host "Required cmdlet $cmdlet is not available" -ForegroundColor Red } 75 | $cmdletsAvailable = $False 76 | break 77 | } 78 | } 79 | 80 | return $cmdletsAvailable 81 | } 82 | 83 | Function ImportExchangeManagementSession() 84 | { 85 | param ( 86 | $RequiredCmdlets = "Get-Mailbox" 87 | ) 88 | 89 | # Check we have Exchange Management Session available. If not, we attempt to connect to and import one. 90 | if ( CmdletsAvailable $RequiredCmdlets $True ) 91 | { 92 | # Cmdlets we need are available, so no need to import any session 93 | return 94 | } 95 | 96 | if ([String]::IsNullOrEmpty($PowerShellUrl)) 97 | { 98 | Write-Host "PowerShell Url not specified and Exchange PowerShell session not available. Cannot continue." -ForegroundColor Red 99 | exit 100 | } 101 | 102 | Write-Host "Attempting to connect to and import Exchange Management session" -ForegroundColor Gray 103 | $global:session = $null 104 | if ($Credentials -eq $null) 105 | { 106 | # No credentials specified, so we attempt to connect without specifying them (which will attempt to authenticate as the logged on user) 107 | $global:session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri $PowerShellUrl -AllowRedirection 108 | } 109 | else 110 | { 111 | # We have credentials, so we use them - we only use basic auth if the Url is https 112 | if (!$PowerShellUrl.ToLower().StartsWith("https")) 113 | { 114 | $global:session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri $PowerShellUrl -Credential $Credentials -AllowRedirection 115 | } 116 | else 117 | { 118 | # With HTTPS we use basic auth, as this is required for Office 365 119 | $global:session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri $PowerShellUrl -Credential $Credentials -Authentication Basic -AllowRedirection 120 | } 121 | } 122 | 123 | if ($global:session -eq $null) 124 | { 125 | Write-Host "Failed to open Exchange Administration session, cannot continue" -ForegroundColor Red 126 | exit 127 | } 128 | Write-Host "Exchange PowerShell session successfully established" -ForegroundColor Green 129 | Import-PSSession $global:session 130 | 131 | # Now check that we have the cmdlets we need available 132 | if ( CmdletsAvailable($RequiredCmdlets) ) 133 | { 134 | return 135 | } 136 | 137 | exit 138 | } 139 | 140 | Function ConvertFolderIdToEntryId($folderId) 141 | { 142 | # Get-MailboxFolderStatistics returns a modified EntryId as the FolderId 143 | # We need to decode it, and remove the first and last bytes to convert it to 144 | # standard EntryId 145 | 146 | # Convert the id to a byte array 147 | $id = [System.Convert]::FromBase64String($folderId) 148 | Write-Host $id -ForegroundColor Gray 149 | 150 | # Create the real EntryId from the FolderId (i.e. copy everything except first and last bytes) 151 | [byte[]]$entryId = @() 152 | for ($i = 1; $i -lt $id.Length-1; $i++) 153 | { 154 | $entryId = $entryId + $id[$i] 155 | } 156 | 157 | # The id is now a standard EntryId, so just Base64 encode it again 158 | return [System.Convert]::ToBase64String($entryId) 159 | } 160 | 161 | Function ProcessFolder($realFolder) 162 | { 163 | # Fix the given folder. We find any other folders with the same name, move any contents to this folder, and then delete the duplicates 164 | 165 | # First of all check whether we have been passed a group of folders 166 | if ($realFolder.Count -gt 1) 167 | { 168 | foreach ($f in $realFolder) 169 | { 170 | ProcessFolder($f) 171 | } 172 | return 173 | } 174 | 175 | # Search the folder list for duplicates of this folder 176 | LogVerbose "Searching for duplicates: $($realFolder.Name)" -ForegroundColor Gray 177 | 178 | foreach ($folder in $script:folders) 179 | { 180 | if (($folder.Name -eq $realFolder.Name) -and ($folder.FolderPath -eq $realFolder.FolderPath) -and ($folder.FolderId -ne $realFolder.FolderId)) 181 | { 182 | # This is a duplicate folder, so we want to merge it into the main folder, then delete it 183 | Log "Duplicate folder $($folder.Name) found: $($folder.FolderId)" Yellow 184 | $script:duplicateFolderFound = $true 185 | if ($Repair) 186 | { 187 | $targetId = ConvertFolderIdToEntryId($realFolder.FolderId) 188 | $sourceId = ConvertFolderIdToEntryId($folder.Id) 189 | if ($Impersonate) 190 | { 191 | .\Merge-MailboxFolder.ps1 -SourceMailbox $Mailbox -MergeFolderList @{ $targetId = $sourceId } -ByEntryId -ProcessSubfolders -CreateTargetFolder -Delete -Impersonate -Credentials $Credentials -EwsUrl $EWSUrl -LogFile $LogFile 192 | } 193 | else 194 | { 195 | .\Merge-MailboxFolder.ps1 -SourceMailbox $Mailbox -MergeFolderList @{ $targetId = $sourceId } -ByEntryId -ProcessSubfolders -CreateTargetFolder -Delete -Credentials $Credentials -EwsUrl $EWSUrl -LogFile $LogFile 196 | } 197 | } 198 | } 199 | } 200 | } 201 | 202 | Function ProcessMailbox($mbx) 203 | { 204 | if ([String]::IsNullOrEmpty($mbx)) 205 | { 206 | if (![String]::IsNullOrEmpty($Mailbox)) 207 | { 208 | Log "Processing $($Mailbox)" 209 | $mbx = Get-Mailbox $Mailbox 210 | } 211 | else 212 | { 213 | return 214 | } 215 | } 216 | 217 | if ($mbx -eq $Null) 218 | { 219 | Log "Invalid mailbox" Red 220 | exit 221 | } 222 | 223 | # Retrieve the list of all mailbox folders (this is so we can identify the duplicates) 224 | $script:folders = $Null 225 | $script:folders = Get-MailboxFolderStatistics -Identity $mbx.Identity 226 | if ($script:folders -eq $Null) 227 | { 228 | Log "Failed to read mailbox folders for $mbx.Identity" Red 229 | exit 230 | } 231 | 232 | # Now process each folder and remove any duplicates 233 | LogVerbose "Searching mailbox $($mbx.PrimarySmtpAddress) for duplicate folders" 234 | $script:duplicateFolderFound = $false 235 | ForEach ($folder in $script:folders) 236 | { 237 | ProcessFolder($folder) 238 | } 239 | if (!$script:duplicateFolderFound) 240 | { 241 | Log "No duplicate folders found for $($mbx.PrimarySmtpAddress)" Green 242 | } 243 | } 244 | 245 | ImportExchangeManagementSession( @( "Get-Mailbox", "Get-MailboxFolderStatistics") ) 246 | 247 | if ([String]::IsNullOrEmpty($Mailbox)) 248 | { 249 | # No mailbox specified, so run a report against all of them 250 | $mbxs = Get-Mailbox -ResultSize Unlimited 251 | ForEach ($mbx in $mbxs) { 252 | Log "Processing $($mbx.PrimarySmtpAddress)" 253 |   ProcessMailbox $mbx 254 | } 255 | } 256 | else 257 | { 258 | ProcessMailbox "" 259 | } -------------------------------------------------------------------------------- /Legacy/Get-EmptyFolders.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Get-EmptyFolders.ps1 3 | # 4 | # By David Barrett, Microsoft Ltd. 2015. Use at your own risk. No warranties are given. 5 | # 6 | # DISCLAIMER: 7 | # THIS CODE IS SAMPLE CODE. THESE SAMPLES ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. 8 | # MICROSOFT FURTHER DISCLAIMS ALL IMPLIED WARRANTIES INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR OF FITNESS FOR 9 | # A PARTICULAR PURPOSE. THE ENTIRE RISK ARISING OUT OF THE USE OR PERFORMANCE OF THE SAMPLES REMAINS WITH YOU. IN NO EVENT SHALL 10 | # MICROSOFT OR ITS SUPPLIERS BE LIABLE FOR ANY DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, 11 | # BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE 12 | # SAMPLES, EVEN IF MICROSOFT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. BECAUSE SOME STATES DO NOT ALLOW THE EXCLUSION OR LIMITATION 13 | # OF LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE LIMITATION MAY NOT APPLY TO YOU. 14 | 15 | param ( 16 | [Parameter(Position=0,Mandatory=$False,HelpMessage="Specifies the mailbox to be accessed")] 17 | [ValidateNotNullOrEmpty()] 18 | [string]$Mailbox, 19 | 20 | [Parameter(Mandatory=$False,HelpMessage="When specified, the archive mailbox will be accessed (instead of the main mailbox)")] 21 | [switch]$Archive, 22 | 23 | [Parameter(Mandatory=$False,HelpMessage="If this switch is present, folder path is required and the path points to a public folder")] 24 | [switch]$PublicFolders, 25 | 26 | [Parameter(Mandatory=$False,HelpMessage="Folder to search from - if omitted, the mailbox message root folder is assumed. All folders beneath this folder will be scanned.")] 27 | [string]$FolderPath, 28 | 29 | [Parameter(Mandatory=$False,HelpMessage="List of folders to ignore")] 30 | $IgnoreList = @("\Conversation History", "\Contacts\Skype for Business Contacts"), 31 | 32 | [Parameter(Position=1,Mandatory=$False,HelpMessage="If specified, export CSV of empty folders to this file ({0} will be replaced by SMTP address of the mailbox being reported on). e.g. c:\Temp\EmptyFolderReport-{0}.csv")] 33 | [ValidateNotNullOrEmpty()] 34 | [string]$ReportToFile, 35 | 36 | [Parameter(Mandatory=$False,HelpMessage="If this switch is present, empty folders will be deleted (user will be prompted to confirm unless -Force is also specified)")] 37 | [switch]$Delete, 38 | 39 | [Parameter(Mandatory=$False,HelpMessage="If this switch is present, empty folders will be deleted without confirmation")] 40 | [switch]$Force, 41 | 42 | [Parameter(Mandatory=$False,HelpMessage="Credentials used to authenticate with EWS")] 43 | [System.Management.Automation.PSCredential]$Credentials, 44 | 45 | [Parameter(Mandatory=$False,HelpMessage="Username used to authenticate with EWS")] 46 | [string]$Username, 47 | 48 | [Parameter(Mandatory=$False,HelpMessage="Password used to authenticate with EWS")] 49 | [string]$Password, 50 | 51 | [Parameter(Mandatory=$False,HelpMessage="Domain used to authenticate with EWS")] 52 | [string]$Domain, 53 | 54 | [Parameter(Mandatory=$False,HelpMessage="Whether we are using impersonation to access the mailbox")] 55 | [switch]$Impersonate, 56 | 57 | [Parameter(Mandatory=$False,HelpMessage="EWS Url (if omitted, then autodiscover is used)")] 58 | [string]$EwsUrl, 59 | 60 | [Parameter(Mandatory=$False,HelpMessage="Path to managed API (if omitted, a search of standard paths is performed)")] 61 | [string]$EWSManagedApiPath = "", 62 | 63 | [Parameter(Mandatory=$False,HelpMessage="Whether to ignore any SSL errors (e.g. invalid certificate)")] 64 | [switch]$IgnoreSSLCertificate, 65 | 66 | [Parameter(Mandatory=$False,HelpMessage="Whether to allow insecure redirects when performing autodiscover")] 67 | [switch]$AllowInsecureRedirection, 68 | 69 | [Parameter(Mandatory=$False,HelpMessage="Log file - activity is logged to this file if specified")] 70 | [string]$LogFile = "", 71 | 72 | [Parameter(Mandatory=$False,HelpMessage="Trace file - if specified, EWS tracing information is written to this file")] 73 | [string]$TraceFile 74 | ) 75 | 76 | # Define our functions 77 | 78 | Function Log([string]$Details, [ConsoleColor]$Colour) 79 | { 80 | if ($Colour -eq $null) 81 | { 82 | $Colour = [ConsoleColor]::White 83 | } 84 | Write-Host $Details -ForegroundColor $Colour 85 | if ( [String]::IsNullOrEmpty($LogFile) ) { return } 86 | $Details | Out-File $LogFile -Append 87 | } 88 | 89 | Function LogVerbose([string]$Details) 90 | { 91 | Write-Verbose $Details 92 | 93 | if ($VerbosePreference -eq "SilentlyContinue") { return } # We only log verbose messages to the log-file if verbose messages are shown in the console 94 | if ( $LogFile -eq "" ) { return } 95 | $Details | Out-File $LogFile -Append 96 | } 97 | 98 | Function LoadEWSManagedAPI() 99 | { 100 | # Find and load the managed API 101 | 102 | if ( ![string]::IsNullOrEmpty($EWSManagedApiPath) ) 103 | { 104 | if ( Test-Path $EWSManagedApiPath ) 105 | { 106 | Add-Type -Path $EWSManagedApiPath 107 | return $true 108 | } 109 | Write-Host ( [string]::Format("Managed API not found at specified location: {0}", $EWSManagedApiPath) ) Yellow 110 | } 111 | 112 | $a = Get-ChildItem -Recurse "C:\Program Files (x86)\Microsoft\Exchange\Web Services" -ErrorAction SilentlyContinue | Where-Object { ($_.PSIsContainer -eq $false) -and ( $_.Name -eq "Microsoft.Exchange.WebServices.dll" ) } 113 | if (!$a) 114 | { 115 | $a = Get-ChildItem -Recurse "C:\Program Files\Microsoft\Exchange\Web Services" -ErrorAction SilentlyContinue | Where-Object { ($_.PSIsContainer -eq $false) -and ( $_.Name -eq "Microsoft.Exchange.WebServices.dll" ) } 116 | } 117 | 118 | if ($a) 119 | { 120 | # Load EWS Managed API 121 | Write-Host ([string]::Format("Using managed API {0} found at: {1}", $a.VersionInfo.FileVersion, $a.VersionInfo.FileName)) -ForegroundColor Gray 122 | Add-Type -Path $a.VersionInfo.FileName 123 | $script:EWSManagedApiPath = $a.VersionInfo.FileName 124 | return $true 125 | } 126 | return $false 127 | } 128 | 129 | Function CurrentUserPrimarySmtpAddress() 130 | { 131 | # Attempt to retrieve the current user's primary SMTP address 132 | $searcher = [adsisearcher]"(samaccountname=$env:USERNAME)" 133 | $result = $searcher.FindOne() 134 | 135 | if ($result -ne $null) 136 | { 137 | return $result.Properties["mail"] 138 | } 139 | return $null 140 | } 141 | 142 | Function TrustAllCerts() 143 | { 144 | # Implement call-back to override certificate handling (and accept all) 145 | $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider 146 | $Compiler=$Provider.CreateCompiler() 147 | $Params=New-Object System.CodeDom.Compiler.CompilerParameters 148 | $Params.GenerateExecutable=$False 149 | $Params.GenerateInMemory=$True 150 | $Params.IncludeDebugInformation=$False 151 | $Params.ReferencedAssemblies.Add("System.DLL") | Out-Null 152 | 153 | $TASource=@' 154 | namespace Local.ToolkitExtensions.Net.CertificatePolicy { 155 | public class TrustAll : System.Net.ICertificatePolicy { 156 | public TrustAll() 157 | { 158 | } 159 | public bool CheckValidationResult(System.Net.ServicePoint sp, 160 | System.Security.Cryptography.X509Certificates.X509Certificate cert, 161 | System.Net.WebRequest req, int problem) 162 | { 163 | return true; 164 | } 165 | } 166 | } 167 | '@ 168 | $TAResults=$Provider.CompileAssemblyFromSource($Params,$TASource) 169 | $TAAssembly=$TAResults.CompiledAssembly 170 | 171 | ## We now create an instance of the TrustAll and attach it to the ServicePointManager 172 | $TrustAll=$TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll") 173 | [System.Net.ServicePointManager]::CertificatePolicy=$TrustAll 174 | } 175 | 176 | Function CreateTraceListener($service) 177 | { 178 | # Create trace listener to capture EWS conversation (useful for debugging) 179 | if ($script:Tracer -eq $null) 180 | { 181 | $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider 182 | $Params=New-Object System.CodeDom.Compiler.CompilerParameters 183 | $Params.GenerateExecutable=$False 184 | $Params.GenerateInMemory=$True 185 | $Params.IncludeDebugInformation=$False 186 | $Params.ReferencedAssemblies.Add("System.dll") | Out-Null 187 | $Params.ReferencedAssemblies.Add($EWSManagedApiPath) | Out-Null 188 | 189 | $traceFileForCode = $traceFile.Replace("\", "\\") 190 | 191 | if (![String]::IsNullOrEmpty($TraceFile)) 192 | { 193 | LogVerbose "Tracing to: $TraceFile" 194 | } 195 | 196 | $TraceListenerClass = @" 197 | using System; 198 | using System.Text; 199 | using System.IO; 200 | using System.Threading; 201 | using Microsoft.Exchange.WebServices.Data; 202 | 203 | namespace TraceListener { 204 | class EWSTracer: Microsoft.Exchange.WebServices.Data.ITraceListener 205 | { 206 | private StreamWriter _traceStream = null; 207 | private string _lastResponse = String.Empty; 208 | 209 | public EWSTracer() 210 | { 211 | try 212 | { 213 | _traceStream = File.AppendText("$traceFileForCode"); 214 | } 215 | catch { } 216 | } 217 | 218 | ~EWSTracer() 219 | { 220 | Close(); 221 | } 222 | 223 | public void Close() 224 | { 225 | try 226 | { 227 | _traceStream.Flush(); 228 | _traceStream.Close(); 229 | } 230 | catch { } 231 | } 232 | 233 | 234 | public void Trace(string traceType, string traceMessage) 235 | { 236 | if ( traceType.Equals("EwsResponse") ) 237 | _lastResponse = traceMessage; 238 | 239 | if ( traceType.Equals("EwsRequest") ) 240 | _lastResponse = String.Empty; 241 | 242 | if (_traceStream == null) 243 | return; 244 | 245 | lock (this) 246 | { 247 | try 248 | { 249 | _traceStream.WriteLine(traceMessage); 250 | _traceStream.Flush(); 251 | } 252 | catch { } 253 | } 254 | } 255 | 256 | public string LastResponse 257 | { 258 | get { return _lastResponse; } 259 | } 260 | } 261 | } 262 | "@ 263 | 264 | $TraceCompilation=$Provider.CompileAssemblyFromSource($Params,$TraceListenerClass) 265 | $TraceAssembly=$TraceCompilation.CompiledAssembly 266 | $script:Tracer=$TraceAssembly.CreateInstance("TraceListener.EWSTracer") 267 | } 268 | 269 | # Attach the trace listener to the Exchange service 270 | $service.TraceListener = $script:Tracer 271 | } 272 | 273 | Function Throttled() 274 | { 275 | # Checks if we've been throttled. If we have, we wait for the specified number of BackOffMilliSeconds before returning 276 | 277 | if ([String]::IsNullOrEmpty($script:Tracer.LastResponse)) 278 | { 279 | return $false # Throttling does return a response, if we don't have one, then throttling probably isn't the issue (though sometimes throttling just results in a timeout) 280 | } 281 | 282 | $lastResponse = $script:Tracer.LastResponse.Replace("", "") 283 | $lastResponse = "$lastResponse" 284 | $responseXml = [xml]$lastResponse 285 | 286 | if ($responseXml.Trace.Envelope.Body.Fault.detail.MessageXml.Value.Name -eq "BackOffMilliseconds") 287 | { 288 | # We are throttled, and the server has told us how long to back off for 289 | 290 | # Increase our throttling delay to try and avoid throttling (we only increase to a maximum delay of 15 seconds between requests) 291 | if ( $script:throttlingDelay -lt 15000) 292 | { 293 | if ($script:throttlingDelay -lt 1) 294 | { 295 | $script:throttlingDelay = 2000 296 | } 297 | else 298 | { 299 | $script:throttlingDelay = $script:throttlingDelay * 2 300 | } 301 | if ( $script:throttlingDelay -gt 15000) 302 | { 303 | $script:throttlingDelay = 15000 304 | } 305 | } 306 | LogVerbose "Updated throttling delay to $($script:throttlingDelay)ms" 307 | 308 | # Now back off for the time given by the server 309 | Log "Throttling detected, server requested back off for $($responseXml.Trace.Envelope.Body.Fault.detail.MessageXml.Value."#text") milliseconds" Yellow 310 | Sleep -Milliseconds $responseXml.Trace.Envelope.Body.Fault.detail.MessageXml.Value."#text" 311 | Log "Throttling budget should now be reset, resuming operations" Gray 312 | return $true 313 | } 314 | return $false 315 | } 316 | 317 | function ThrottledFolderBind() 318 | { 319 | param ( 320 | $folderId, 321 | $propset = $null) 322 | 323 | LogVerbose "Attempting to bind to folder $folderId" 324 | try 325 | { 326 | if ($propset -eq $null) 327 | { 328 | $folder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($script:service, $folderId) 329 | } 330 | else 331 | { 332 | $folder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($script:service, $folderId, $propset) 333 | } 334 | Sleep -Milliseconds $script:throttlingDelay 335 | if (-not ($folder -eq $null)) 336 | { 337 | LogVerbose "Successfully bound to $($folderId): $($folder.DisplayName)" White 338 | } 339 | return $folder 340 | } 341 | catch 342 | { 343 | } 344 | 345 | if (Throttled) 346 | { 347 | try 348 | { 349 | if ($propset -eq $null) 350 | { 351 | $folder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($script:service, $folderId) 352 | } 353 | else 354 | { 355 | $folder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($script:service, $folderId, $propset) 356 | } 357 | return $folder 358 | } 359 | catch {} 360 | } 361 | 362 | # If we get to this point, we have been unable to bind to the folder 363 | return $null 364 | } 365 | 366 | function CreateService($targetMailbox) 367 | { 368 | # Creates and returns an ExchangeService object to be used to access mailboxes 369 | 370 | # First of all check to see if we have a service object for this mailbox already 371 | if ($script:services -eq $null) 372 | { 373 | $script:services = @{} 374 | } 375 | if ($script:services.ContainsKey($targetMailbox)) 376 | { 377 | return $script:services[$targetMailbox] 378 | } 379 | 380 | # Create new service 381 | $exchangeService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013) 382 | 383 | # Set credentials if specified, or use logged on user. 384 | if ($Credentials -ne $Null) 385 | { 386 | LogVerbose "Applying given credentials" 387 | $exchangeService.Credentials = $Credentials.GetNetworkCredential() 388 | } 389 | elseif ($Username -and $Password) 390 | { 391 | LogVerbose "Applying given credentials for $Username" 392 | if ($Domain) 393 | { 394 | $exchangeService.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials($Username,$Password,$Domain) 395 | } else { 396 | $exchangeService.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials($Username,$Password) 397 | } 398 | } 399 | else 400 | { 401 | LogVerbose "Using default credentials" 402 | $exchangeService.UseDefaultCredentials = $true 403 | } 404 | 405 | # Set EWS URL if specified, or use autodiscover if no URL specified. 406 | if ($EwsUrl) 407 | { 408 | $exchangeService.URL = New-Object Uri($EwsUrl) 409 | } 410 | else 411 | { 412 | try 413 | { 414 | LogVerbose "Performing autodiscover for $targetMailbox" 415 | if ( $AllowInsecureRedirection ) 416 | { 417 | $exchangeService.AutodiscoverUrl($targetMailbox, {$True}) 418 | } 419 | else 420 | { 421 | $exchangeService.AutodiscoverUrl($targetMailbox) 422 | } 423 | if ([string]::IsNullOrEmpty($exchangeService.Url)) 424 | { 425 | Log "$targetMailbox : autodiscover failed" Red 426 | return $null 427 | } 428 | LogVerbose "EWS Url found: $($exchangeService.Url)" 429 | } 430 | catch 431 | { 432 | Log "$targetMailbox : error occurred during autodiscover: $($Error[0])" Red 433 | return $null 434 | } 435 | } 436 | 437 | if ($Impersonate) 438 | { 439 | $exchangeService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $targetMailbox) 440 | } 441 | 442 | # We enable tracing so that we can retrieve the last response (and read any throttling information from it - this isn't exposed in the EWS Managed API) 443 | CreateTraceListener $exchangeService 444 | $exchangeService.TraceFlags = [Microsoft.Exchange.WebServices.Data.TraceFlags]::All 445 | $exchangeService.TraceEnabled = $True 446 | 447 | $script:services.Add($targetMailbox, $exchangeService) 448 | return $exchangeService 449 | } 450 | 451 | Function GetFolder() 452 | { 453 | # Return a reference to a folder specified by path 454 | 455 | $RootFolder, $FolderPath, $Create = $args[0] 456 | 457 | if ( $RootFolder -eq $null ) 458 | { 459 | LogVerbose "GetFolder called with null root folder" 460 | return $null 461 | } 462 | 463 | $Folder = $RootFolder 464 | if ($FolderPath -ne '\') 465 | { 466 | $PathElements = $FolderPath -split '\\' 467 | For ($i=0; $i -lt $PathElements.Count; $i++) 468 | { 469 | if ($PathElements[$i]) 470 | { 471 | $View = New-Object Microsoft.Exchange.WebServices.Data.FolderView(2,0) 472 | $View.PropertySet = [Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly 473 | 474 | $SearchFilter = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName, $PathElements[$i]) 475 | 476 | $FolderResults = $Null 477 | try 478 | { 479 | $FolderResults = $Folder.FindFolders($SearchFilter, $View) 480 | Sleep -Milliseconds $script:throttlingDelay 481 | } 482 | catch {} 483 | if ($FolderResults -eq $Null) 484 | { 485 | if (Throttled) 486 | { 487 | try 488 | { 489 | $FolderResults = $Folder.FindFolders($SearchFilter, $View) 490 | } 491 | catch {} 492 | } 493 | } 494 | if ($FolderResults -eq $null) 495 | { 496 | return $null 497 | } 498 | 499 | if ($FolderResults.TotalCount -gt 1) 500 | { 501 | # We have more than one folder returned... We shouldn't ever get this, as it means we have duplicate folders 502 | $Folder = $null 503 | Write-Host "Duplicate folders ($($PathElements[$i])) found in path $FolderPath" -ForegroundColor Red 504 | break 505 | } 506 | elseif ( $FolderResults.TotalCount -eq 0 ) 507 | { 508 | if ($Create) 509 | { 510 | # Folder not found, so attempt to create it 511 | $subfolder = New-Object Microsoft.Exchange.WebServices.Data.Folder($script:service) 512 | $subfolder.DisplayName = $PathElements[$i] 513 | try 514 | { 515 | $subfolder.Save($Folder.Id) 516 | LogVerbose "Created folder $($PathElements[$i])" 517 | } 518 | catch 519 | { 520 | # Failed to create the subfolder 521 | $Folder = $null 522 | Log "Failed to create folder $($PathElements[$i]) in path $FolderPath" Red 523 | break 524 | } 525 | $Folder = $subfolder 526 | } 527 | else 528 | { 529 | # Folder doesn't exist 530 | $Folder = $null 531 | Log "Folder $($PathElements[$i]) doesn't exist in path $FolderPath" Red 532 | break 533 | } 534 | } 535 | else 536 | { 537 | $Folder = ThrottledFolderBind $FolderResults.Folders[0].Id 538 | } 539 | } 540 | } 541 | } 542 | 543 | $Folder 544 | } 545 | 546 | function GetFolderPath($Folder) 547 | { 548 | # Return the full path for the given folder 549 | 550 | # We cache our folder lookups for this script 551 | if (!$script:folderCache) 552 | { 553 | # Note that we can't use a PowerShell hash table to build a list of folder Ids, as the hash table is case-insensitive 554 | # We use a .Net Dictionary object instead 555 | $script:folderCache = New-Object 'System.Collections.Generic.Dictionary[System.String,System.Object]' 556 | } 557 | 558 | $propset = New-Object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly, [Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName, [Microsoft.Exchange.WebServices.Data.FolderSchema]::ParentFolderId) 559 | 560 | if ($Folder -eq "\") 561 | { 562 | # Special handling for root folder 563 | if ($script:folderCache.ContainsKey("\")) 564 | { 565 | return $script:folderCache["\"] 566 | } 567 | $mbx = New-Object Microsoft.Exchange.WebServices.Data.Mailbox( $Mailbox ) 568 | $folderId = New-Object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot, $mbx ) 569 | $rootFolder = ThrottledFolderBind $folderId $propset 570 | if ($rootFolder) 571 | { 572 | $folderPath = "\$($rootFolder.DisplayName)" 573 | $script:folderCache.Add("\", $folderPath) 574 | $script:FolderCache.Add($rootFolder.Id.UniqueId, $rootFolder) 575 | return $folderPath 576 | } 577 | return "" 578 | } 579 | else 580 | { 581 | $parentFolder = ThrottledFolderBind $Folder.Id $propset 582 | $folderPath = $Folder.DisplayName 583 | $parentFolderId = $Folder.Id 584 | } 585 | 586 | while ($parentFolder.ParentFolderId -ne $parentFolderId) 587 | { 588 | if ($script:folderCache.ContainsKey($parentFolder.ParentFolderId.UniqueId)) 589 | { 590 | $parentFolder = $script:folderCache[$parentFolder.ParentFolderId.UniqueId] 591 | } 592 | else 593 | { 594 | $parentFolder = ThrottledFolderBind $parentFolder.ParentFolderId $propset 595 | $script:FolderCache.Add($parentFolder.Id.UniqueId, $parentFolder) 596 | } 597 | $folderPath = "$($parentFolder.DisplayName)\$folderPath" 598 | $parentFolderId = $parentFolder.Id 599 | } 600 | return $folderPath 601 | } 602 | 603 | function GetWellKnownFolderIds() 604 | { 605 | # Get the Ids of all the well known folders (so that we can exclude them from our search) 606 | $wellKnownFolders = [System.Enum]::GetNames([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]) 607 | $script:wellKnownFolderIds = @() 608 | 609 | foreach ($wellKnownFolder in $wellKnownFolders) 610 | { 611 | try 612 | { 613 | $propset = New-Object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly) 614 | $folder = ThrottledFolderBind $wellKnownFolder $propset 615 | $script:wellKnownFolderIds += $folder.Id 616 | } 617 | catch 618 | { 619 | # We ignore any errors, as it just means that the particular folder we've queried doesn't exist 620 | } 621 | } 622 | } 623 | 624 | function IsHidden() 625 | { 626 | param ( 627 | $folder 628 | ) 629 | 630 | # Returns true if the folder is hidden 631 | if ($folder.ExtendedProperties.Count -lt 1) 632 | { 633 | return $false 634 | } 635 | foreach ($prop in $folder.ExtendedProperties) 636 | { 637 | if ($prop.PropertyDefinition -eq $script:PidTagAttributeHidden) 638 | { 639 | #Write-Host "Hidden" -ForegroundColor Red 640 | return $prop.Value 641 | } 642 | } 643 | return $false 644 | } 645 | 646 | function SearchEmptyFolders() 647 | { 648 | param ( 649 | $folder 650 | ) 651 | 652 | $folder.Load($script:FolderPropSet) 653 | if (IsHidden $folder) 654 | { 655 | LogVerbose "Ignoring hidden folder: $($Folder.DisplayName)" 656 | return 657 | } 658 | LogVerbose "Processing: $($Folder.DisplayName)" 659 | 660 | # Recurse into any subfolders first 661 | $FolderView = New-Object Microsoft.Exchange.WebServices.Data.FolderView(1000) 662 | $FolderView.PropertySet = $script:FolderPropSet 663 | $FindFolderResults = $folder.FindFolders($FolderView) 664 | Sleep -Milliseconds $script:throttlingDelay 665 | ForEach ($subFolder in $FindFolderResults.Folders) 666 | { 667 | SearchEmptyFolders $subFolder 668 | } 669 | 670 | # Now we load the properties for this folder to see if it is empty (i.e. no subfolders, no items) 671 | $folder.Load($script:FolderPropSet) 672 | if ( ($folder.TotalCount -eq 0) -and ($folder.ChildFolderCount -eq 0) ) 673 | { 674 | # This folder is empty 675 | $folderPath = GetFolderPath $folder 676 | $isSpecialFolder = $false 677 | if ($script:wellKnownFolderIds.Contains($folder.Id)) 678 | { 679 | $isSpecialFolder = $true 680 | Log "$folderPath is empty, but is well known folder" Gray 681 | } 682 | if ($IgnoreList) 683 | { 684 | if ($IgnoreList.Contains("$folderPath".ToLower())) 685 | { 686 | $isSpecialFolder = $true 687 | Log "$folderPath is empty, but is on ignore list" Gray 688 | } 689 | } 690 | 691 | if (-not $isSpecialFolder) 692 | { 693 | Log "$folderPath is empty" Green 694 | $script:emptyFolders += $folderPath 695 | if ($Delete) 696 | { 697 | # Attempt to delete the folder 698 | try 699 | { 700 | $deleteThisFolder = $true 701 | if (-not $Force) 702 | { 703 | # Need to ask the user whether we should delete this folder 704 | $response = Read-Host -Prompt "Confirm delete (YyNn)? $folderPath" 705 | if (-not $response.ToLower().Equals("y")) 706 | { 707 | $deleteThisFolder = $false 708 | } 709 | } 710 | if ($deleteThisFolder) 711 | { 712 | $folder.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::SoftDelete) 713 | Log "$folderPath has been deleted" Yellow 714 | } 715 | else 716 | { 717 | Log "$folderPath was not deleted" Gray 718 | } 719 | } 720 | catch 721 | { 722 | if ($Error[0].Exception.Message.Contains("Distinguished folders cannot be deleted.")) 723 | { 724 | # We shouldn't encounter this error as we exclude WellKnownFolders 725 | Log "$folderPath was NOT deleted as it is a distinguished folder" Gray 726 | } 727 | else 728 | { 729 | Log "$folderPath was NOT deleted due to error: $($Error[0].Exception.Message)" Red 730 | } 731 | } 732 | } 733 | } 734 | } 735 | } 736 | 737 | function ProcessMailbox() 738 | { 739 | # Process the mailbox 740 | if ( [string]::IsNullOrEmpty($Mailbox) ) 741 | { 742 | Log "ProcessMailbox called with no mailbox set" Red 743 | return 744 | } 745 | Write-Host ([string]::Format("Processing mailbox {0}", $Mailbox)) -ForegroundColor Gray 746 | $script:service = CreateService($Mailbox) 747 | if ($script:service -eq $Null) 748 | { 749 | Log "Failed to create ExchangeService" Red 750 | return 751 | } 752 | 753 | $script:throttlingDelay = 0 754 | 755 | # Bind to root folder 756 | $mbx = New-Object Microsoft.Exchange.WebServices.Data.Mailbox( $Mailbox ) 757 | $Folder = $Null 758 | if ([String]::IsNullOrEmpty($FolderPath)) 759 | { 760 | $FolderPath = "wellknownfoldername.MsgFolderRoot" 761 | } 762 | if ($FolderPath.ToLower().StartsWith("wellknownfoldername.")) 763 | { 764 | # Well known folder specified (could be different name depending on language, so we bind to it using WellKnownFolderName enumeration) 765 | $wkf = $FolderPath.SubString(20) 766 | LogVerbose "Attempting to bind to well known folder: $wkf" 767 | $folderId = New-Object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::$wkf, $mbx ) 768 | $Folder = ThrottledFolderBind($folderId) 769 | } 770 | else 771 | { 772 | $folderId = New-Object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot, $mbx ) 773 | $Folder = ThrottledFolderBind($folderId) 774 | if ($Folder -and ($FolderPath -ne "\")) 775 | { 776 | $Folder = GetFolder($Folder, $FolderPath, $false) 777 | } 778 | } 779 | 780 | if (!$Folder) 781 | { 782 | Log "Failed to find folder $FolderPath" Red 783 | return 784 | } 785 | 786 | # Now we search for empty folders below our root folder 787 | 788 | # Declare the property set that we need to retrieve for each folder 789 | $script:PidTagAttributeHidden = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x10F4, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Boolean) 790 | $script:FolderPropSet = New-Object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly, [Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName, 791 | [Microsoft.Exchange.WebServices.Data.FolderSchema]::ChildFolderCount, [Microsoft.Exchange.WebServices.Data.FolderSchema]::TotalCount, $script:PidTagAttributeHidden) 792 | 793 | GetWellKnownFolderIds # We use these to exclude any default folders (e.g. Inbox) 794 | 795 | # Go through the ignore list and add the root folder to the path 796 | if ($IgnoreList.Count -gt 0) 797 | { 798 | $rootFolderPath = GetFolderPath("\") 799 | for ($i=0; $i -lt $IgnoreList.Count; $i++) 800 | { 801 | if ($IgnoreList[$i].StartsWith("\")) 802 | { 803 | $IgnoreList[$i] = "$rootFolderPath$($IgnoreList[$i])" 804 | } 805 | else 806 | { 807 | $IgnoreList[$i] = "$rootFolderPath\$($IgnoreList[$i])" 808 | } 809 | $IgnoreList[$i] = $IgnoreList[$i].ToLower() 810 | } 811 | } 812 | 813 | # Now search the folder heirarchy for empty folders, and report 814 | $script:emptyFolders = @() # Collect the paths of any empty folders to export to CSV 815 | SearchEmptyFolders $Folder 816 | if (![String]::IsNullOrEmpty($ReportToFile)) 817 | { 818 | if ($script:emptyFolders.Count -gt 0) 819 | { 820 | $export = @() 821 | foreach ($emptyFolder in $script:emptyFolders) 822 | { 823 | $info = @{ "Mailbox" = $Mailbox; "Empty Folder" = $emptyFolder } 824 | $export += New-Object PSObject -Property $info 825 | } 826 | $exportFile = [String]::Format($ReportToFile, $Mailbox) 827 | $export | Select-Object Mailbox,"Empty Folder" | Sort-Object Mailbox,"Empty Folder" | Export-CSV $exportFile -NoTypeInformation 828 | } 829 | } 830 | } 831 | 832 | 833 | # The following is the main script 834 | 835 | if ( [string]::IsNullOrEmpty($Mailbox) ) 836 | { 837 | $Mailbox = CurrentUserPrimarySmtpAddress 838 | if ( [string]::IsNullOrEmpty($Mailbox) ) 839 | { 840 | Write-Host "Mailbox not specified. Failed to determine current user's SMTP address." -ForegroundColor Red 841 | Exit 842 | } 843 | else 844 | { 845 | Write-Host ([string]::Format("Current user's SMTP address is {0}", $Mailbox)) -ForegroundColor Green 846 | } 847 | } 848 | 849 | # Check if we need to ignore any certificate errors 850 | # This needs to be done *before* the managed API is loaded, otherwise it doesn't work consistently (i.e. usually doesn't!) 851 | if ($IgnoreSSLCertificate) 852 | { 853 | Write-Host "WARNING: Ignoring any SSL certificate errors" -foregroundColor Yellow 854 | TrustAllCerts 855 | } 856 | 857 | # Load EWS Managed API 858 | if (!(LoadEWSManagedAPI)) 859 | { 860 | Write-Host "Failed to locate EWS Managed API, cannot continue" -ForegroundColor Red 861 | Write-Host "The API can be downloaded from the Microsoft Download Centre: http://www.microsoft.com/en-us/search/Results.aspx?q=exchange%20web%20services%20managed%20api&form=DLC" 862 | Write-Host "Use the latest version available" 863 | Exit 864 | } 865 | 866 | # Check we have valid credentials 867 | if ($Credentials -ne $Null) 868 | { 869 | If ($Username -or $Password) 870 | { 871 | Write-Host "Please specify *either* -Credentials *or* -Username and -Password" Red 872 | Exit 873 | } 874 | } 875 | 876 | 877 | 878 | Write-Host "" 879 | 880 | # Check whether we have a CSV file as input... 881 | $FileExists = Test-Path $Mailbox 882 | If ( $FileExists ) 883 | { 884 | # We have a CSV to process 885 | Write-Verbose "Reading mailboxes from CSV file" 886 | $csv = Import-CSV $Mailbox -Header "PrimarySmtpAddress" 887 | foreach ($entry in $csv) 888 | { 889 | Write-Verbose $entry.PrimarySmtpAddress 890 | if (![String]::IsNullOrEmpty($entry.PrimarySmtpAddress)) 891 | { 892 | if (!$entry.PrimarySmtpAddress.ToLower().Equals("primarysmtpaddress")) 893 | { 894 | $Mailbox = $entry.PrimarySmtpAddress 895 | ProcessMailbox 896 | } 897 | } 898 | } 899 | } 900 | Else 901 | { 902 | # Process as single mailbox 903 | ProcessMailbox 904 | } 905 | 906 | if ($script:Tracer -ne $null) 907 | { 908 | $script:Tracer.Close() 909 | } -------------------------------------------------------------------------------- /Legacy/Get-Office365ServiceStatus.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Get-Office365ServiceStatus.ps1 3 | # 4 | # By David Barrett, Microsoft Ltd. 2021. Use at your own risk. No warranties are given. 5 | # 6 | # DISCLAIMER: 7 | # THIS CODE IS SAMPLE CODE. THESE SAMPLES ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. 8 | # MICROSOFT FURTHER DISCLAIMS ALL IMPLIED WARRANTIES INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR OF FITNESS FOR 9 | # A PARTICULAR PURPOSE. THE ENTIRE RISK ARISING OUT OF THE USE OR PERFORMANCE OF THE SAMPLES REMAINS WITH YOU. IN NO EVENT SHALL 10 | # MICROSOFT OR ITS SUPPLIERS BE LIABLE FOR ANY DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, 11 | # BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE 12 | # SAMPLES, EVEN IF MICROSOFT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. BECAUSE SOME STATES DO NOT ALLOW THE EXCLUSION OR LIMITATION 13 | # OF LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE LIMITATION MAY NOT APPLY TO YOU. 14 | 15 | <# 16 | .SYNOPSIS 17 | Retrieve data from the Service Communications API. 18 | 19 | .DESCRIPTION 20 | This script demonstrates how to retrieve data from the Service Communications API (which provides current and historical status about Office 365 services). 21 | 22 | .EXAMPLE 23 | .\Get-Office365ServiceStatus.ps1 -AppId "" -TenantId "" -AppSecretKey "" -CurrentStatus 24 | 25 | This will display the current reported status for each workload 26 | 27 | .EXAMPLE 28 | .\Get-Office365ServiceStatus.ps1 -AppId "" -TenantId "" -AppSecretKey "" -Message 29 | 30 | This will display the current list of messages for each service and workload. 31 | 32 | #> 33 | 34 | 35 | param ( 36 | [Parameter(Mandatory=$True,HelpMessage="Application Id (obtained when registering the application in Azure AD")] 37 | [ValidateNotNullOrEmpty()] 38 | [string]$AppId, 39 | 40 | [Parameter(Mandatory=$True,HelpMessage="Application secret key (obtained when registering the application in Azure AD")] 41 | [ValidateNotNullOrEmpty()] 42 | [string]$AppSecretKey, 43 | 44 | [Parameter(Mandatory=$True,HelpMessage="Tenant Id")] 45 | [ValidateNotNullOrEmpty()] 46 | [string]$TenantId, 47 | 48 | [Parameter(Mandatory=$True,HelpMessage="Tenant domain")] 49 | [ValidateNotNullOrEmpty()] 50 | [string]$TenantDomain, 51 | 52 | [Parameter(Mandatory=$False,HelpMessage="Retrieve list of subscribed services")] 53 | [ValidateNotNullOrEmpty()] 54 | [switch]$Services, 55 | 56 | [Parameter(Mandatory=$False,HelpMessage="Retrieve the status of the service from the previous 24 hours")] 57 | [ValidateNotNullOrEmpty()] 58 | [switch]$CurrentStatus, 59 | 60 | [Parameter(Mandatory=$False,HelpMessage="Retrieve the status of the service from the previous 24 hours")] 61 | [ValidateNotNullOrEmpty()] 62 | [switch]$HistoricalStatus, 63 | 64 | [Parameter(Mandatory=$False,HelpMessage="Retrieve the status of the service from the previous 24 hours")] 65 | [ValidateNotNullOrEmpty()] 66 | [switch]$Messages, 67 | 68 | [Parameter(Mandatory=$False,HelpMessage="Report save path (reported are prepended by the current date)")] 69 | [ValidateNotNullOrEmpty()] 70 | [string]$ReportSavePath 71 | ) 72 | 73 | 74 | # Acquire token 75 | $body = @{grant_type="client_credentials";resource="https://manage.office.com";client_id=$AppId;client_secret=$AppSecretKey} 76 | #$body = @{grant_type="client_credentials";scope="https://graph.microsoft.com/.default";client_id=$AppId;client_secret=$AppSecretKey} 77 | try 78 | { 79 | #$oauth = Invoke-RestMethod -Method Post -Uri https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token -Body $body 80 | $oauth = Invoke-RestMethod -Method Post -Uri https://login.windows.net/$TenantDomain/oauth2/token?api-version=1.0 -Body $body 81 | 82 | } 83 | catch 84 | { 85 | Write-Host "Failed to obtain OAuth token" -ForegroundColor Red 86 | exit # Failed to obtain a token 87 | } 88 | $token = @{'Authorization'="$($oauth.token_type) $($oauth.access_token)"} 89 | Write-Verbose "$($oauth.token_type) $($oauth.access_token)" 90 | 91 | 92 | if ($Services) 93 | { 94 | # Get Services 95 | $getServices = Invoke-WebRequest -Method 'GET' -Uri "https://manage.office.com/api/v1.0/$TenantId/ServiceComms/Services" -Headers $token 96 | $global:services = $getServices.Content 97 | 98 | if (![String]::IsNullOrEmpty($ReportSavePath)) 99 | { 100 | $getServices.Content | Out-File "$ReportSavePath$([DateTime]::Today.ToString("yyyyMMdd")) Services.CSV" 101 | } 102 | } 103 | 104 | if ($CurrentStatus) 105 | { 106 | # Get current Status - we just report the feature status, not the individual workloads 107 | $getCurrentStatus = Invoke-WebRequest -Method Get -Uri "https://manage.office.com/api/v1.0/$TenantId/ServiceComms/CurrentStatus" -Headers $token 108 | $statusJson = ConvertFrom-Json $getCurrentStatus.Content 109 | $global:currentStatus = $statusJson 110 | 111 | Write-Host "Current Status" 112 | Write-Host "--------------" 113 | Write-Host "" 114 | 115 | foreach ($FeatureStatus in $statusJson.value) 116 | { 117 | Write-Host "$($FeatureStatus.Id): $($FeatureStatus.Status)" 118 | } 119 | 120 | if (![String]::IsNullOrEmpty($ReportSavePath)) 121 | { 122 | $getCurrentStatus.Content | Out-File "$ReportSavePath$([DateTime]::Now.ToString("yyyyMMddhhmmss")) CurrentStatus.CSV" 123 | } 124 | Write-Host "" 125 | } 126 | 127 | if ($HistoricalStatus) 128 | { 129 | # Get historical Status 130 | $getHistoricalStatus = Invoke-WebRequest -Method Get -Uri "https://manage.office.com/api/v1.0/$TenantId/ServiceComms/HistoricalStatus" -Headers $token 131 | $statusJson = ConvertFrom-Json $getHistoricalStatus.Content 132 | $global:historicalStatus = $statusJson 133 | 134 | Write-Host "Historical Status" 135 | Write-Host "-----------------" 136 | Write-Host "" 137 | 138 | foreach ($FeatureStatus in $statusJson.value) 139 | { 140 | Write-Host "$($FeatureStatus.Id): $($FeatureStatus.Status)" 141 | } 142 | 143 | if (![String]::IsNullOrEmpty($ReportSavePath)) 144 | { 145 | $getHistoricalStatus.Content | Out-File "$ReportSavePath$([DateTime]::Now.ToString("yyyyMMddhhmmss")) HistoricalStatus.CSV" 146 | } 147 | Write-Host "" 148 | } 149 | 150 | if ($Messages) 151 | { 152 | # Get messages 153 | $getMessages = Invoke-WebRequest -Method Get -Uri "https://manage.office.com/api/v1.0/$TenantId/ServiceComms/Messages" -Headers $token 154 | $statusJson = ConvertFrom-Json $getMessages.Content 155 | 156 | Write-Host "Messages" 157 | Write-Host "--------" 158 | Write-Host "" 159 | 160 | foreach ($messageGroup in $statusJson.value) 161 | { 162 | Write-Host "$($messageGroup.Workload) $($messageGroup.Id): $($messageGroup.Status) - $($messageGroup.Messages.Count) message(s)" 163 | } 164 | 165 | if (![String]::IsNullOrEmpty($ReportSavePath)) 166 | { 167 | $getMessages.Content | Out-File "$ReportSavePath$([DateTime]::Now.ToString("yyyyMMddhhmmss")) Messages.CSV" 168 | } 169 | } -------------------------------------------------------------------------------- /Legacy/Import-CalendarCSV.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Import-CalendarCSV.ps1 3 | # 4 | # By David Barrett, Microsoft Ltd. 2015-2021. Use at your own risk. No warranties are given. 5 | # 6 | # THIS CODE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 7 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 8 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 9 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 10 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 11 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 12 | # THE SOFTWARE. 13 | 14 | param( 15 | [Parameter(Position=1,Mandatory=$True,HelpMessage="CSV file containing the appointments to import.")] 16 | [ValidateNotNullOrEmpty()] 17 | [string]$CSVFileName, 18 | 19 | [Parameter(Position=2,Mandatory=$True,HelpMessage="Specifies the mailbox into which the calendar items will be imported.")] 20 | [ValidateNotNullOrEmpty()] 21 | [string]$Mailbox, 22 | 23 | [Parameter(Mandatory=$False,HelpMessage="If specified, a sample CSV will be created.")] 24 | [ValidateNotNullOrEmpty()] 25 | [switch]$GenerateSampleCSV, 26 | 27 | [Parameter(Mandatory=$False,HelpMessage="Credentials used to authenticate with EWS.")] 28 | [alias("Credential")] 29 | [System.Management.Automation.PSCredential]$Credentials, 30 | 31 | [Parameter(Mandatory=$False,HelpMessage="If set, then we will use OAuth to access the mailbox (required for MFA enabled accounts).")] 32 | [switch]$OAuth, 33 | 34 | [Parameter(Mandatory=$False,HelpMessage="The client Id that this script will identify as. Must be registered in Azure AD.")] 35 | [string]$OAuthClientId = "8799ab60-ace5-4bda-b31f-621c9f6668db", 36 | 37 | [Parameter(Mandatory=$False,HelpMessage="The tenant Id in which the application is registered. If missing, application is assumed to be multi-tenant and the common log-in URL will be used.")] 38 | [string]$OAuthTenantId = "", 39 | 40 | [Parameter(Mandatory=$False,HelpMessage="The redirect Uri of the Azure registered application.")] 41 | [string]$OAuthRedirectUri = "http://localhost/code", 42 | 43 | [Parameter(Mandatory=$False,HelpMessage="If using application permissions, specify the secret key OR certificate.")] 44 | [string]$OAuthSecretKey = "", 45 | 46 | [Parameter(Mandatory=$False,HelpMessage="If using application permissions, specify the secret key OR certificate. Please note that certificate auth requires the MSAL dll to be available.")] 47 | $OAuthCertificate = $null, 48 | 49 | [Parameter(Mandatory=$False,HelpMessage="Whether we are using impersonation to access the mailbox")] 50 | [switch]$Impersonate, 51 | 52 | [Parameter(Mandatory=$False,HelpMessage="EWS Url (if omitted, and -Office365 not specified, then autodiscover is used)")] 53 | [string]$EwsUrl, 54 | 55 | [Parameter(Mandatory=$False,HelpMessage="If specified, requests are directed to Office 365 endpoint (this overrides -EwsUrl)")] 56 | [switch]$Office365, 57 | 58 | [Parameter(Mandatory=$False,HelpMessage="Path to managed API (if omitted, a search of standard paths is performed)")] 59 | [string]$EWSManagedApiPath = "", 60 | 61 | [Parameter(Mandatory=$False,HelpMessage="Whether to ignore any SSL errors (e.g. invalid certificate)")] 62 | [switch]$IgnoreSSLCertificate, 63 | 64 | [Parameter(Mandatory=$False,HelpMessage="Whether to allow insecure redirects when performing autodiscover")] 65 | [switch]$AllowInsecureRedirection, 66 | 67 | [Parameter(Mandatory=$False,HelpMessage="Log file - activity is logged to this file if specified")] 68 | [string]$LogFile = "", 69 | 70 | [Parameter(Mandatory=$False,HelpMessage="Trace file - if specified, EWS tracing information is written to this file")] 71 | [string]$TraceFile 72 | ) 73 | 74 | 75 | $RequiredFields=@{ 76 | "Subject" = "Subject"; 77 | "StartDate" = "Start Date"; 78 | "StartTime" = "Start Time"; 79 | "EndDate" = "End Date"; 80 | "EndTime" = "End Time" 81 | } 82 | 83 | $script:ScriptVersion = "2.0.1" 84 | 85 | 86 | # Define our functions 87 | 88 | $script:LastError = $Error[0] 89 | Function ErrorReported($Context) 90 | { 91 | # Check for any error, and return the result ($true means a new error has been detected) 92 | 93 | # We check for errors using $Error variable, as try...catch isn't reliable when remoting 94 | if ([String]::IsNullOrEmpty($Error[0])) { return $false } 95 | 96 | # We have an error, have we already reported it? 97 | if ($Error[0] -eq $script:LastError) { return $false } 98 | 99 | # New error, so log it and return $true 100 | $script:LastError = $Error[0] 101 | if ($Context) 102 | { 103 | Log "Error ($Context): $($Error[0])" Red 104 | } 105 | else 106 | { 107 | Log "Error: $($Error[0])" Red 108 | } 109 | return $true 110 | } 111 | 112 | Function ReportError($Context) 113 | { 114 | # Reports error without returning the result 115 | ErrorReported $Context | Out-Null 116 | } 117 | 118 | Function LogToFile([string]$Details) 119 | { 120 | if ( [String]::IsNullOrEmpty($LogFile) ) { return } 121 | $logInfo = "$([DateTime]::Now.ToShortDateString()) $([DateTime]::Now.ToLongTimeString()) $Details" 122 | $logInfo | Out-File $LogFile -Append 123 | } 124 | 125 | Function Log([string]$Details, [ConsoleColor]$Colour) 126 | { 127 | if ($Colour -eq $null) 128 | { 129 | $Colour = [ConsoleColor]::White 130 | } 131 | Write-Host $Details -ForegroundColor $Colour 132 | LogToFile $Details 133 | } 134 | Log "$($MyInvocation.MyCommand.Name) version $($script:ScriptVersion) starting" Green 135 | 136 | Function LogVerbose([string]$Details) 137 | { 138 | Write-Verbose $Details 139 | if ( !$VerboseLogFile -and !$DebugLogFile -and ($VerbosePreference -eq "SilentlyContinue") ) { return } 140 | LogToFile $Details 141 | } 142 | 143 | Function LogDebug([string]$Details) 144 | { 145 | Write-Debug $Details 146 | if (!$DebugLogFile -and ($DebugPreference -eq "SilentlyContinue") ) { return } 147 | LogToFile $Details 148 | } 149 | 150 | function LoadLibraries() 151 | { 152 | param ( 153 | [bool]$searchProgramFiles, 154 | $dllNames, 155 | [ref]$dllLocations = @() 156 | ) 157 | # Attempt to find and load the specified libraries 158 | 159 | foreach ($dllName in $dllNames) 160 | { 161 | # First check if the dll is in current directory 162 | LogDebug "Searching for DLL: $dllName" 163 | $dll = $null 164 | try 165 | { 166 | $dll = Get-ChildItem $dllName -ErrorAction SilentlyContinue 167 | } 168 | catch {} 169 | 170 | if ($searchProgramFiles) 171 | { 172 | if ($dll -eq $null) 173 | { 174 | $dll = Get-ChildItem -Recurse "C:\Program Files (x86)" -ErrorAction SilentlyContinue | Where-Object { ($_.PSIsContainer -eq $false) -and ( $_.Name -eq $dllName ) } 175 | if (!$dll) 176 | { 177 | $dll = Get-ChildItem -Recurse "C:\Program Files" -ErrorAction SilentlyContinue | Where-Object { ($_.PSIsContainer -eq $false) -and ( $_.Name -eq $dllName ) } 178 | } 179 | } 180 | } 181 | $script:LastError = $Error[0] # We do this to suppress any errors encountered during the search above 182 | 183 | if ($dll -eq $null) 184 | { 185 | Log "Unable to load locate $dll" Red 186 | return $false 187 | } 188 | else 189 | { 190 | try 191 | { 192 | LogVerbose ([string]::Format("Loading {2} v{0} found at: {1}", $dll.VersionInfo.FileVersion, $dll.VersionInfo.FileName, $dllName)) 193 | Add-Type -Path $dll.VersionInfo.FileName 194 | if ($dllLocations) 195 | { 196 | $dllLocations.value += $dll.VersionInfo.FileName 197 | ReportError 198 | } 199 | } 200 | catch 201 | { 202 | ReportError "LoadLibraries" 203 | return $false 204 | } 205 | } 206 | } 207 | return $true 208 | } 209 | 210 | function GetTokenWithCertificate 211 | { 212 | # We use MSAL with certificate auth 213 | if (!$script:msalApiLoaded) 214 | { 215 | $msalLocation = @() 216 | $script:msalApiLoaded = $(LoadLibraries -searchProgramFiles $false -dllNames @("Microsoft.Identity.Client.dll") -dllLocations ([ref]$msalLocation)) 217 | if (!$script:msalApiLoaded) 218 | { 219 | Log "Failed to load MSAL. Cannot continue with certificate authentication." Red 220 | exit 221 | } 222 | } 223 | 224 | $cca1 = [Microsoft.Identity.Client.ConfidentialClientApplicationBuilder]::Create($OAuthClientId) 225 | $cca2 = $cca1.WithCertificate($OAuthCertificate) 226 | $cca3 = $cca2.WithTenantId($OAuthTenantId) 227 | $cca = $cca3.Build() 228 | 229 | $scopes = New-Object System.Collections.Generic.List[string] 230 | $scopes.Add("https://outlook.office365.com/.default") 231 | $acquire = $cca.AcquireTokenForClient($scopes) 232 | $authResult = $acquire.ExecuteAsync().Result 233 | $script:oauthToken = $authResult 234 | $script:oAuthAccessToken = $script:oAuthToken.AccessToken 235 | } 236 | 237 | function GetTokenViaCode 238 | { 239 | # Acquire auth code (needed to request token) 240 | $authUrl = "https://login.microsoftonline.com/$OAuthTenantId/oauth2/v2.0/authorize?client_id=$OAuthClientId&response_type=code&redirect_uri=$OAuthRedirectUri&response_mode=query&scope=openid%20profile%20email%20offline_access%20https://outlook.office365.com/.default" 241 | Write-Host "Please complete log-in via the web browser, and then paste the redirect URL (including auth code) here to continue" -ForegroundColor Green 242 | Start-Process $authUrl 243 | 244 | $authcode = Read-Host "Auth code" 245 | $codeStart = $authcode.IndexOf("?code=") 246 | if ($codeStart -gt 0) 247 | { 248 | $authcode = $authcode.Substring($codeStart+6) 249 | } 250 | $codeEnd = $authcode.IndexOf("&session_state=") 251 | if ($codeEnd -gt 0) 252 | { 253 | $authcode = $authcode.Substring(0, $codeEnd) 254 | } 255 | Write-Verbose "Using auth code: $authcode" 256 | 257 | # Acquire token (using the auth code) 258 | $body = @{grant_type="authorization_code";scope="https://outlook.office365.com/.default";client_id=$OAuthClientId;code=$authcode;redirect_uri=$OAuthRedirectUri} 259 | try 260 | { 261 | $script:oauthToken = Invoke-RestMethod -Method Post -Uri https://login.microsoftonline.com/$OAuthTenantId/oauth2/v2.0/token -Body $body 262 | $script:oAuthAccessToken = $script:oAuthToken.access_token 263 | $script:oauthTokenAcquireTime = [DateTime]::UtcNow 264 | } 265 | catch 266 | { 267 | Write-Host "Failed to obtain OAuth token" -ForegroundColor Red 268 | exit # Failed to obtain a token 269 | } 270 | } 271 | 272 | function GetTokenWithKey 273 | { 274 | $Body = @{ 275 | "grant_type" = "client_credentials"; 276 | "client_id" = "$OAuthClientId"; 277 | "client_secret" = "$OAuthSecretKey"; 278 | "scope" = "https://outlook.office365.com/.default" 279 | } 280 | 281 | try 282 | { 283 | $script:oAuthToken = Invoke-RestMethod -Method POST -uri "https://login.microsoftonline.com/$OAuthTenantId/oauth2/v2.0/token" -Body $body 284 | $script:oAuthAccessToken = $script:oAuthToken.access_token 285 | $script:oauthTokenAcquireTime = [DateTime]::UtcNow 286 | } 287 | catch 288 | { 289 | Write-Host "Failed to obtain OAuth token: $Error" -ForegroundColor Red 290 | exit # Failed to obtain a token 291 | } 292 | } 293 | 294 | function GetOAuthCredentials 295 | { 296 | # Obtain OAuth token for accessing mailbox 297 | param ( 298 | [switch]$RenewToken 299 | ) 300 | $exchangeCredentials = $null 301 | 302 | if ($script:oauthToken -ne $null) 303 | { 304 | # We already have a token 305 | if ($script:oauthTokenAcquireTime.AddSeconds($script:oauthToken.expires_in) -gt [DateTime]::UtcNow.AddMinutes(1)) 306 | { 307 | # Token still valid, so return that 308 | $exchangeCredentials = New-Object Microsoft.Exchange.WebServices.Data.OAuthCredentials($script:oAuthAccessToken) 309 | return $exchangeCredentials 310 | } 311 | 312 | # Token needs renewing 313 | 314 | } 315 | 316 | if (![String]::IsNullOrEmpty($OAuthSecretKey)) 317 | { 318 | GetTokenWithKey 319 | } 320 | elseif ($OAuthCertificate -ne $null) 321 | { 322 | GetTokenWithCertificate 323 | } 324 | else 325 | { 326 | GetTokenViaCode 327 | } 328 | 329 | # If we get here we have a valid token 330 | $exchangeCredentials = New-Object Microsoft.Exchange.WebServices.Data.OAuthCredentials($script:oAuthAccessToken) 331 | return $exchangeCredentials 332 | } 333 | 334 | function ApplyEWSOAuthCredentials 335 | { 336 | # Apply EWS OAuth credentials to all our service objects 337 | 338 | if ( -not $OAuth ) { return } 339 | if ( $script:services -eq $null ) { return } 340 | if ( $script:services.Count -lt 1 ) { return } 341 | if ( $script:oauthTokenAcquireTime.AddSeconds($script:oauthToken.expires_in) -gt [DateTime]::UtcNow.AddMinutes(1)) { return } 342 | 343 | # The token has expired and needs refreshing 344 | LogVerbose("[ApplyEWSOAuthCredentials] OAuth access token invalid, attempting to renew") 345 | $exchangeCredentials = GetOAuthCredentials -RenewToken 346 | if ($exchangeCredentials -eq $null) { return } 347 | if ( $script:oauthTokenAcquireTime.AddSeconds($script:oauthToken.expires_in) -le [DateTime]::Now ) 348 | { 349 | Log "[ApplyEWSOAuthCredentials] OAuth Token renewal failed" 350 | exit # We no longer have access to the mailbox, so we stop here 351 | } 352 | 353 | Log "[ApplyEWSOAuthCredentials] OAuth token successfully renewed; new expiry: $($script:oAuthToken.ExpiresOn)" 354 | if ($script:services.Count -gt 0) 355 | { 356 | foreach ($service in $script:services.Values) 357 | { 358 | $service.Credentials = New-Object Microsoft.Exchange.WebServices.Data.OAuthCredentials($exchangeCredentials) 359 | } 360 | LogVerbose "[ApplyEWSOAuthCredentials] Updated OAuth token for $($script.services.Count) ExchangeService object(s)" 361 | } 362 | } 363 | 364 | Function LoadEWSManagedAPI 365 | { 366 | # Find and load the managed API 367 | $ewsApiLocation = @() 368 | $ewsApiLoaded = $(LoadLibraries -searchProgramFiles $true -dllNames @("Microsoft.Exchange.WebServices.dll") -dllLocations ([ref]$ewsApiLocation)) 369 | ReportError "LoadEWSManagedAPI" 370 | 371 | if (!$ewsApiLoaded) 372 | { 373 | # Failed to load the EWS API, so try to install it from Nuget 374 | $ewsapi = Find-Package "Exchange.WebServices.Managed.Api" 375 | if ($ewsapi.Entities.Name.Equals("Microsoft")) 376 | { 377 | # We have found EWS API package, so install as current user (confirm with user first) 378 | Write-Host "EWS Managed API is not installed, but is available from Nuget. Install now for current user (required for this script to continue)? (Y/n)" -ForegroundColor Yellow 379 | $response = Read-Host 380 | if ( $response.ToLower().Equals("y") ) 381 | { 382 | Install-Package $ewsapi -Scope CurrentUser -Force 383 | $ewsApiLoaded = $(LoadLibraries -searchProgramFiles $true -dllNames @("Microsoft.Exchange.WebServices.dll") -dllLocations ([ref]$ewsApiLocation)) 384 | ReportError "LoadEWSManagedAPI" 385 | } 386 | } 387 | } 388 | 389 | if ($ewsApiLoaded) 390 | { 391 | if ($ewsApiLocation[0]) 392 | { 393 | Log "Using EWS Managed API found at: $($ewsApiLocation[0])" Gray 394 | $script:EWSManagedApiPath = $ewsApiLocation[0] 395 | } 396 | else 397 | { 398 | Write-Host "Failed to read EWS API location: $ewsApiLocation" 399 | Exit 400 | } 401 | } 402 | 403 | return $ewsApiLoaded 404 | } 405 | 406 | Function CurrentUserPrimarySmtpAddress() 407 | { 408 | # Attempt to retrieve the current user's primary SMTP address 409 | $searcher = [adsisearcher]"(samaccountname=$env:USERNAME)" 410 | $result = $searcher.FindOne() 411 | 412 | if ($result -ne $null) 413 | { 414 | $mail = $result.Properties["mail"] 415 | LogDebug "Current user's SMTP address is: $mail" 416 | return $mail 417 | } 418 | return $null 419 | } 420 | 421 | Function TrustAllCerts() 422 | { 423 | # Implement call-back to override certificate handling (and accept all) 424 | $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider 425 | $Compiler=$Provider.CreateCompiler() 426 | $Params=New-Object System.CodeDom.Compiler.CompilerParameters 427 | $Params.GenerateExecutable=$False 428 | $Params.GenerateInMemory=$True 429 | $Params.IncludeDebugInformation=$False 430 | $Params.ReferencedAssemblies.Add("System.DLL") | Out-Null 431 | 432 | $TASource=@' 433 | namespace Local.ToolkitExtensions.Net.CertificatePolicy { 434 | public class TrustAll : System.Net.ICertificatePolicy { 435 | public TrustAll() 436 | { 437 | } 438 | public bool CheckValidationResult(System.Net.ServicePoint sp, 439 | System.Security.Cryptography.X509Certificates.X509Certificate cert, 440 | System.Net.WebRequest req, int problem) 441 | { 442 | return true; 443 | } 444 | } 445 | } 446 | '@ 447 | $TAResults=$Provider.CompileAssemblyFromSource($Params,$TASource) 448 | $TAAssembly=$TAResults.CompiledAssembly 449 | 450 | ## We now create an instance of the TrustAll and attach it to the ServicePointManager 451 | $TrustAll=$TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll") 452 | [System.Net.ServicePointManager]::CertificatePolicy=$TrustAll 453 | } 454 | 455 | Function CreateTraceListener($service) 456 | { 457 | # Create trace listener to capture EWS conversation (useful for debugging) 458 | 459 | if ([String]::IsNullOrEmpty($EWSManagedApiPath)) 460 | { 461 | Log "Managed API path missing; unable to create tracer" Red 462 | Exit 463 | } 464 | 465 | if ($script:Tracer -eq $null) 466 | { 467 | $Provider=New-Object Microsoft.CSharp.CSharpCodeProvider 468 | $Params=New-Object System.CodeDom.Compiler.CompilerParameters 469 | $Params.GenerateExecutable=$False 470 | $Params.GenerateInMemory=$True 471 | $Params.IncludeDebugInformation=$False 472 | $Params.ReferencedAssemblies.Add("System.dll") | Out-Null 473 | $Params.ReferencedAssemblies.Add($EWSManagedApiPath) | Out-Null 474 | 475 | $traceFileForCode = $traceFile.Replace("\", "\\") 476 | 477 | if (![String]::IsNullOrEmpty($TraceFile)) 478 | { 479 | Log "Tracing to: $TraceFile" 480 | } 481 | 482 | $TraceListenerClass = @" 483 | using System; 484 | using System.Text; 485 | using System.IO; 486 | using System.Threading; 487 | using Microsoft.Exchange.WebServices.Data; 488 | 489 | namespace TraceListener { 490 | class EWSTracer: Microsoft.Exchange.WebServices.Data.ITraceListener 491 | { 492 | private StreamWriter _traceStream = null; 493 | private string _lastResponse = String.Empty; 494 | 495 | public EWSTracer() 496 | { 497 | try 498 | { 499 | _traceStream = File.AppendText("$traceFileForCode"); 500 | } 501 | catch { } 502 | } 503 | 504 | ~EWSTracer() 505 | { 506 | Close(); 507 | } 508 | 509 | public void Close() 510 | { 511 | try 512 | { 513 | _traceStream.Flush(); 514 | _traceStream.Close(); 515 | } 516 | catch { } 517 | } 518 | 519 | 520 | public void Trace(string traceType, string traceMessage) 521 | { 522 | if ( traceType.Equals("EwsResponse") ) 523 | _lastResponse = traceMessage; 524 | 525 | if ( traceType.Equals("EwsRequest") ) 526 | _lastResponse = String.Empty; 527 | 528 | if (_traceStream == null) 529 | return; 530 | 531 | lock (this) 532 | { 533 | try 534 | { 535 | _traceStream.WriteLine(traceMessage); 536 | _traceStream.Flush(); 537 | } 538 | catch { } 539 | } 540 | } 541 | 542 | public string LastResponse 543 | { 544 | get { return _lastResponse; } 545 | } 546 | } 547 | } 548 | "@ 549 | 550 | $script:Tracer = $null 551 | $TraceCompilation=$Provider.CompileAssemblyFromSource($Params,$TraceListenerClass) 552 | If ($TraceCompilation) 553 | { 554 | $TraceAssembly=$TraceCompilation.CompiledAssembly 555 | $script:Tracer=$TraceAssembly.CreateInstance("TraceListener.EWSTracer") 556 | # Attach the trace listener to the Exchange service 557 | $service.TraceListener = $script:Tracer 558 | } 559 | } 560 | } 561 | 562 | function CreateService($smtpAddress) 563 | { 564 | # Creates and returns an ExchangeService object to be used to access mailboxes 565 | 566 | # First of all check to see if we have a service object for this mailbox already 567 | if ($script:services -eq $null) 568 | { 569 | $script:services = @{} 570 | } 571 | if ($script:services.ContainsKey($smtpAddress)) 572 | { 573 | return $script:services[$smtpAddress] 574 | } 575 | 576 | # Create new service 577 | $exchangeService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2) 578 | 579 | # Do we need to use OAuth? 580 | if ($OAuth) 581 | { 582 | $exchangeService.Credentials = GetOAuthCredentials 583 | if ($exchangeService.Credentials -eq $null) 584 | { 585 | # OAuth failed 586 | return $null 587 | } 588 | } 589 | else 590 | { 591 | # Set credentials if specified, or use logged on user. 592 | if ($Credentials -ne $Null) 593 | { 594 | LogVerbose "Applying given credentials: $($Credentials.UserName)" 595 | $exchangeService.Credentials = $Credentials.GetNetworkCredential() 596 | } 597 | else 598 | { 599 | LogVerbose "Using default credentials" 600 | $exchangeService.UseDefaultCredentials = $true 601 | } 602 | } 603 | 604 | 605 | 606 | # Set EWS URL if specified, or use autodiscover if no URL specified. 607 | if ($EwsUrl -or $Office365) 608 | { 609 | if ($Office365) { $EwsUrl = "https://outlook.office365.com/EWS/Exchange.asmx" } 610 | $exchangeService.URL = New-Object Uri($EwsUrl) 611 | } 612 | else 613 | { 614 | try 615 | { 616 | LogVerbose "Performing autodiscover for $smtpAddress" 617 | if ( $AllowInsecureRedirection ) 618 | { 619 | $exchangeService.AutodiscoverUrl($smtpAddress, {$True}) 620 | } 621 | else 622 | { 623 | $exchangeService.AutodiscoverUrl($smtpAddress) 624 | } 625 | if ([string]::IsNullOrEmpty($exchangeService.Url)) 626 | { 627 | Log "$smtpAddress : autodiscover failed" Red 628 | return $Null 629 | } 630 | LogVerbose "EWS Url found: $($exchangeService.Url)" 631 | } 632 | catch 633 | { 634 | Log "$smtpAddress : error occurred during autodiscover: $($Error[0])" Red 635 | return $null 636 | } 637 | } 638 | 639 | $exchangeService.HttpHeaders.Add("X-AnchorMailbox", $smtpAddress) 640 | if ($Impersonate) 641 | { 642 | $exchangeService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $smtpAddress) 643 | } 644 | 645 | # We enable tracing so that we can retrieve the last response (and read any throttling information from it - this isn't exposed in the EWS Managed API) 646 | if (![String]::IsNullOrEmpty($EWSManagedApiPath)) 647 | { 648 | CreateTraceListener $exchangeService 649 | if ($script:Tracer) 650 | { 651 | $exchangeService.TraceFlags = [Microsoft.Exchange.WebServices.Data.TraceFlags]::All 652 | $exchangeService.TraceEnabled = $True 653 | } 654 | else 655 | { 656 | Log "Failed to create EWS trace listener. Throttling back-off time won't be detected." Yellow 657 | } 658 | } 659 | 660 | $script:services.Add($smtpAddress, $exchangeService) 661 | LogVerbose "Currently caching $($script:services.Count) ExchangeService objects" $true 662 | return $exchangeService 663 | } 664 | 665 | 666 | 667 | ################ 668 | # 669 | # Main script 670 | # 671 | ################ 672 | 673 | 674 | 675 | # Check if we need to generate a sample CSV first of all 676 | if ($GenerateSampleCSV) 677 | { 678 | # CSV Headers 679 | $csvData = new-object Text.StringBuilder 680 | [void]$csvData.AppendLine("Subject,StartDate,StartTime,EndDate,EndTime") 681 | 682 | # Now generate some sample appointments 683 | $i = 1 684 | while ($i -lt 10) 685 | { 686 | # Generate the start and end time 687 | $startDate = [DateTime]::Today.AddDays(7*$i).AddHours(10) 688 | $endDate = $startDate.AddHours(1) 689 | 690 | # Add the appointment to the CSV data 691 | [void]$csvData.Append("CSV Import Test $i,") 692 | [void]$csvData.Append($startDate.ToString("yyyy-MM-dd")) 693 | [void]$csvData.Append(",") 694 | [void]$csvData.Append($startDate.ToString("HH:mm:ss")) 695 | [void]$csvData.Append(",") 696 | [void]$csvData.Append($endDate.ToString("yyyy-MM-dd")) 697 | [void]$csvData.Append(",") 698 | [void]$csvData.AppendLine($endDate.ToString("HH:mm:ss")) 699 | $i++ 700 | } 701 | 702 | # And export the data to CSV 703 | try 704 | { 705 | $csvData.ToString() | Out-File $CSVFileName 706 | Log "Successfully exported sample CSV data to $CSVFileName" Green 707 | } 708 | catch {} 709 | } 710 | 711 | 712 | # CSV File Check 713 | if (!(Get-Item -Path $CSVFileName -ErrorAction SilentlyContinue)) 714 | { 715 | throw "Unable to open file: $CSVFileName" 716 | } 717 | 718 | # Import CSV File 719 | try 720 | { 721 | $CSVFile = Import-Csv -Path $CSVFileName 722 | } 723 | catch { } 724 | if (!$CSVFile) 725 | { 726 | Write-Host "CSV header line not found, using predefined header: Subject;StartDate;StartTime;EndDate;EndTime" 727 | $CSVFile = Import-Csv -Path $CSVFileName -header Subject,StartDate,StartTime,EndDate,EndTime 728 | } 729 | 730 | # Check file has required fields 731 | foreach ($Key in $RequiredFields.Keys) 732 | { 733 | if (!$CSVFile[0].$Key) 734 | { 735 | # Missing required field 736 | throw "Import file is missing required field: $Key" 737 | } 738 | } 739 | 740 | # Check if we need to ignore any certificate errors 741 | # This needs to be done *before* the managed API is loaded, otherwise it doesn't work consistently (i.e. usually doesn't!) 742 | if ($IgnoreSSLCertificate) 743 | { 744 | Write-Host "WARNING: Ignoring any SSL certificate errors" -foregroundColor Yellow 745 | TrustAllCerts 746 | } 747 | 748 | # Load EWS Managed API 749 | if (!(LoadEWSManagedAPI)) 750 | { 751 | Write-Host "Failed to locate EWS Managed API, cannot continue" -ForegroundColor Red 752 | Exit 753 | } 754 | 755 | # Create Service Object 756 | $service = CreateService($Mailbox) 757 | 758 | 759 | # Bind to the calendar folder 760 | $CalendarFolder = [Microsoft.Exchange.WebServices.Data.CalendarFolder]::Bind($service, [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Calendar) 761 | if (!$CalendarFolder) 762 | { 763 | Log "Failed to locate calendar folder" Red 764 | exit 765 | } 766 | 767 | # Parse the CSV file and add the appointments 768 | foreach ($CalendarItem in $CSVFile) 769 | { 770 | # Create the appointment and set the fields 771 | $NoError=$true 772 | try 773 | { 774 | $Appointment = New-Object Microsoft.Exchange.WebServices.Data.Appointment($service) 775 | $Appointment.Subject=$CalendarItem."Subject" 776 | $StartDate=[DateTime]($CalendarItem."StartDate" + " " + $CalendarItem."StartTime") 777 | $Appointment.Start=$StartDate 778 | $EndDate=[DateTime]($CalendarItem."EndDate" + " " + $CalendarItem."EndTime") 779 | $Appointment.End=$EndDate 780 | } 781 | catch 782 | { 783 | # If we fail to set any of the required fields, we will not write the appointment 784 | $NoError=$false 785 | } 786 | 787 | if ($NoError) 788 | { 789 | # Check for any other fields 790 | foreach ($Field in ($CalendarItem | Get-Member -MemberType Properties)) 791 | { 792 | if (!($RequiredFields.Keys -contains $Field.Name)) 793 | { 794 | # This is a custom (optional) field, so try to map it 795 | try 796 | { 797 | $Appointment.$($Field.Name)=$CalendarItem.$($Field.Name) 798 | } 799 | catch 800 | { 801 | # Failed to write this field 802 | Log "Failed to set custom field $($Field.Name)" yellow 803 | } 804 | } 805 | } 806 | # Save the appointment 807 | try 808 | { 809 | $Appointment.Save([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Calendar) 810 | Log "Created $($CalendarItem."Subject")" green 811 | } 812 | catch 813 | { 814 | Log "Failed to create appointment (error on save): $($CalendarItem."Subject")" red 815 | } 816 | } 817 | else 818 | { 819 | # Failed to set a required field 820 | Log "Failed to create appointment (required field missing): $($CalendarItem."Subject")" red 821 | } 822 | } -------------------------------------------------------------------------------- /Legacy/Microsoft.Exchange.WebServices.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/David-Barrett-MS/PowerShell-EWS-Scripts/a5d27bfd02d02e369b93f2ffc4a9bc8d327b657d/Legacy/Microsoft.Exchange.WebServices.dll -------------------------------------------------------------------------------- /Legacy/Search-Appointments.ps1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/David-Barrett-MS/PowerShell-EWS-Scripts/a5d27bfd02d02e369b93f2ffc4a9bc8d327b657d/Legacy/Search-Appointments.ps1 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerShell EWS Scripts 2 | A collection of PowerShell scripts that use EWS to perform actions against Exchange mailboxes 3 | 4 | See the [Wiki](https://github.com/David-Barrett-MS/PowerShell-EWS-Scripts/wiki) for documentation. 5 | --------------------------------------------------------------------------------