<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:atom="https://www.w3.org/2005/Atom" xmlns:sy="https://purl.org/rss/1.0/modules/syndication/" xmlns:dc="https://purl.org/dc/elements/1.1/" xmlns:content="https://purl.org/rss/1.0/modules/content/" xmlns:wfw="https://wellformedweb.org/CommentAPI/" xmlns:slash="https://purl.org/rss/1.0/modules/slash/"
  version="2.0">
  <channel>
    <title>Santhosh Sethumadhavan</title>
    <atom:link
      href="https://docs.microsoft.com/archive/blogs/santhse/feed.xml"
      rel="self"
      type="application/rss+xml" />
    <link>https://docs.microsoft.com/archive/blogs/santhse/feed.xml</link>
    <description>Sharing the creations and discoveries from the Field</description>
    <lastBuildDate>Tue, 18 Jun 2019 10:57:34 GMT</lastBuildDate>
    <language>en-US</language>
    <sy:updatePeriod>hourly</sy:updatePeriod>
    <sy:updateFrequency>1</sy:updateFrequency>
    <item>
      <title>Script to Manage External Sharing Settings for OneDrive Personal Sites in Bulk</title>
      <link>https://docs.microsoft.com/archive/blogs/santhse/oofbui</link>
      <pubDate>Wed, 27 Dec 2017 15:23:36 GMT</pubDate>
      <dc:creator><![CDATA[Santhosh Sethumadhavan]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.technet.microsoft.com/santhse/?p=175</guid>
      <description><![CDATA[Here’s another script I developed for my customer and wanted to share here. This one is to manage...]]></description>
      <content:encoded><![CDATA[Here’s another script I developed for my customer and wanted to share here. This one is to<em> <a target="_blank" href="https://support.office.com/en-us/article/Manage-external-sharing-for-your-SharePoint-Online-environment-c8a462eb-0723-4b0b-8d0a-70feafe4be85?CorrelationId=ef669bdf-eb96-4909-92f9-6be6f8e014f2&amp;ui=en-US&amp;rs=en-US&amp;ad=US&amp;ocmsassetID=HA102849864" rel="noopener">manage</a></em> the External Sharing settings for Onedrive for Business sites (my sites) for a Sharepoint online Tenant. I have published the script to Technet Gallery, it briefs on the instructions on how to use it and you can download it from<em> <a target="_blank" href="https://gallery.technet.microsoft.com/office/Script-to-Manage-External-de426da2" rel="noopener"><strong>there</strong></a></em>

Note: The Onedrive Personal site is created when the user first access it. To apply the External sharing settings, pre-provision the onedrive for users who has no onedrive site provisioned yet by using <a target="_blank" href="https://docs.microsoft.com/en-us/powershell/module/sharepoint-online/Request-SPOPersonalSite?view=sharepoint-ps" rel="noopener">Request-SPOPersonalSite cmdlet</a>

This my first ever work on sharepoint, the script and the walk-through below are based on my research and learning. Please feel free to correct me on any of it that's not accurate.

<strong>Walk-through</strong> <strong>of the </strong><b>Design</b>:

I decided to re-use the UI I had created for the <em><a target="_blank" href="https://blogs.technet.microsoft.com/santhse/o365licensemgmtscript/" rel="noopener">O365 License Assignment</a></em> script, so I have less coding to do. Here's how the UI looks

[caption id="attachment_196" align="aligncenter" width="600"]<a href="https://gallery.technet.microsoft.com/office/Script-to-Manage-External-de426da2"><img src="https://msdnshared.blob.core.windows.net/media/2017/12/oofb.png" alt="" width="600" height="474" class="wp-image-196" /></a> <strong><em>OOFB External sharing Editor UI</em></strong>[/caption]

Some of the functionalities required for the tool were
<ul>
 	<li>Need to query for personal site and its properties with a user email address or UPN.</li>
 	<li>Option to query for all Onedrive Sites on a tenant and its properties</li>
 	<li>The tenant-level settings override any settings at the site collection level. Collect Tenant Level External Sharing settings and limit the User level with same or more restrictive than the Tenant level</li>
</ul>
The APIs that are available to query sharepoint online are as listed <em><a target="_blank" href="https://docs.microsoft.com/en-us/sharepoint/dev/solution-guidance/multigeo-onedrive" rel="noopener">here</a></em>
<ul>
 	<li>Microsoft Graph (preferred)</li>
 	<li>SharePoint CSOM</li>
 	<li>SharePoint REST API</li>
</ul>
SharePoint CSOM seem fit as I am using powershell code and I need to query for Tenant/User level External sharing settings and to modify the settings on User level.

I can use the <em><a target="_blank" href="https://blogs.technet.microsoft.com/heyscriptingguy/2016/07/27/manage-your-sharepoint-online-tenant-with-powershell/" rel="noopener">Sharepoint Client cmdlets</a></em> for most of the needs, but to query for Personal Site properties for an user, I will need to use the CSOM methods. Our Sharepoint expert  @brendankarl has provided the code I could use, on his <em><a target="_blank" href="https://blogs.technet.microsoft.com/fromthefield/2014/02/27/office-365-retrieve-user-profile-properties-using-csom-with-powershell/" rel="noopener">blog</a></em>.

But when legacy authentication (<em><a target="_blank" href="https://blog.areflyen.no/2017/06/18/problem-with-connecting-to-sharepoint-online-in-office-365-with-powershell-sharepoint-designer-and-other-3-party-tools/" rel="noopener">LegacyAuthProtocolsEnabled</a></em>) is turned off, the CSOM methods just will not work and will throw errors

<em>Exception calling "GetUserProfileByName" with "1" argument(s): "Server was unable to process request. ---&gt; Attempted to perform an unauthorized operation."</em>

<em> -and -</em>

<em>X-MSDAVEXT_Error: 917656; Access denied. Before opening files in this location, you must first browse to the web site and select the option to login automatically.</em>

I started to explore <em><a target="_blank" href="https://github.com/SharePoint/PnP-PowerShell" rel="noopener">Sharepoint PNP</a></em> Cmdlets, they seem to work well with Modern authentication. Another advantage of using PNP cmdlets is, Connect-PnPOnline -UseWebLogin option can be used and with this I don’t have to prompt user for authentication twice. I had completed the script using the PNP and sharepoint client cmdlets.  But soon encountered this <em><a target="_blank" href="https://github.com/SharePoint/PnP-PowerShell/issues/1211" rel="noopener">issue</a></em> where not all Onedrive Sites are returned.

So, I had to fall back to the CSOM methods, but this time I need to create SPO <em><a target="_blank" href="https://msdn.microsoft.com/en-us/library/microsoft.sharepoint.client.clientcontext.aspx" rel="noopener">ClientContext</a> </em>to support modern authentication and use it with <em><a target="_blank" href="https://msdn.microsoft.com/library/Microsoft.SharePoint.Client.UserProfiles.PeopleManager.GetPropertiesFor.aspx" rel="noopener">Peoplemanager</a></em>.

I used the Classes <em><a target="_blank" href="https://github.com/SharePoint/PnP-Sites-Core/blob/master/Core/OfficeDevPnP.Core/Utilities/CookieReader.cs" rel="noopener">CookieReader</a></em> and <em><a target="_blank" href="https://github.com/SharePoint/PnP-Sites-Core/blob/master/Core/OfficeDevPnP.Core/AuthenticationManager.cs" rel="noopener">GetWebLoginClientContext</a></em> from Sharepoint PNP modules to build the SPO client context without prompting for credentials to the user the second time. With this, I completed  the script and it did work well and faster than I expected.

I hope you find the script useful, please drop me a note for any suggestions or comments you may have.

&nbsp;]]></content:encoded>
    </item>
    <item>
      <title>Faster method to collect Send-as permissions</title>
      <link>https://docs.microsoft.com/archive/blogs/santhse/report-send-as</link>
      <pubDate>Sat, 09 Dec 2017 23:21:02 GMT</pubDate>
      <dc:creator><![CDATA[Santhosh Sethumadhavan]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.technet.microsoft.com/santhse/?p=156</guid>
      <description><![CDATA[I recently came across a situation where Send-as permissions for all Distribution group needs to be...]]></description>
      <content:encoded><![CDATA[I recently came across a situation where Send-as permissions for all Distribution group needs to be collected. The Exchange command Get-ADPermission was ran and it took forever to complete (more than a day, before the powershell session disconnected). Below was the code used..


<strong>Using Get-ADPermission</strong>

<pre>
$Result = @()
$DLList = Get-DistributionGroup -RecipientTypeDetails MailUniversalSecuritygroup -resultsize unlimited|Select Alias,name,PrimaryEmailAddress
foreach($DL in $DLList)
{
     $DLPerm = Get-ADPermission -Identity $DL.Name | where {($_.ExtendedRights -like "*Send-As*") -and ($_.IsInherited -eq $false) -and -not ($_.User -like "NT AUTHORITY\SELF")}|Select Identity,User
     $Result += $DLPerm

$Result|select Identity,User|Export-Csv SendAs_report.csv -NoTypeInformation 
</pre>

To overcome the challenges, I put together a script which uses AD module and it was much faster to produce the output.


<strong>Rewritten Code, using ActiveDirectory Module</strong>

<pre>
Start-Transcript -Path C:\temp\DLSend-asPerms.txt
Import-Module ActiveDirectory
Set-Location AD:
$MailDLs = Get-ADObject -Filter "(objectClass -eq 'group') -and (proxyAddresses -like '*')" -SearchScope Subtree -ResultPageSize 200000
Write-Host "Total $($MailDLs.Count) MailDLs found"

$i = 0

foreach($MailDL in $MailDLs){
     $i++
     $SendasEntries = $NULL
     Write-Progress -Status "Processing $i / $($MailDLs.Count)" -PercentComplete ($i / @($MailDLs).Count) * 100
     $SendasEntries = Get-ACL $MailDL.DistinguishedName | select -ExpandProperty Access | ?{($_.ActiveDirectoryRights -eq "ExtendedRight") -and ($_.objectType -eq "ab721a54-1e2f-11d0-9819-00aa0040529b") -and          
                      ($_.IsInherited -eq $false) -and $_.IdentityReference -ne "NT AUTHORITY\SELF" }
     if($NULL -eq $SendasEntries){
          Write-Host "DL $($MailDL.Name) - Has no Sendas Entries"
     }
    foreach($SendasEntry in $SendasEntries){
          Write-host "DL $($MailDL.Name) - $($SendasEntry.IdentityReference) has Sendas Perms"
    }
}
Stop-Transcript
</pre>
&nbsp;
<blockquote>The re-written code uses Get-ACL cmdlet to collect the Send-as permissions and for over 100,000 DGs it took a little over 3 hours to generate the report</blockquote>
This way of collecting send-as permissions is much faster than using Get-ADPermission cmdlet on a remote PS Session. The GUID <em>"<a href="https://technet.microsoft.com/en-in/library/ff406131.aspx">ab721a54-1e2f-11d0-9819-00aa0040529b</a>"</em> used on the script is the Object GUID for Send as permission.
I haven't made proper comments or added code to create a formal report, this is just to demonstrate the better way to collect the Send-as Permission report. Hope you find it useful.]]></content:encoded>
    </item>
    <item>
      <title>Bulk Assign / remove Office 365 Licenses – AzureAD V2 module based PowerShell script with UI</title>
      <link>https://docs.microsoft.com/archive/blogs/santhse/o365licensemgmtscript</link>
      <pubDate>Tue, 17 Oct 2017 22:03:41 GMT</pubDate>
      <dc:creator><![CDATA[Santhosh Sethumadhavan]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.technet.microsoft.com/santhse/?p=125</guid>
      <description><![CDATA[Managing licenses with office 365 has become easier with Azure-AD group based licensing and...]]></description>
      <content:encoded><![CDATA[Managing licenses with office 365 has become easier with Azure-AD group based licensing and multi-select option on the office portal.

But, there are many scenarios where Adminstrators go for direct License assignment and removal for a custom list of users. The powershell scripts my customer had for these purposes uses complex parameters and it didn’t support all the scenarios they needed. Also, any mistake on passing parameters would result in wrong assignment of licenses.

I created <strong><a href="https://gallery.technet.microsoft.com/office/Simple-UI-based-Office-365-ef8a9e68">this</a></strong> UI tool to help customer manage any kind of direct license management against a custom list of Users from an input file or by running a query against AzureAD (Get-AzureADUser). It is UI based and flexible to use. This script uses new<strong> AzureAD V2 module</strong> which implements the Graph API in PowerShell and provides access to newer functionality.

The script can be downloaded from <a href="https://aka.ms/o365licensemgmtscript">https://aka.ms/o365licensemgmtscript</a>

<img src="https://msdnshared.blob.core.windows.net/media/2017/10/o365lic.png" alt="" width="1247" height="1021" class="alignnone size-full wp-image-135" />

&nbsp;
<ul>
 	<li>This script launches Windows Presentation framework based UI form.</li>
 	<li>Provides users with options either to run Online Query or process users from a CSV list.</li>
 	<li>Using a License picker window, users can Add and/or Remove licenses.</li>
 	<li>Apply it on all users or selected users from the list, all at once.</li>
 	<li>This script will only append to or remove the existing user licenses and plans.</li>
 	<li>At Present, this script requires no Input parameters, but default switches like -Verbose can be added to view progress on the Powershell window.</li>
 	<li>It creates a log by default on the script's directory which includes the Licenses and runtime details.</li>
</ul>
Note: <em>This script will only append to or remove the existing user licenses and plans. </em>It doesn’t replace any Licenses.

I hope you find this script useful when dealing with direct assignment / removal of Office 365 licenses.

<span style="font-size: 10.0pt;font-family: 'Segoe UI',sans-serif;color: black">Please let me know if you find it useful or for any suggestions you may have.</span>

Thank you]]></content:encoded>
    </item>
    <item>
      <title>EWS Powershell Script to find the ‘True’ Read Status of an email message</title>
      <link>https://docs.microsoft.com/archive/blogs/santhse/get-readstatus</link>
      <pubDate>Mon, 16 Oct 2017 19:01:30 GMT</pubDate>
      <dc:creator><![CDATA[Santhosh Sethumadhavan]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.technet.microsoft.com/santhse/?p=106</guid>
      <description><![CDATA[Some business requires to track the READ status of some critical messages that was sent to all or...]]></description>
      <content:encoded><![CDATA[Some business requires to track the READ status of some critical messages that was sent to all or some of the users. <em>They want to know if the email was delivered to the Mailbox and was read by the user</em>.

<strong><a href="https://gallery.technet.microsoft.com/Script-to-find-the-true-1294c4ed">Here’s</a> </strong>another script I created for one of my customer with such requirement. This is an EWS PowerShell script that is used to find the “True” read status of an email message.

Below is the report Generated from the script.

<img src="https://msdnshared.blob.core.windows.net/media/2017/10/get-readstatusoutput.png" alt="" width="998" height="500" class="alignnone size-full wp-image-116" />

To automate the complete solution, you could create a script, but it is not in scope of this blog post.
<ul>
 	<li>Use Get-Messagetrackinglog and/or Get-Messagetrace (if you are in Hybrid, you will need to track both) to collect the recipients details to whom the message was delivered to.</li>
 	<li>Use an account that has <a href="https://msdn.microsoft.com/en-us/library/office/dn722376(v=exchg.150).aspx">Impersonation permissions</a> on On-perm and Online if you are in Hybrid, and use it with this script to find the read status of the given Message. Something like below..<img src="https://msdnshared.blob.core.windows.net/media/2017/10/get-readstatus.png" alt="" width="1059" height="669" class="alignnone wp-image-115" /></li>
</ul>
Get-ReadStatus is the function exported by the script. The script can be used against On-perm or Online mailboxes to find the Read / Unread status of an email message. The function returns a Custom Object with the READ status of the email. It first searches for the email message in the Inbox of the target mailbox with provided Search filters and if not found, it searches the Entire Mailbox using Search folders.

After reading the message, user may mark the message as 'UnRead' to review it later. This script retrieves the 'EverRead' status, an extended MAPI property value to report the Read Status of the message.

<strong>Features:</strong>
<ul>
 	<li>Uses Autodiscover to find the EWS URL, but for increased performance, one can specify the EWSUrl parameter manually.</li>
 	<li>Can be used against Office 365 or On-Perm mailbox. If the Email message was sent to Online and On-Perm mailboxes, use an account that has impersonation permissions on both org for easy tracking.</li>
 	<li>Searches Inbox first and if the message is not found in the Inbox Folder, then the script searches the entire mailbox.</li>
 	<li>Error message is returned on the result object, no separate log file parsing is required to find the complete status</li>
</ul>
<strong>Prerequisite   : </strong>Requires Powershell V3 or Higher.

<strong>                       : </strong>Requires EWS API 1.2 or higher installed on the machine.

&nbsp;

Please leave a comment if you find it useful or for any questions.]]></content:encoded>
    </item>
    <item>
      <title>Easily Multi thread Powershell commands and scripts</title>
      <link>https://docs.microsoft.com/archive/blogs/santhse/invokeall</link>
      <pubDate>Mon, 16 Oct 2017 13:24:06 GMT</pubDate>
      <dc:creator><![CDATA[Santhosh Sethumadhavan]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.technet.microsoft.com/santhse/?p=75</guid>
      <description><![CDATA[Update: I had recreated the same functionality in C# using Tasks. The Powershell binary module has...]]></description>
      <content:encoded><![CDATA[<blockquote>Update: I had recreated the same functionality in C# using <a href="https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/task-based-asynchronous-programming" rel="noopener" target="_blank">Tasks</a>. The Powershell binary module has some added features when compared with the below PS Version. <a href="https://github.com/santhse/InvokeAll/releases/download/V1.1/Invokeall_V_1.1.dll" rel="noopener" target="_blank">

Download </a> the powershell binary module from <a href="https://github.com/santhse/InvokeAll" rel="noopener" target="_blank">Github repo</a>. 

If you require the PS1 version of the script, download it here.
<a href="https://blogs.technet.microsoft.com/santhse/invokeall/invokeallv2-5-renametops1/" rel="attachment wp-att-215">invokeallV2.5.renametoPS1</a>
Rename the file to .ps1 to use it on the PS window </blockquote>

I want to share this script, I wrote for my customer few months back. This one is special as it is my first script without any Exchange commands in it :-) .

There are many everyday tasks in support and administration which takes long time to complete. Some of the common tasks are
<ul>
 	<li>               Collecting asset information, configuration from all the workstations</li>
 	<li>               Collect mailbox statistics or other details for thousands of objects...etc.</li>
</ul>
I had seen some commands/scripts that was executed for large number of objects and took more than 24 hours to complete. Scripting is all about reducing time spent performing mundane tasks, but seems it is not enough. The commands simply take so long just because of too many objects it has to process sequentially. Then comes the idea of Multi-threading in powershell, using powershell background runspaces seem fit for the job than using Powershell jobs. Background Runspaces are more <a href="https://learn-powershell.net/2012/05/13/using-background-runspaces-instead-of-psjobs-for-better-performance/">faster and light weight</a> to process more number of tasks in parallel.

I started integrating runspaces to achieve multi-threading on my scripts, but for every script, the requirement is different and coding it consumes time. To overcome this challenge, I decided to create a generic function that can be used on all the commands and scripts with very less modification to the actual command. Not all Administrators who uses powershell for the daily administrative tasks and reporting are well versed on scripting. With all that kept in mind, Invoke-all function was created, I have made the function very generic, easy-to-use and lightweight as possible.

The script can be downloaded from <a href="https://aka.ms/invokeall">https://aka.ms/invokeall</a>

<img src="https://msdnshared.blob.core.windows.net/media/2017/10/invoke-all1.png" alt="" width="1231" height="545" class="alignnone size-full wp-image-105" />

<strong>Usage example:</strong>

#Actual command:
[powershell]
Get-Mailbox -database 'db1' | foreach{ Get-MailboxFolderStatistics -Identity $_.name -FolderScope &quot;inbox&quot; -IncludeOldestAndNewestItems } | Where-Object{$_.ItemsInFolder -gt 0} | Select Identity, Itemsinfolder, Foldersize, NewestItemReceivedDate
[/powershell]
Same command with Invoke-all
[powershell]
Get-Mailbox -database 'db1' | Invoke-All{ Get-MailboxFolderStatistics -Identity $_.name -Folder Scope &quot;inbox&quot; -IncludeOldestAndNewestItems }  | Where-Object{$_.ItemsInFolder -gt 0} | Select Identity, Itemsinfolder, Foldersize, NewestItemReceivedDate
[/powershell]
<em>As you see it is that easy to use. Imagine, 100s of DBs and using invoke-all function is 10x faster than the conventional way.</em>

<strong>Note:</strong> The scriptblock is the first parameter to the Invoke-all function and it should only contain the first command and its parameters that needs to be executed. Any Filtering that is done using another command (Where-Object) must be outside of the script block as mentioned in the above example.

<strong>Features</strong>:
<ol>
 	<li>Easy to use.</li>
 	<li>When used with Cmdlets or Remote PowerShell, it automatically detects and loads the (only) required modules to the sessions.</li>
 	<li>Checks if it can complete the first job without errors before scheduling all the jobs to run in parallel.</li>
 	<li>By default, the script processes the jobs in batches. With batching, the resource consumption is very less and its proportional to the batch size which can be adjusted as required.</li>
 	<li>Ability to pause the Job scheduling in-between batches. This is very useful if too many requests are overloading the destination.</li>
 	<li>Creates a runtime log by default.</li>
</ol>
The Invoke-all is a function exported and is a wrapper like function that takes input from Pipeline or using explicit parameter and process the command concurrently using runspaces. I have mainly focused on Exchange On-Perm (and some Online Remote powershell (RPS) commands) while developing the script. Though I have only tested it with Exchange On-Perm Remote sessions and Snap-ins, it can be used on any powershell cmdlet or function.

I haven’t tested it with any Set-* commands, but it is possible to use this script to modify objects in parallel. Carefully chose the command so that there is no contention created when using multiple threads.

Exchange remote PowerShell uses Implicit remoting, the Proxyfunctions used by the Remote session doesn’t accept value from pipeline. Therefore, the Script requires you to declare all the Parameters that you want to use in the command when using such commands. See examples on how to do it.

It can handle cmdlets and functions from any modules. I have updated script to handle external scripts as well. I will see to include applications handling in the next version, but it can be achieved in this version with a simple script as shown in the examples that can execute your EXE or application.

<strong></strong>Please refer to the comments on the script to learn more about the technique used on the script or please leave a comment here. Hope you find this one useful to save some time on your long running powershell commands.]]></content:encoded>
    </item>
    <item>
      <title>Unlicensed Mailboxes on Exchange Online</title>
      <link>https://docs.microsoft.com/archive/blogs/santhse/hello-world</link>
      <pubDate>Wed, 29 Mar 2017 12:33:42 GMT</pubDate>
      <dc:creator><![CDATA[Santhosh Sethumadhavan]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.technet.microsoft.com/santhse/?p=1</guid>
      <description><![CDATA[Hello All,

First off, I am not a usual blogger or an active technical forums participant :) ....]]></description>
      <content:encoded><![CDATA[Hello All,

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.

<hr />

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.

<hr />


[powershell]#################################################################################
# 
# 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 &quot;The MSOLService is not connected, please run Connect-MSOLService and try again&quot;
    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 &quot;Unable to find the session configuration, please reconnect to Exchange online powershell session and try again. &quot;
    Exit
}

#Check if the Outputfile exists, if yes just rename and continue
[STRING]$Outfile = &quot;C:\Temp\UnlicensedMbxs_$(Get-Date -format &quot;yyyyMMdd&quot;).CSV&quot;
if(Test-Path $Outfile){
    Rename-Item $Outfile &quot;$Outfile.old&quot;

}

#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
}[/powershell]
]]></content:encoded>
    </item>
  </channel>
</rss>