Pushing and auditing companywide Exchange mailbox calendar permissions for multinational firms via Powershell

My team recently received a request to grant editor access to every employee’s mailbox so that our homegrown time off management system could push approved entries in the calendar of end users. We are currently an Exchange Hybrid environment, and setting this up for Exchange Online Mailboxes was fairly simple as it only required an Entra AD application with Calendar.Read and Calendar.Write. On prem Exchange was a different story as there is no easy way to automatically assign just a folder level permission. While you could do so via a RBAC role that grants impersonation to the account that will be adding and removing the entries, that is a gross over permission.

Seeing that my team already has a scheduled ‘daily tasks’ PowerShell job that we use to call a number of small house keeping scripts that don’t warrant a dedicated script, we figured this would be a good area to include a function to audit this requested permission. As we started down this road, we did run into a few snags that made this a little more difficult then expected. The major issue were:

  • There is no easy way to filter for folder level permissions, you need to check each mailbox individually.
    • As is there is no -Filter parameter for the Get-MailboxFolderPermission cmdlet to only return mailboxes with the permission you are looking for
  • If you are a multiple national or part of your user base does not speak American English, you will run into issues where the default calendar in an Exchange mailbox is in the that users localized language and will need to handle that
  • The Get-MailboxFolderPermission cmdlet doesn’t seem to support the -errorvariable parameter the same way as other cmdlets. Meaning that it won’t hold the error if you also specify Ignore or SilentlyContinue
    • This is only an issue if you are trying to keep your host output clean, which is a big thing for me personally
  • Error trapping from the Exchange Management Shell is not great if you are trying to capture for specific exceptions, as all exceptions are all System.Management.Automation.RemoteException due to the PSSession

So with all that in mind I had do some more trickery then I originally planned for in order to handle all possible scenarios which are

  • Permission needs to be added
  • Permission is there but with the incorrect access right
  • The mailbox has non-US English Localization and thus the Calendar is not name “calendar”

Below is the function we built and added to our daily tasks script

Function SetCalendarPermission {
    <#
        .SYNOPSIS
            Ensures every mailbox's Calendar has the specified ServicePermission set for the specified ServiceAccount
    #>
    [CmdletBinding()]param(
        [String]$ServiceAccount = 'ServiceTimeOff',
        [String]$ServicePermission = 'Editor'
    )

    [System.Collections.ArrayList]$ChangeTable = @()

    Try{
        #TODO If we add another check that needs to check all mailboxes then lets make this a global variable
        Write-Verbose "Gathering All Mailboxes"
        $AllMBX = Get-Mailbox -ResultSize Unlimited -ErrorAction Stop
    }#Try
    Catch {
        Write-Warning "Issue gathering all local mailboxes to work on"
        $Global:IssueFound = $true
        Throw $_
        Exit 1
    }#Catch

    Write-Verbose "Validating that $ServiceAccount has the $ServicePermission permission on the Calendar for all mailboxes in scope"
    Foreach ($MBX in $AllMBX) {
        Write-Verbose "Checking : $($MBX.UserPrincipalName)"
        #Create Table Entry
        $TableEntry = [PSCustomObject]@{
            Mailbox = $MBX.UserPrincipalName
            CalendarPermissionStatus = 'UNKNOWN'
        }
        #TDOD I really wish there was an easier way to handle errors from the Get-MailboxFolderPermission cmdlet. It doesn't support -errorvariable without also forcing the error to the host
        #You also can't capture on exception itself since it's a remote session and all the errors exception types are the same
        #So in order to try and support all all known edge cases we really need to get tricky with our calls and error handling 
        $UserCalendarPath = "$($MBX.UserPrincipalName):\Calendar"
        $CompletedCalCheck = $FALSE
        do {
            Try {
                $CalPerm = Get-MailboxFolderPermission -Identity $UserCalendarPath -user $ServiceAccount -ErrorAction Stop
                $CompletedCalCheck = $TRUE
            }
            Catch {
                if ($_.Exception.Message -eq "There is no existing permission entry found for user: $ServiceAccount.") {
                    $CompletedCalCheck = $TRUE
                    #I dont think I need to to do this, but just being safe
                    $CalPerm = $NULL
                }
                #This will catch anyone whose mailbox is not using the English Latin alphabet
                #TODO depending on how often this happens it might make more sense to check the locality first then go this route, but in my current environment this is a rare exception and not worth the extra call
                Elseif ($_.Exception.Message -like "The operation couldn't be performed because '*:\Calendar' couldn't be found.") {
                    Write-Warning "Issue finding calendar, attempting a search : $($MBX.UserPrincipalName)"
                    Try {
                        $CalenderFolder = Get-MailboxFolderStatistics -Identity $MBX.UserPrincipalName -FolderScope Calendar -ErrorAction Stop
                        if ($CalenderFolder) {
                            #On the off chance there are more then one Calendar present we will assume the first one is the default one. I wish I could figure out a better way to handle this if it happens
                            $UserCalendarPath ="$($CalenderFolder[0].Identity -replace '\\',':\')"
                        }
                    }#TRY
                    Catch {
                        Write-Warning "Can't find Calendar : $($MBX.UserPrincipalName)"
                        $TableEntry.CalendarPermissionStatus = "ERROR : $_"
                        $CompletedCalCheck = $TRUE
                    }#CATCH
                }#Elseif ($_.Exception.Message -like "The operation couldn't be performed because '*:\Calendar' couldn't be found.") {
                ELse {
                    Write-Warning "Issue trying to pull info : $($MBX.UserPrincipalName)"
                    $TableEntry.CalendarPermissionStatus = "ERROR : $_"
                    $CompletedCalCheck = $TRUE
                }#Else
            }#Catch
        }While ($CompletedCalCheck -ne $TRUE)

        #Handle any hard errors encountered while looking for the  permission
        If ($TableEntry.CalendarPermissionStatus -ne "UNKNOWN") {
            $ChangeTable.add($TableEntry) | Out-Null
            Continue
        }#If ($TableEntry.CalendarPermissionStatus -ne "UNKNOWN") {

        #Handle adds and changes to the permission
        If (-not $CalPerm) {
            Write-Verbose "ADDING : $($MBX.UserPrincipalName)"
            Try {
                Add-MailboxFolderPermission -Identity $UserCalendarPath -User $ServiceAccount -AccessRights $ServicePermission -erroraction Stop | Out-Null
                $TableEntry.CalendarPermissionStatus = "ADDED"
            }#try
            Catch {
                Write-Warning "Issue adding permission : $($MBX.UserPrincipalName)"
                $TableEntry.CalendarPermissionStatus = "ERROR : $_"
            }#catch
        }#If ($CalPerm -eq $NULL) {
        #! I had a really hard time checking the accessrights as the reverse ($CalPerm.AccessRights -eq $ServicePermission) was giving me odd results
        ElseIf ($CalPerm.user.ADRecipient.Name -ne $ServiceAccount -or $ServicePermission -ne $CalPerm.AccessRights) {
            Write-Verbose "FIXING : $($MBX.UserPrincipalName)"
            Try {
                Set-MailboxFolderPermission -Identity $UserCalendarPath -User $ServiceAccount -AccessRights $ServicePermission -erroraction Stop | Out-Null
                $TableEntry.CalendarPermissionStatus = "CORRECTED"
            }#TRY
            Catch {
                Write-Warning "Issue fixing permission : $($MBX.UserPrincipalName)"
                $TableEntry.CalendarPermissionStatus = "ERROR : $_"
            }#Catch
        }#ElseIf ($CalPerms.user.ADRecipient.Name -ne $ServiceAccount -or (-not ($CalPerms.AccessRights -ne $ServicePermission))) {
        Else {
            Write-Verbose "NO ACTION NEEDED : $($MBX.UserPrincipalName)"
            Continue
        }#Else

        $ChangeTable.add($TableEntry) | Out-Null
    }#Foreach ($MBX in $AllMBX) {

    if ($ChangeTable.count -gt 0) {
        Write-Warning "$($ChangeTable.count) mailboxes whose calendar was either set or fixed so that the $ServiceAccount has the $ServicePermission permission"
        Return $ChangeTable |
            Sort-Object Mailbox
    }#if ($ChangeTable.count -gt 0) {
    Else {
        Write-Verbose "All Mailboxes have their calendar set so that the $ServiceAccount has the $ServicePermission permission"
    }
}#Function SetCalendarPermission {
Posted in Exchange, PowerShell | Leave a comment

Custom RBAC roles in Exchange Online do not work as expected when compared to on premises Exchange servers

At our company we have created multiple paired down RBAC roles for various business units like Information security and our various regional helpdesks. As we started our journey into Office 365 in 2019, we replicated these paired down RBAC roles in Exchange Online as well. This is mostly accomplished by creating new management role from an native parent role via the new-managementrole exchange PowerShell cmdlet and then using the Remove-ManagementRoleEntry exchange PowerShell cmdlet to remove any cmdlets in that role we don’t feel fit the custom role. One of the main reasons for doing so is providing just the needed cmdlets for a given role as opposed to overpromising. MS has a few articles on how to do this and here is an example of a role we created in our on premises Exchange environment for 1st level Exchange Server Support

New-ManagementRole –Name “EXCH 1st Level Support Database Copies” –Parent “Database Copies”
Remove-ManagementRoleEntry “EXCH 1st Level Support Database Copies\Remove-UMMailboxPolicy” -confirm:$FALSE
Remove-ManagementRoleEntry “EXCH 1st Level Support Database Copies\Remove-MailboxDatabaseCopy”-confirm:$FALSE

New-ManagementRole –Name “EXCH 1st Level Support Databases” –Parent “Databases”
Remove-ManagementRoleEntry “EXCH 1st Level Support Databases\Remove-StoreMailbox” -confirm:$FALSE
Remove-ManagementRoleEntry “EXCH 1st Level Support Databases\Remove-PublicFolderDatabase” -confirm:$FALSE
Remove-ManagementRoleEntry “EXCH 1st Level Support Databases\Remove-MailboxDatabase” -confirm:$FALSE
Remove-ManagementRoleEntry “EXCH 1st Level Support Databases\New-MailboxDatabase” -confirm:$FALSE
Remove-ManagementRoleEntry “EXCH 1st Level Support Databases\New-PublicFolderDatabase” -confirm:$FALSE

New-ManagementRole –Name “EXCH 1st Level Support Database Availability Groups” –Parent “Database Availability Groups"
Remove-ManagementRoleEntry “EXCH 1st Level Support Database Availability Groups\Remove-DatabaseAvailabilityGroupNetwork” -confirm:$FALSE
Remove-ManagementRoleEntry “EXCH 1st Level Support Database Availability Groups\Remove-DatabaseAvailabilityGroup” -confirm:$FALSE
Remove-ManagementRoleEntry “EXCH 1st Level Support Database Availability Groups\New-DatabaseAvailabilityGroupNetwork” -confirm:$FALSE
Remove-ManagementRoleEntry “EXCH 1st Level Support Database Availability Groups\New-DatabaseAvailabilityGroup” -confirm:$FALSE

New-ManagementRole –Name “EXCH 1st Level Support Exchange Servers” –Parent “Exchange Servers”
Remove-ManagementRoleEntry “EXCH 1st Level Support Exchange Servers\Remove-DatabaseAvailabilityGroupServer” -confirm:$FALSE

New-ManagementRole –Name “EXCH 1st Level Support Send Connectors” –Parent “Send Connectors”
Remove-ManagementRoleEntry “EXCH 1st Level Support Send Connectors\new-SendConnector” -confirm:$FALSE
Remove-ManagementRoleEntry “EXCH 1st Level Support Send Connectors\Remove-SendConnector” -confirm:$FALSE

New-RoleGroup –Name "EXCH 1st Level Support Exchange Tasks" –Roles “EXCH 1st Level Support Database Copies”, “EXCH 1st Level Support Databases”, “EXCH 1st Level Support Database Availability Groups”, “EXCH 1st Level Support Exchange Servers”, “EXCH 1st Level Support Send Connectors”, "Active Directory Permissions", "Transport Queues", "Monitoring", "Public Folder Replication", "Public Folders", "Disaster Recovery", "Address Lists", "Recipient Policies", "Mail Recipients", "Message Tracking", "View-Only Configuration", "Mail Enabled Public Folders", "Mail Recipient Creation", "Distribution Groups", "Security Group Creation and Membership", "Migration", "Move Mailboxes", "Mailbox Import Export", "Mailbox Search", "Legal Hold", "Retention Management" -ManagedBy EXCHAdmins -Description "EXCH 1st Level Support specific collection of cmdelts needed for Exchange management" -members "TechSupport-MSExchange"

Recently we ran into an issue with our helpdesk support staff not being able to download extended message trace reports from Exchange online after granting them ability to do so via a custom RBAC role group. When doing so they would receive a HTTP 403 error from Microsoft 365. The role group in question we created appeared to have all the necessary cmdlets to run standard and historical searches. Example of the role we created.

#Add all non destructive cmdlets from "HistoricalSearch"
New-ManagementRole -Name "US Helpdesk HistoricalSearch" -Parent "HistoricalSearch"
Get-ManagementRoleEntry "US Helpdesk HistoricalSearch\*" |Where-Object Name -in @('New-TenantExemptionInfo','Remove-PublicFolderMailboxMigrationRequest','Reset-EventsFromEmailBlockStatus','Set-EventsFromEmailConfiguration','Update-License') |
Foreach-object { Remove-ManagementRoleEntry -Identity "$($_.Role)\$($_.Name)" -Confirm:$False }

#Add all non destructive cmdlets from "Message Tracking"
New-ManagementRole -Name "US Helpdesk Message Tracking" -Parent "Message Tracking"
Get-ManagementRoleEntry "US Helpdesk Message Tracking\*" |Where-Object Name -in @('Add-AvailabilityAddressSpace','New-IntraOrganizationConnector','New-OrganizationRelationship','Remove-AvailabilityAddressSpace','Set-AvailabilityConfig','Set-IntraOrganizationConnector','Set-OrganizationRelationship','Set-UnifiedAuditSetting') |
Foreach-object { Remove-ManagementRoleEntry -Identity "$($_.Role)\$($_.Name)" -Confirm:$False }

#Add all the Get and test commands from "View-Only recipients"
New-ManagementRole -Name "US Helpdesk View-Only recipients" -Parent "View-Only recipients"
Get-ManagementRoleEntry "US Helpdesk View-Only recipients\*" |Where-Object Name -notmatch '(Get)|(Test)|(Write-AdminAuditLog)|(Start-AuditAssistant)|(Get-InboxRule)|(Start-HistoricalSearch)|(Stop-HistoricalSearch)|( Get-HistoricalSearch)'|
Foreach-object { Remove-ManagementRoleEntry -Identity "$($_.Role)\$($_.Name)" -Confirm:$False }

#Create Role Group contain all needed roles
$GroupSplat = @{
     Name = 'US Helpdesk Exchange Tasks'
     Roles = @("US Helpdesk View-Only Configuration", "US Helpdesk View-Only recipients", "US Helpdesk View Inbox Rules","US Helpdesk HistoricalSearch","US Helpdesk Message Tracking")
     ManagedBy = 'EXOAdmins'
     Description = "US specific collection of cmdlets needed for view only Exchange management"
     members = @("HelpDesk-NYC","HelpDesk-SFO","Helpdesk-BOS")
 }  New-RoleGroup @GroupSplat

In troubleshooting we created a test role with the same native roles with no cmdlets removed and the 403 issue went away. So we opened a ticket with Microsoft to assist us in finding the minimum number of cmdlets needed to download extended messages trace reports. After much back and forth what we found out was that

  1. The Historical Search and Message tracking Role in Exchange Online should allow all the needed functionality, but it their current state they do not due to technical limitations when it comes propagation of all the needed permission throughout the various components of Exchange Online in order to enable the matching functionality.
    • At some point in the future, this might work as expected, but for now it does not and there are significant technical barriers to getting it to work as expected.
  2. Similarly, the “View-Only Recipients” role has technical limitations if the role is paired down in any fashion. For any of the cmdlets contained in this role to work as expected it should be assigned wholly and not in a custom faction with a reduced cmdlet set.
  3. Furthermore, no role in Exchange Online should be paired down as Microsoft cannot guarantee it will work as expected in Exchange Online   

So in summary, you should only use the native roles in Exchange Online and any custom versions that remove cmdlets may or may not work as expected due to the distributed nature of Exchange Online

Posted in Exchange, Exchange Online, Microsoft 365, Office 365 | Tagged , , | Leave a comment

Inventorying what extensionAttributes are used in your Active Directory Environment

Recently we my team was asked what extensionAttributes were in use in my company’s Active Directory environment so I wrote this quick PowerShell script to search for any filled in extensionAttribute and compile a list of that objects

  • Name
  • ObjectClass
  • The name of the filled in extenstionAttribute
  • That attributes value

Then compile a summary list of all filled in attributes that provides

  • The name of the filled in extensionAttribute
  • How many objects it was found filled in on
  • The total unique filled in values found
  • The most frequent filled in value

as well as a list of extensionAttibutes not filled in

PowerShell Script

#Load the Active Directory Module
#This could be done with ADSI accelrator as well, but this is much easier to work with
Import-Module ActiveDirectory -ErrorAction Stop

#Table of known attrbitues
$AttributeTable = @{
    extensionAttribute = 1..15
    msExchExtensionAttribute = 16..45
    msExchExtensionCustomAttribute = 1..5
}

#Array to hold unused attributes
$EmptyList = @()

#Array list to hold all objects with at least one attibute filled in
$ExtensionEntries = [System.Collections.ArrayList]@()

foreach ($Attribute in $AttributeTable.GetEnumerator()) {
    [String]$AttributeBase = $Attribute.Name
    $Attribute.value |
        ForEach-Object {
            $CurrenntAttribute = "$AttributeBase$_"
            [array]$current = Get-ADObject -Filter "$CurrenntAttribute -like '*'" -Properties $CurrenntAttribute |
                Select-object Name, ObjectClass, @{N='AttributeName';E={$CurrenntAttribute}}, @{N='AttributeValue';E={$_.$CurrenntAttribute}}
            if ($current.count -ne 0) {
                $ExtensionEntries.AddRange($current)
            }
            Else {
                $EmptyList+=$CurrenntAttribute
            }
        }
}

Write-output "--Currently used Attributes--"
$ExtensionEntries |
    Group-Object AttributeName |
    Sort-Object Count -Descending |
    Select-Object Name, Count, @{N = 'TotalUniqueValues'; E = { ($_.Group.AttributeValue | Sort-Object -Unique).count } }, @{N = 'MostFrequentValue'; E = { ($_.Group.AttributeValue | Group-Object | Sort-Object Count -Descending | Select-Object -First 1).Name}}

Write-output "`n--Unused Attributes--"
$EmptyList | Sort-object

Example Output

--Currently used Attributes--

Name                            Count TotalUniqueValues MostFrequentValue
----                            ----- ----------------- -----------------
msExchExtensionAttribute16      18849              6007 {...
extensionAttribute1              1194               226 Dell Inc.
extensionAttribute3              1015               713 4 Seats
extensionAttribute2               934                71 Precision Tower 3420
extensionAttribute4               875               607 No Asset Tag
extensionAttribute10              721               310 CONTOSO
extensionAttribute5               687               389 20221119
extensionAttribute14              224               151 06/17/2010
extensionAttribute15              120                 8 PreDefault
msExchExtensionAttribute17         83                 1 False
extensionAttribute9                60                23 USA
extensionAttribute12               51                 1 MimeCastUSA
extensionAttribute13               17                 4 USR
msExchExtensionCustomAttribute1     3               989 moore
msExchExtensionAttribute45          1                 1 {"Account Name":"ScriptRunner","Account Type":"task","Description":"Run various automations","Application Name":"n/a","Windows Service Name":"n/a","Servers Windows Service will ru...

--Unused Attributes--
extensionAttribute11
extensionAttribute6
extensionAttribute7
extensionAttribute8
msExchExtensionAttribute18
msExchExtensionAttribute19
msExchExtensionAttribute20
msExchExtensionAttribute21
msExchExtensionAttribute22
msExchExtensionAttribute23
msExchExtensionAttribute24
msExchExtensionAttribute25
msExchExtensionAttribute26
msExchExtensionAttribute27
msExchExtensionAttribute28
msExchExtensionAttribute29
msExchExtensionAttribute30
msExchExtensionAttribute31
msExchExtensionAttribute32
msExchExtensionAttribute33
msExchExtensionAttribute34
msExchExtensionAttribute35
msExchExtensionAttribute36
msExchExtensionAttribute37
msExchExtensionAttribute38
msExchExtensionAttribute39
msExchExtensionAttribute40
msExchExtensionAttribute41
msExchExtensionAttribute42
msExchExtensionAttribute43
msExchExtensionAttribute44
msExchExtensionCustomAttribute2
msExchExtensionCustomAttribute3
msExchExtensionCustomAttribute4
msExchExtensionCustomAttribute5
Posted in Active Directory, Exchange, PowerShell | Leave a comment

Removing certain email addresses by domain or prefix from all mail objects in Exchange on-premises

Over the summer my company moved our email ingress and egress from on premises Cisco IronPort Gateways to Exchange Online Protection. While this moved us to a modern and cloud-based mail hygiene solution, we did lose some of custom mail routing schemes we implemented when we had full control of our mail flow. One such custom feature was the ability to selectively block SMTP addresses on any given mail object in our on-premises Exchange environment from external mail without having to block all external mail to the object.

This was accomplished by adding a second proxy address for each address we wanted to block from receiving external email at the Cisco Iron port level with ‘HIDE’. Such that when our Cisco IronPort’s did an LDAP lookup for an internal proxy address, if the same address was returned twice, (an SMTP and HIDE entry) then drop the message since that address should be internal only. In this example of the proxyaddresses on a given mail object:

hide:John.Mello@contoso.com
smtp:mello@mountainview.usa.com
smtp:mello@contosocloud.mail.onmicrosoft.com
smtp:mello@tailspintoys.com
SMTP:John.Mello@contoso.com

john.mello@contso.com would be blocked at the Cisco IronPort level since an LDAP search would return the following two addresses

hide:John.Mello@contoso.com
SMTP:John.Mello@contoso.com

The remaining address would be allowed through

smtp:mello@mountainview.usa.com
smtp:mello@contosocloud.mail.onmicrosoft.com
smtp:mello@tailspintoys.com

Once we moved to Exchange Online Protection, and the HIDE proxy addresses no longer functioned, we had to clean-up all the entries we had in our environment. To do so we created an Exchange Management Shell PowerShell script to do the following

  1. Run the script from the Exchange management shell
    1. The script assume you are getting non-deserialized objects. If you are not using the Exchange Management shell, then you will need to update it to take into account that you are getting deserialized objects
  2. Ensure you are viewing the entire forest
    1. This was needed for us since we have Exchange on premises in a resource domain and thus our public folders are in this domain while everything else is in a separate domain
  3. Grab all recipients
  4. Check if there are any HIDE proxyaddresses on the mail object. If not, then skip if so then
  5. Determine the Recipient detail type so we know which Set-* command to use to remove the addresses
    1. We could have just used Set-ADObject and instead to update the proxyaddresses property, but I wanted to keep this strictly within the commands available to Exchange Administrator
    1. In our environment we had a lot of contacts that required updating before we could edit the email addresses. So while we tried to force an update whenever we came across one, it still required manual intervention at the console to approve the change. We only had a few dozen in this state so after coming across the first one we filtered them out in the big run and then addressed them and manually accepted the update. In this case Set-ADObject would have allowed us to automate it, but wouldn’t fix the underlying need to update them
  6. Save the results to an array for later referencing in case we need to roll back

Outside the need to update our mail contacts here are some issues we ran into while running the script

  1. We encountered a number of distribution groups that gave the following error when we tried to remove the addresses : ‘Members can’t add themselves to security groups. Please set the group to Closed or Owner Approval for requests to join.’
    • In some research this was due to the fact that the distribution group was flipped to a Security using ADUC (Active directory users and computers) or vice verse, as opposed to done in Exchange
    • This put the distribution group on an inconsistent state as far as Exchange was concerned and we needed to run the following parameters when running the set-distributiongroup with the following parameters: -MemberJoinRestriction closed -MemberDepartRestriction closed
  2. We had exactly 1 object with a plus sign (‘+’) in it’s name, which the various set-* cmdlets do not like when using a distinguished name with a plus sign. In this case switching the script to leverage the Alias instead for each set-* cmdlet fixed that issue

Script

#This will ensure mail objects in other forests are included
Set-ADServerSettings -ViewEntireForest:$TRUE
$AllObjects = Get-Recipient -ResultSize Unlimited
[System.Collections.ArrayList]$Changelist = @()

ForEach ($MailObject in $AllObjects) {
    $TableEntry = [PSCustomObject]@{
        Name        = $MailObject.Name
        DistinguishedName = $MailObject.DistinguishedName
        PreviousEmailAddresses = $MailObject.EmailAddresses
        RecipientTypeDetails = $MailObject.RecipientTypeDetails
        RemovedAddress      = ""
        Result = "UNPROCESSED"
    }#$TableEntry = [PSCustomObject]@
    #If you were looking for specific doamin you could use a regex like such
    #[String[]]$ProxyAddressesToRemove = ($MailObject.EmailAddresses | Where-object SMTPAddress -match '^.*@DOMAIN\.com$').ProxyAddressString
    [String[]]$ProxyAddressesToRemove = ($MailObject.EmailAddresses | Where-object prefix -eq 'hide').ProxyAddressString
    If ($ProxyAddressesToRemove.count -ne 0) {
        $TableEntry.RemovedAddress = $ProxyAddressesToRemove
        Try{
            switch -regex ($MailObject.RecipientTypeDetails) {
                '^(MailUniversalSecurityGroup|MailUniversalDistributionGroup|RoomList)$' { $SetCommand = "Set-DistributionGroup"; break }
                '^(UserMailbox|TeamMailbox|SharedMailbox|RoomMailbox|EquipmentMailbox|DiscoveryMailbox)$' { $SetCommand = "Set-Mailbox"; break }
                'MailUser' { $SetCommand = "Set-MailUser"; break }
                '^(RemoteUserMailbox|RemoteRoomMailbox|RemoteEquipmentMailbox|RemoteSharedMailbox)$' { $SetCommand = "Set-RemoteMailbox"; break }
                'MailContact' { $SetCommand = "Set-MailContact"; break }
                'PublicFolder' { $SetCommand = "Set-MailPublicFolder"; break }
                'DynamicDistributionGroup' { $SetCommand = "Set-DynamicDistributionGroup"; break }
                Default {Throw "Unhandled RecipientTypeDetails: $($MailObject.RecipientTypeDetails)"}
            }#switch -regex ($MailObject.RecipientTypeDetails) {
            #We noticed that a good majority of the mail contacts in our enviroment were of 'ExchangeVersion' '0.0 (6.5.6500.0)'
            #Any attempt to update them resulted in a prompt to update, so in all instances we force an upgrade to our current version, which is '0.20 (15.0.0.0)'
            if ($SetCommand -ne 'Set-MailContact"' ) {
                .$SetCommand $MailObject.DistinguishedName -EmailAddresses @{Remove=$ProxyAddressesToRemove} -erroraction stop
            }#if ($SetCommand -ne 'Set-MailContact"' ) {
            Else {
                .$SetCommand $MailObject.DistinguishedName -EmailAddresses @{Remove=$ProxyAddressesToRemove} -ForceUpgrade:$true -Confirm:$false -erroraction stop
            }#Else
            $TableEntry.Result = 'HIDES REMOVED'
        }#Try
        Catch {
            $TableEntry.Result = $_
        }#catch
        $Changelist.add($TableEntry) | Out-Null
    }#If ($ProxyAddressesToRemove.count -ne 0) {
    Else{
        $TableEntry.Result = 'NO ACTION NEEDED'
        $Changelist.add($TableEntry) | Out-Null
    }#Else
    [System.GC]::Collect()
}#ForEach ($MailObject in $AllObjects) {
Posted in Uncategorized | Tagged , | Leave a comment

Removing 1000’s of folders in an on oprem Exchange Mailbox via EWS

Recently our helpdesk received a report of a user leveraging Outlook 2013 (x86) in a VM on a Mac to access a local mailbox (i.e. Not O365) crashing with an out of memory error. Before they reached out to my team (which are the stewards of Exchange 2016 and O365) they tired the following

The issue only went away once we removed all attached shared mailboxes (2 total). When adding them back, we noticed one of the shared mailboxes generated a lot of entries in the ‘Sync Issues’ folder. When we investigated the shared mailbox in question, it had 4207 user created folders. And while the user had access to this mailbox for years, it wasn’t until the gained access to another shared mailbox that this issue starting to occur. So we surmised that this mailbox in particular was the cause and was ripe for clean-up due to the amount of folders it contained. Using the get-mailboxfolderstatistics cmdlet we generated a CSV of all the user created folders along with some metrics around them for the owners of the shared mailbox to review for deletion

get-mailboxfolderstatistics PROBLEM_MAILBOX | Where-Object FolderType -eq "User Created" | Select Name, FolderPath, ItemsInFolder, ItemsInFolderAndSubfolders,@{N='FolderSize(MB)';E={$_.FolderSize.tomb()}}, @{N="FolderAndSubfolderSize(MB)";e={$_.FolderAndSubfolderSize.toMB()}}, NewestItemReceivedDate, OldestItemReceivedDate | Export-CsV -Path C:\Temp\FolderReview.csv

The owners then edited that list down to 3551 folders to delete. At that point we leverage EWS (Exchange Web Services) in PowerShell to delete the folders in question . This was a quick and dirty process as I didn’t have much time to create something more elegant. One of wish list items I had was figuring out how to properly weed out only top level folder paths to delete, but after spending 30 minutes trying to figure that out I decided it wasn’t worth the effort for this one off task. If I did delete a top level folder and the list had multiple child folders then our deletion attempt would just error out. We also used an account setup for application impersonation for this effort. Bare bones script below:

Make an Exchange EMS Connection
$ExchSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri YOUR_EXCHANGE_PS_URL -Authentication Kerberos -Name Exchange -ErrorAction Stop
Import-PSSession $ExchSession -allowclobber -ErrorAction Stop -DisableNameChecking -WarningAction SilentlyContinue *> $NULL

#Variables for EWS connection
$ExchangeVersion = "Exchange2013"
[String]$EWSManagedApiPath = "C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll"

#Impersonation Account
$ImpersonationAccountNAme = "YOUR IMPERSONATION ACCOUNT"
$ImpersanationAccountPassword = "YOUR IMPERSONATION ACCOUNT"

#Mailbox clean up info
$Mailbox = 'PROBLEM_MAILBOX@YOURDOMAIN.COM'
$TaggedFolders = Import-Csv -Path 'YOUR_CSV.CSV'
$AllFolders = get-mailboxfolderstatistics $Mailbox |
    Where-Object FolderType -eq "User Created"
$FoldersToRemove = $AllFolders |
    Where-Object FolderPath -in $TaggedFolders.FolderPath

# Load the needed dlls
Add-Type -Path $EWSManagedApiPath
Add-Type -AssemblyName System.Web

# Set EWS Service
$ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::$ExchangeVersion
$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)  

# Setup impersonation
$Service.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials($ImpersonationUserName, $ImpersonationPassword) -ErrorAction Stop
$Service.ImpersonatedUserId = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ImpersonatedUserId -ArgumentList "SmtpAddress", $Mailbox

$service.AutodiscoverUrl($Mailbox, { $true })  

#Function to convert the folderID from the Exchange Management shell to one we can us in EWS
function ConvertId {    
    param (
        $OwaId = "$( throw 'OWAId is a mandatory Parameter' )"
          )
    process {
        $aiItem = New-Object Microsoft.Exchange.WebServices.Data.AlternateId      
        $aiItem.Mailbox = $Mailbox     
        $aiItem.UniqueId = $OwaId   
        $aiItem.Format = [Microsoft.Exchange.WebServices.Data.IdFormat]::OwaId 
        $convertedId = $service.ConvertId($aiItem, [Microsoft.Exchange.WebServices.Data.IdFormat]::EwsId) 
        return $convertedId.UniqueId
    }
}

# Run through the folders ot delete
$FoldersToRemove | 
    ForEach-Object {
        Write-Output "Deleting Folder $($_.FolderPath)"
        try {
            $urlEncodedId = [System.Web.HttpUtility]::UrlEncode($_.FolderId.ToString())
            $folderid = New-Object Microsoft.Exchange.WebServices.Data.FolderId((Convertid $urlEncodedId))  
            $ewsFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service, $folderid)        

            $ewsFolder.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete)
            "Folder Deleted"
        }
        catch {
            Write-Warning "Problems deleting $($_.FolderPath)"
        }
    }

Posted in Uncategorized | Tagged , | Leave a comment

Inventorying Exchange custom attribute (extensionAttribute#\CustomAttribute#) usage within your environment

My team recently fielded a request for an unused Exchange custom attribute to use for various automation tasks around AD user groups. Since multiple teams at my company have access to various types of AD objects, we decided to do a quick inventory of the usage of those various attributes. Some quick background on those attributes

In total there are 50 total attributes that Exchange uses to extend the AD schema:

  • extensionAttribute1 to extensionAttribute15 (String)
  • msExchExtensionAttribute16 to msExchExtensionAttribute45 (String)
  • msExchExtensionCustomAttribute1 to msExchExtensionCustomAttribute5 (String: Multi-Valued)

MS recommends sticking to 1-15 and the 5 extension ones. 16-45 might be used for future Exchange features, but I personally haven’t seen this.

in Exchange Online, the data from extensionAttribute# are stored as CustomAttribute#. The multi valued ones also get synced up as ExtensionCustomAttribute#

Normally, in an Exchange Hybrid environment, Azure AD connect will sync the attributes that appear in Exchange Online. You can also extend the rest of the extension attributes as well as any locally created AD attributes.

The following PowerShell code will generate a list of which objects have those 50 attributes filled

$AttributeTable = @{
    extensionAttribute = 1..15
    msExchExtensionAttribute = 16..45
    msExchExtensionCustomAttribute = 1..5
}

$ExtensionEntries = [System.Collections.ArrayList]@()

foreach ($Attribute in $AttributeTable.GetEnumerator()) {
    [String]$AttributeBase = $Attribute.Name
    $Attribute.value | 
        ForEach-Object {
            $CurrenntAttribute = "$AttributeBase$_"
            [array]$current = Get-ADObject -Filter "$CurrenntAttribute -like '*'" -Properties $CurrenntAttribute | Select-object Name, ObjectClass, $CurrenntAttribute, @{N='FilledAttribute';E={$CurrenntAttribute}}
            if ($current.count -ne 0) {
                $ExtensionEntries.AddRange($current)
            }
        }
}

$ExtensionEntries | group-object FilledAttribute | Sort-object Name | Select-object Name, Count

The direct output will be like so:

Name                            Count
----                            -----
extensionAttribute1              1115
extensionAttribute2               842
extensionAttribute3               925
extensionAttribute4               799
extensionAttribute5               603
extensionAttribute9                63
extensionAttribute10              580
extensionAttribute12                1
extensionAttribute13               17
extensionAttribute14              235
extensionAttribute15              110
msExchExtensionAttribute16      17047
msExchExtensionAttribute17         72
msExchExtensionAttribute45          1
msExchExtensionCustomAttribute1     3

But you can also sort by ObjectClass, or any other properties you would like to add as part of the Get-ADObject call

Posted in Uncategorized | Tagged , , | Leave a comment

Hybrid free busy not working for just one user

A quick break down of the environment where this issue occurred

  • Office 365 in Hybrid with Exchange 2016
  • Local 2019 AD Domain with Azure AD Connect syncing to Office 365 every 30 minutes, with no AD write back
  • 350+ could mailboxes with 2200+ still on prem

I recently had an issue at work with my Exchange online mailbox not being able to pull free busy information from any of my company’s onprem mailboxes. My mailbox was migrated a little over 2 years ago and I’m not exactly sure when it stopped, but I only noticed as my company started to enact it’s return to work plan early in the summer of 2021 and I couldn’t see the free busy information of any of our conference rooms. All of which we haven’t migrated to Exchange Online yet. At first, I checked in with my teammates and discovered the issue was just with my account. From there I tried using the Microsoft Remote Connectivity Analyzer to troubleshoot, but got a generic error when attempting the various Exchange tests. I then checked for any odd attributes set on my account, but that turned up nothing obvious. I then tried doing a New-MoveRequest PowerShell cmdlet, which sometimes helps with fixing odd issues in Exchange Online. The hope in doing so is that the post move validations might correct whatever was causing the free buys look up issue. So, after exhausting all the options I could think of I opened a ticket with Microsoft Support. After going through the normal troubleshooting steps, they suggested that I clear out the following AD attributes and wait for a sync from our on premises AD to Azure AD

  • msExchRecipientDisplayType
  • msExchRecipientTypeDetails
  • msExchMailboxGuid

Before clearing these attributes, I made a note of the values and then waited for 2 Azure AD connect sync session. Unfortunately the issue persisted and my companies various other integrated SAAS platforms were throwing errors since those attributed weren’t populated for my account. At first, I tired running the Update-Recipient PowerShell cmdlet in both Exchange Online and Exchange on premises to see if it would re-populate those attributes. Sadly it did not so I added back the values I saved for each. About 24 hours later I checked if the issue was still present, and it was fixed! MS support wasn’t exactly sure why the issue was resolved, but the next time I run into an odd Exchange issue I’m going to try clearing those attributes first.

Posted in Uncategorized | Tagged , , | Leave a comment

Adventures in PowerShell dot sourcing

As my company has started our journey to Office 365, we have had to go back and update a lot of our daily checks, scripts, and automations we put in place for user accounts. As a specific example we have a disable user script that performs various actions on a user’s mailbox depending on the wishes of the manager of said user. Depending on where the mailbox is located (on premises or in the cloud), we need to use either the set-mailbox or set-remotemailbox commands. In order support both scenarios in our existing scripts we leveraged dot sourcing to store the needed commands per mailbox type to the same variables used throughout the script. So that when we need to perform those mailbox actions, we don’t need to add a separate if or case statement for each action for the location of the mailbox. We instead leverage the variable holding the command via dot sourcing. The snippet below shows that we use the msExchRemoteRecipientType attribute of the user to determine the location of the user mailbox, and then save the appropriate commands to the matching variables.

        #Checking which Set-*mailbox command to use depending on the user is on prem or in the cloud
        if ($FoundAccount.msExchRemoteRecipientType -ne $NULL) {
            Write-Verbose "$UserAccount : Has a remote mailbox, using *-remotemailbox for all Exchange tasks"
            $SetMBXCommand = "Set-RemoteMailbox"
            $GetMBXCommand = "Get-RemoteMailbox"
            $DisableMBXCommand = "Disable-RemoteMailbox"
        }#if ($FoundAccount.msExchRemoteRecipientType -ne $NULL) {
        Else {
            Write-Verbose "$UserAccount : Has a local mailbox, using *-mailbox for all Exchange tasks"
            $SetMBXCommand = "Set-Mailbox"
            $GetMBXCommand = "Get-Mailbox"
            $DisableMBXCommand = "Disable-Mailbox"
        }#Else

When performing an action on the user mailbox we do source the variable for the command, which will expand the variable and treat it as the command store in the variable.

.$DisableMBXCommand $UserAccount -ErrorAction Stop -Confirm:$FALSE
Posted in Exchange, Office 365, PowerShell | Leave a comment

Issues installing Windows Admin Center on Windows Server 2016

At my Job we have been playing around with Windows Admin Center, and while it’s pretty neat it has been a pain to install. We ran into multiple issues trying to get it to properly install in Gateway Mode on our Windows Servers 2016 management host that weren’t called out in the troubleshooting steps. Here are the steps we had to go through in order to get it to actually install

  1. You can’t install it over Remote Desktop
  2. You must have the following services enabled and running
    1. Windows Update
    2. Windows firewall

The last two were interesting since they aren’t really called out during the install. They are sorta called out in in the install log, but it’s not obvious that it’s directly related to the services not being enabled and running. Hopefully this helps out anyone else who has issues installing it on Windows server 2016.

Posted in Windows Admin Center | Leave a comment

Hunting down personal Microsoft accounts using an corporate Email address in O365

Recently my company started endeavored on setting up an O365 (Office 365) tenant in full Hybrid mode with our on-premise Exchange infrastructure. One of the issues we ran into as we started migrating pilot users was that some of our various Exchange integrations didn’t handle the authentication when the user in question had an existing personal Microsoft account associated with their corporate email address.  As an example, users in this state would se something like this when they logged into the O365 portal

My company leverages Blackberry UEM as our mobile email solution, and for any user migrated to Office 365 that was in this state could not user mobile email access via BlackBerry work due to the fact that it would receive this prompt and not be able to process it. We contacted MS and ask if we could be provided a list of all personal Microsoft accounts that were using a domain owned by my company, but for obvious legal reasons they could not. So the only option left to use was to test every email address in our environment against the office 365 login page (https://login.microsoftonline.com). After some testing we were able to properly inspect the login process and create a PowerShell script that could present an email address to this page and return not only if it was associated with a personal account but the federated gateway it was associated with. Here is a link to the Github repository for the PowerShell function in question. We tested it against about 8000 email addresses with no issues, but your mileage may vary.

https://github.com/Iczer1/O365-Scripts/blob/master/Check-PersonalAccount.ps1

Posted in Office 365 | Leave a comment