Unlicensed Mailboxes on Exchange Online

First off, I am not a usual blogger or an active technical forums participant :) . But, I've got help from many blogs and forums many a times and I thought it would be mindful to repay by sharing the some of the work I do on my field engagements.

Btw, my name is Santhosh, work as a Sr PFE with Microsoft and been with Exchange Team for almost 8 years now. This is my first entry on my personal blog, although am more active on Exchange on-perm side of things, i thought it would be splendid to start it off with an Exchange Online post.

One of my customer has Hybrid environment and often does on-boarding of mailboxes to Exchange Online. Out of the Odds, one mailbox was not licensed and was removed from Exchange Online. It was too late when the user reported the loss of mailbox to helpdesk, and when it was brought to our notice, the Mailbox was permanently deleted from Exchange Online. The user got a brand new mailbox. As of now, Exchange Online permits usage of Unlicensed mailboxes for 30 days from the Mailbox creation date. You may find mailboxes that are not licensed and accessible even after 30 days, the mailbox stay longer sometimes as the workflow deletes the mailboxes no sooner than 30 days Old. It is recommended to assign licenses within the 30 day grace period.

You could retrieve the list of unlicensed mailboxes from Portal or using powershell.

Below is the sample powershell code I provided for them to use to schedule a report generation monthly and work on it. As this is a sample and a small script, I will just drop it here.

#################################################################################
#
# The sample scripts are not supported under any Microsoft standard support
# program or service. The sample scripts are provided AS IS without warranty
# of any kind. Microsoft further disclaims all implied warranties including, without
# limitation, any implied warranties of merchantability or of fitness for a particular
# purpose. The entire risk arising out of the use or performance of the sample scripts
# and documentation remains with you. In no event shall Microsoft, its authors, or
# anyone else involved in the creation, production, or delivery of the scripts be liable
# for any damages whatsoever (including, without limitation, damages for loss of business
# profits, business interruption, loss of business information, or other pecuniary loss)
# arising out of the use of or inability to use the sample scripts or documentation,
# even if Microsoft has been advised of the possibility of such damages
#
#################################################################################
#
# Author: Santhosh Sethumadhavan santhse@microsoft.com
#
#################################################################################
#Requires -Modules Msonline
Set-StrictMode -Version Latest
#Check if MSOLService is connected
try
{
Get-MsolDomain -ErrorAction Stop | Out-Null
}
catch
{
Write-Error "The MSOLService is not connected, please run Connect-MSOLService and try again"
Exit
}
#Find the Remote session that is currently active. If for some reasons the session broke inbetween the script, user needs to fix it and re-run the script
$RemoteSession = Get-PSSession | Where-Object{$_.state -eq 'opened' -and $_.configurationname -eq 'Microsoft.Exchange' -and $_.ComputerName -eq 'outlook.office365.com' } | Select-Object -First 1
If(-not $RemoteSession){
Write-Error "Unable to find the session configuration, please reconnect to Exchange online powershell session and try again. "
Exit
}
#Check if the Outputfile exists, if yes just rename and continue
[STRING]$Outfile = "C:\Temp\UnlicensedMbxs_$(Get-Date -format "yyyyMMdd").CSV"
if(Test-Path $Outfile){
Rename-Item $Outfile "$Outfile.old"
}
#LicenseReconciliationNeededOnly - indicates whether an additional license assignment is required to bring the user into compliance
#Lets just collect all of them
$ReconciliationUsers = Get-MsolUser -All -LicenseReconciliationNeededOnly | Select-Object UserPrincipalName, IsLicensed, LicenseReconciliationNeeded, Licenses
$Params = @{UserPrincipalName=[STRING];MailboxCreatedON=[DateTime];IsLicensed=[BOOL]$false;LicenseReconciliationNeeded=[BOOL]$false;DaysUnlicensed=[INT]-1;HasAnyExcLicense=[BOOL]$False}
#We are interested only on Unlicensed mailboxes, If the MailboxCreatedON is blank, there is no mailbox provisioned for this user on Online.
#The script outputs everything in to the CSV file, Users can filter on MailboxCreatedON field for filtering Unlicensed mailboxes.
Foreach($User in $ReconciliationUsers){
#Create the Custom object to store the results
$UserObj = New-Object -TypeName PSCustomObject -Property $Params
$UserObj.UserPrincipalName = $user.UserPrincipalName
$UserObj.IsLicensed = $user.IsLicensed
$UserObj.LicenseReconciliationNeeded = $user.LicenseReconciliationNeeded
$Userobj.MailboxCreatedON = Get-Mailbox $User.UserPrincipalName -ErrorAction silentlycontinue | Select-Object -ExpandProperty WhenMailboxCreated
#If user has mailbox, check how many days it is there unlicensed
if($UserObj.MailboxCreatedON){
$UserObj.DaysUnlicensed = ((Get-Date) - $UserObj.MailboxCreatedON).Days
}
#Check if there is any Exchange license assgined for this user, if yes, Include it in report
if( @($user.Licenses).count){
if($User.Licenses[0].ServiceStatus | Where-Object {$_.serviceplan.servicetype -eq 'Exchange' -and $_.ProvisioningStatus -eq 'Success'}){
$UserObj.HasAnyExcLicense = $true
}
}
#Append the output to the file as the script collects it
$UserObj | Export-Csv $Outfile -Append -NoTypeInformation
}

This script is great, except that it does not seem to capture all unlicensed mailboxes. After running it daily for the past couple weeks as we migrate mailboxes from on-premises to EXO (~500-600 total so far), I discovered about 200 unlicensed mailboxes that are not showing up in the report generated from this script. I discovered these mailboxes by running a full licensing report and a full mailbox report, and comparing with vlookup.

I’m in the process of investigating what is unique about these ~200, but do you have any ideas as to what could be causing this?

The script uses the LicenseReconciliationNeeded attribute of the MSOLUser to identify additional licenses that are required. Are you running the script as soon as the mailbox was migrated? Wondering if it is just a timing issue.
I will check to see how long it requires for the service to update this attribute and respond to you.

The attribute LicenseReconciliationNeeded seems to retrieve only the mailboxes that were migrated and not yet licensed.
Mailboxes that used to have a license, but then removed, are not reported using that attribute.
To get them you can use the following command
Get-MsolUser -UnlicensedUsersOnly | Where-Object MSExchRecipientTypeDetails -eq 2147483648