Category Archives: Exchange Online

M365Permissions v1.0.2 – Exchange Online

This version I’ve added Exchange Online to the supported resources!

All updates / changes since v1.0.1:

  1. Exchange Online Admin Roles
  2. Exchange Online Send As rights (groups, mailboxes etc etc)
  3. Exchange Online Send On Behalf (mailboxes)
  4. Exchange other Mailbox Permissions (full control etc)
  5. enhanced token handling (1 prompt instead of per API)
  6. auto setup and instructions at module load
  7. Ignore Current User support for the EntraID resource

For more info, check the M365Permissions module page, Github or PSGallery

Dynamic membership rule for Teams Room accounts

Teams Room accounts are usually excluded from conditional access. To do so, they have to be in a security group, which of course we don’t want to do manually.

Most companies choose to use a naming standard and simply use that as a rule to create an exclusion group. This is easy to circumvent, I can create a guest user / get invited with the right name et voila zero CA policies!

A better way is to identify the accounts based on their assigned licenses, e.g. Teams Rooms Basic (6af4b3d6-14bb-4a2a-960c-6c902aad34f3). This, however, is not supported as an Azure AD group membership rule as this is stored in the AssignedLicenses property which will throw an “Unsupported Property” error.

The assignedPlans property however does contain the GUID we need.

The following Azure AD Group dynamic membership rule only matches users that have a Teams Room Basic, Teams Room Standard or Teams Room Pro license:

(
	(
		user.assignedPlans -any (
			assignedPlan.servicePlanId -eq "8081ca9c-188c-4b49-a8e5-c23b5e9463a8"
			-and 
			assignedPlan.capabilityStatus -eq "Enabled"
		)
	) -or 
	(
		user.assignedPlans -any (
			assignedPlan.servicePlanId -eq "ec17f317-f4bc-451e-b2da-0167e5c260f9"
			-and 
			assignedPlan.capabilityStatus -eq "Enabled"
		)
	) -or 
	(
		user.assignedPlans -any (
			assignedPlan.servicePlanId -eq "92c6b761-01de-457a-9dd9-793a975238f7"
			-and 
			assignedPlan.capabilityStatus -eq "Enabled"
		)
	)
) -and not (
	user.assignedPlans -all (assignedPlan.servicePlanId -eq "")
)

if you want to do something similar for other licenses, here are the options/combinations:

https://github.com/MicrosoftDocs/entra-docs/blob/main/docs/identity/users/licensing-service-plan-reference.md

Ghost Mailusers

We had an odd case today that Msft support hasn’t fixed yet, but in case someone googles the error and finds this, I did find a workaround.

We had a MailUser object that was inactive (soft deleted), but couldn’t be permanently deleted using

Remove-MailUser -PermanentlyDelete -Identity $mailuser.ExternalDirectoryObjectId

The resulting error was:

This mail enabled user cannot be permanently deleted since there is a user associated with this mail enabled user in Azure Active Directory. You will first need to delete the user in Azure Active
Directory. Please refer to documentation for more details.

However, nothing was found in AzureAD (or deleted users/recycle bin), and nothing was present in our on-prem AD.

This user was deleted a long time ago, and now being rehired….but account creation was being blocked because the mail user was still claiming the email address of this user.

After some messing around, and not patiently waiting for Msft 1st line support to delete this corrupted MailUser, I discovered the RecalculateInactiveMailUser switch in the set-MailUser command.

Normally, you can’t modify the primary smtp / aliases of a soft deleted mailuser or mailbox….BUT, for some reason, if you supply RecalculateInactiveMailUser you can. So:

Set-MailUser -Identity $mailuser.ExternalDirectoryObjectId -EmailAddresses $newProxies -RecalculateInactiveMailUser

Worked fine! While

Set-MailUser -Identity $mailuser.ExternalDirectoryObjectId -EmailAddresses $newProxies

Failed with

The operation couldn’t be performed because object ‘270fc89f-0424-42d2-8f54-25796f74457b’ couldn’t be found on ‘DB8PR02A05DC001.EURPR02A005.PROD.OUTLOOK.COM’.

Legal hold and attribute conflicts in Exchange Online

Consider a large organisation, where deleted mailboxes are kept for many years.

Consider a new user with the same name as an offboarded user, or a user getting rehired, but policy stating that the deleted mailbox should not be restored. The user should start with a clean mailbox.

The inactive old mailbox still has the standardized primary smtp / alias etc and will not allow you to set these on the new user, causing a conflict.

Of course this should be handled during the offboarding process, where perhaps the email address of the user could be appended with _old123 before being soft-deleted.

Since that wasn’t the case here, I had to write a quick script to retroactively add a random string to all such attributes for all inactive mailboxes, hope it helps someone else with the same legacy 🙂

connect-exchangeonline
$inactiveMailboxes = Get-Mailbox -InactiveMailboxOnly -ResultSize unlimited

foreach($inactiveMailbox in $inactiveMailboxesNovib){
    $rand = Get-Random -Maximum 999
    $primary = $inactiveMailbox.PrimarySmtpAddress
    $newMailbox = New-Mailbox -InactiveMailbox $inactiveMailbox.DistinguishedName -name $inactiveMailbox.Name -FirstName $inactiveMailbox.DisplayName.Split(" ")[0] -LastName $inactiveMailbox.DisplayName.Split(" ")[1] -DisplayName $inactiveMailbox.DisplayName -MicrosoftOnlineServicesID $inactiveMailbox.PrimarySmtpAddress.Replace("@","_old$rand@") -Password (ConvertTo-SecureString -String 'W1pos3wsd03?!' -AsPlainText -Force) -ResetPasswordOnNextLogon $true    

    $newMailbox | Remove-Mailbox -Force -Confirm:$False
}

Using Exchange Web Service with Powershell to remove calendar appointments without notification

While helping out a client recently something in their migration went wrong, I ran into an interesting challenge. Calendars of users were merged, many many times. The resulting duplicates that shouldn’t be in user’s calendars could be identified easily: the mailbox was neither the organiser nor invited to these calendar events.

But how to remove them? Graph won’t allow you to do so without notifying the recipients (leading, potentially, to thousands of confused users).

Luckily, EWS DOES allow us to do so, and if you ever need to work with EWS (Office 365 Exchange Online) using Powershell, this code sample could come in handy 🙂

Add-Type -Path "C:\Users\jos\Desktop\net35\Microsoft.Exchange.WebServices.dll"

$Service = [Microsoft.Exchange.WebServices.Data.ExchangeService]::new()
$Service.Credentials = [System.Net.NetworkCredential]::new("admin@onedrivemapper.onmicrosoft.com" , "yourpassword")
$Service.Url = "https://outlook.office365.com/EWS/Exchange.asmx"

$maxDaysIntoTheFuture = 365

function Remove-ObsoleteCalendarItems{
    Param(
        $primaryEmailAddress #eg: admin@onedrivemapper.onmicrosoft.com
    )

    $folderid= new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Calendar,$primaryEmailAddress)   
    $Calendar = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service,$folderid)
    $Recurring = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Appointment, 0x8223,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Boolean); 
    $psPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)  
    $psPropset.Add($Recurring)
    $psPropset.RequestedBodyType = [Microsoft.Exchange.WebServices.Data.BodyType]::Text;

    #Define Date to Query 
    $currentDay = 0
    while($True){
        $StartDate = (Get-Date).AddDays($currentDay)
        $EndDate = $StartDate.AddDays(14)  
        $currentDay += 14

        if($currentDay -gt $maxDaysIntoTheFuture){
            break
        }

        $CalendarView = New-Object Microsoft.Exchange.WebServices.Data.CalendarView($StartDate,$EndDate,1000)    
        $fiItems = $service.FindAppointments($Calendar.Id,$CalendarView)
        if($fiItems.Items.Count -gt 0){
            $type = ("System.Collections.Generic.List"+'`'+"1") -as "Type"
            $type = $type.MakeGenericType("Microsoft.Exchange.WebServices.Data.Item" -as "Type")
            $ItemColl = [Activator]::CreateInstance($type)
            foreach($Item in $fiItems.Items){
                $ItemColl.Add($Item)
            } 
            [Void]$service.LoadPropertiesForItems($ItemColl,$psPropset)  
        }

        foreach($Item in $fiItems.Items){  
            if($Item.Organizer.Address -ne $primaryEmailAddress -and $Item.RequiredAttendees.Address -notcontains $primaryEmailAddress -and $Item.OptionalAttendees.Address -notcontains $primaryEmailAddress){
                $Item.RequiredAttendees.Clear() #this also works if no one is invited
                $Item.OptionalAttendees.Clear() #this also works if no one is invited
                $Item.Update([Microsoft.Exchange.WebServices.Data.ConflictResolutionMode]::AlwaysOverwrite,[Microsoft.Exchange.WebServices.Data.SendInvitationsOrCancellationsMode]::SendToNone)
                $Item.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::MoveToDeletedItems)
                write-host "deleted item $($Item.Subject) without notifying recipients"
            }
        }
    }
}

The required EWS DLL’s can also be found in my git repository: