All-in-one Connect-365 cmdlet: Part 1 – MSOnline, Exchange Online, Skype for Business Online

It gets tedious connecting to all of the Office 365 PowerShell components. MSOnline, Exchange Online, Skype for Business Online, and more – they all need to be connected individually. This isn’t always so bad day-to-day, but when you have a big project that involves repeatedly checking data in both, it’s a huge waste of time. Also, our Get-MITUser function currently relies on being connected to both MSOnline and Exchange Online, and we will soon be adding Skype for Business Online. For both Get-MITUser and otherwise, it will be more efficient to automate the process of connecting to them.

Some people may stick something like the below in their PowerShell profile, where $Credential is previously set – for example either by prompting for it with Get-Credential, or decrypting it from a saved secure string.

     
        Connect-MsolService -Credential $Credential

        $ExSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $Credential -Authentication Basic -AllowRedirection
        Import-PSSession -Session $ExSession -DisableNameChecking

        $CsOnlineSession  = New-CsOnlineSession -Credential $Credential
        Import-PSSession -Session $CsOnlineSession -DisableNameChecking

This is perfectly fine if it suits your needs. However I like to create more versatile cmdlet that handles all of this. In particular:

  1. Connecting to Office 365 PowerShell is paramount, but if it can’t connect to Exchange Online PowerShell or Skype for Business Online PowerShell, we want it to just warn the user. This could either be because the required modules aren’t installed, or the 365 tenant does not include Exchange or Skype.
  2. It’s useful to be able to easily disconnect and reconnect, particularly to different Office 365 tenants using different usernames and passwords. We don’t want to have to close and reopen PowerShell.
  3. In our module we would like to be able to set some script-scope variables that indicate the connectivity status, allowing other cmdlets like Get-MITUser to rely on this. I also like to export some of these variables so that they can be viewed for troubleshooting.

(Another thing I have done in the past is, for 365 Partners with partner delegation to their customers’ 365 tenants, allow selection of the customer / tenant from a list (a makeshift table using Out-GridView) to avoid using messy -TenantId parameters and New-PSSession commands. This is very useful for partners but also very complex, and has no use for non-partners, so I won’t be covering it now.)

Putting it together

Script-scope variables

First, outside of any functions we declare and initialise our script variables.

#region Exported Variables

    # These MilneIT_* variables are all exported.

    $script:MilneIT_365_MSOnline_Connected       = $false   # Are we connected to Office 365?
    $script:MilneIT_365_MSOnline_ConnectionError = $null    # Any error connecting to Office 365

    $script:MilneIT_365_Exchange_Connected       = $false   # Are we connected to Exchange Online?
    $script:MilneIT_365_Exchange_PSSession       = $null    # The Exchange PSSession
    $script:MilneIT_365_Exchange_Module          = $null    # The temporary module created by Import-PSSession
    $script:MilneIT_365_Exchange_ConnectionError = $null    # Any error connecting to Exchange Online

    $script:MilneIT_365_CsOnline_Connected       = $false   # Are we connected to Skype for Business Online?
    $script:MilneIT_365_CsOnline_PSSession       = $null    # The Skype For Business Online PSSession.
    $script:MilneIT_365_CsOnline_Module          = $null    # The temporary module created by Import-PSSession
    $script:MilneIT_365_CsOnline_ConnectionError = $null    # Any error connecting to Skype For Business Online

#endregion Exported Variables

At the end of the script, we also modify our Export-ModuleMember line to export any script variables beginning with MilneIT_. When the module is imported, these can be accessed as global variables. If / when the module is removed, these variables are also removed alongside it.

Export-ModuleMember -Function "*-MIT*" -Variable "MilneIT_*"

Connect-MIT365

Now we begin our Connect-MIT365 cmdlet. This takes five parameters:

  1. A mandatory $Credential parameter, for the username and password for connecting to the Office 365 components. If this is not supplied, the user will be prompted.
  2. Switches $Office365, $ExchangeOnline and $SkypeForBusinessOnline which determine whether we connect to these individual components. By default we try to connect to everything.
  3. A $Force switch, which handles how the cmdlet handles the situation where we are already connected.
    Function Connect-MIT365 {
        [cmdletbinding()]
        Param(
            [Parameter(mandatory=$true)]
            [System.Management.Automation.PSCredential]
            $Credential,

            [switch]
            $Office365 = $true,

            [switch]
            $ExchangeOnline = $true,

            [switch]
            $SkypeForBusinessOnline = $true,

            [switch]
            $Force = $false
        )
        try {
            

We now connect to our three 365 components one-by-one. You will notice some recurring themes throughout:

  • If we are already connected, we warn the user and don’t try to connect. This is unless -Force is specified, in which case we disconnect first. (We define the Disconnect-MIT365 cmdlet later on).
  • If we fail to connect, we either write an error or a warning, but we do not halt the entire command.

First, we try to connect to Office 365 / “MSOnline” PowerShell. This is fairly straightforward as we just need to run Connect-MsolService. There are very few “legitimate” reasons why we would fail to connect to Office 365 PowerShell but succeed in connecting to Exchange Online or Skype for Business Online, so we write an error if we fail (as opposed to just warn).

            <#
                Office 365
            #>

            if ($script:MilneIT_365_MSOnline_Connected -and (-not $Force) ) {
                Write-Warning 'Office 365: Not attempting as already connected.  Use -Force to override.'
            } elseif ($Office365) {
                if ($Force) {
                    Disconnect-MIT365 -Office365:$true -ExchangeOnline:$false -SkypeForBusinessOnline:$false
                } #end if ($Force)
                Write-Verbose '[Connect-MIT365] Office 365: Attempting to connect...'
                try {
                    Connect-MsolService -Credential $Credential -ErrorAction Stop
                    Write-Debug '[Connect-MIT365] Office 365: Connected.'
                    $script:MilneIT_365_MSOnline_Connected = $true
                } catch {
                    <#
                        Unlike with Exchange Online & Skype for Business Online, if we can't connect here
                        we really want to display an error rather than a warning.
                        But we do still want to continue regardless, so we don't throw.
                    #>
                    $script:MilneIT_365_MSOnline_ConnectionError = $_
                    $ErrorMessage = 'Office 365: Could not connect: {0}.  The full error is saved to $MilneIT_365_MSOnline_ConnectionError' -F $_
                    Write-Error $ErrorMessage
                } #end catch
            } else {
                Write-Verbose '[Connect-MIT365] Office 365: Not attempting to connect.'
            } #end else

Next we connect to Exchange Online. This is where it gets tricky as we need to connect to and import a PSSession. Furthermore, unlike when importing a PSSession interactively, we need to worry about scope here.

Import-PSSession normally creates a temporary module containing the session’s cmdlets / functions, and imports that module into the local scope. Running this in a function means that the cmdlets and functions will be accessible within the cmdlet/function but not usable by the user on the interactive console. Import-PSSession has no way to change this behaviour. However, we can first run Import-PSSession, and then afterwards run Import-Module on the temporary module and specify to import it into the global scope, meaning that the cmdlets can be accessed on the interactive console.

            <#
                Exchange Online
            #>

            if ($script:MilneIT_365_Exchange_Connected -and (-not $Force) ) {
                Write-Warning 'Exchange Online: Not attempting as already connected.  Use -Force to override.'
            } elseif ($ExchangeOnline) {
                if ($Force) {
                    Disconnect-MIT365 -Office365:$false -ExchangeOnline:$true -SkypeForBusinessOnline:$false
                } #end if ($Force)

                Write-Verbose '[Connect-MIT365] Exchange Online: Attempting to connect...'
                try {
                    Write-Debug '[Connect-MIT365] Exchange Online: Establishing PSSession.'
                    $script:MilneIT_365_Exchange_PSSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $Credential -Authentication Basic -AllowRedirection -ErrorAction Stop

                    <#
                        When we run Import-PSSession, a temporary module is created and imported into the *local* scope.
                        As we are running Import-PSSession in a function, we need it in the *global* scope, as otherwise
                        the cmdlets cannot be seen or run by the user.  This is not possible with Import-PSSession.
                        However, after running Import-PSSession we can then import the module using Import-Module into the Global Scope.
                    #>

                    Write-Debug '[Connect-MIT365] Exchange Online: Importing PSSession into the local scope.'
                    $script:MilneIT_365_Exchange_Module = Import-PSSession -Session $script:MilneIT_365_Exchange_PSSession -DisableNameChecking -ErrorAction Stop -AllowClobber

                    Write-Debug '[Connect-MIT365] Exchange Online: Importing module into the global scope.'
                    Import-Module $script:MilneIT_365_Exchange_Module -Global -DisableNameChecking -ErrorAction Stop

                    Write-Debug '[Connect-MIT365] Exchange Online: Connected.'
                    $script:MilneIT_365_Exchange_Connected = $true
                } catch {
                    # Save the error to an exported variable.
                    $script:MilneIT_365_Exchange_ConnectionError = $_

                    Write-Warning 'Exchange Online: Could not connect.  This is expected if the tenant is not licenced for Exchange Online.  Otherwise review the error saved to $MilneIT_365_Exchange_ConnectionError.'
                } #end catch

            } else {
                Write-Verbose '[Connect-MIT365] Exchange Online: Not attempting to connect.'
            } #end else

Now, Skype for Business Online. With Exchange Online, you use the generic New-PSSession cmdlet to establish the session to Microsoft’s servers. With Skype for Business Online, you specifically need the New-CsOnlineSession cmdlet from the SkypeOnlineConnector module. So first off, we check that this module is actually present – and if not give a more helpful warning to the user (or if it’s an entirely different error, throw).

            <#
                Skype for Business Online
            #>

            if ($script:MilneIT_365_CsOnline_Connected -and (-not $Force) ) {
                Write-Warning 'Skype for Business Online: Not attempting as already connected.  Use -Force to override.'
            } elseif ($SkypeForBusinessOnline) {
                if ($Force) {
                    Disconnect-MIT365 -Office365:$false -ExchangeOnline:$false -SkypeForBusinessOnline:$true
                } #end if ($Force)

                Write-Verbose '[Connect-MIT365] Skype for Business Online: Attempting to connect...'

                # First try to import the connector module.
                $SkypeConnectorModuleImported = $false
                try {
                    Write-Debug '[Connect-MIT365] Skype for Business Online: Importing SkypeOnlineConnector module.'
                    Import-Module 'SkypeOnlineConnector' -ErrorAction Stop -Global

                    Write-Debug '[Connect-MIT365] Skype for Business Online: Imported SkypeOnlineConnector module.'
                    $SkypeConnectorModuleImported = $true
                } catch [System.IO.FileNotFoundException] {
                    $script:MilneIT_365_CsOnline_ConnectionError = $_
                    if ($_.Exception.Message -eq $CsOnlineModuleError) {
                        Write-Debug '[Connect-MIT365] Skype for Business Online: Import of SkypeOnlineConnector failed as module is not installed.'
                        Write-Warning 'Skype for Business Online: Did not connect as the SkypeOnlineConnector module does not appear to be installed.  If this is not expected, review the error saved to $MilneIT_365_CsOnline_ConnectionError.'
                    } else {
                        Write-Debug '[Connect-MIT365] Skype for Business Online: Import of SkypeOnlineConnector failed for unhandled reason.'
                        throw
                    } #end else
                } #end catch [System.IO.FileNotFoundException]

After this, it’s largely the same as Exchange save for using New-CsOnlineSession instead of New-PSSession.

                # Now try to establish the PSSession.
                if ($SkypeConnectorModuleImported) {
                    # This isn't a lot different from Exchange Online.
                    try {
                        Write-Debug '[Connect-MIT365] Skype for Business Online: Establishing PSSession.'
                        $script:MilneIT_365_CsOnline_PSSession  = New-CsOnlineSession -Credential $Credential -ErrorAction Stop

                        Write-Debug '[Connect-MIT365] Skype for Business Online: Importing PSSession into the local scope.'
                        $script:MilneIT_365_CsOnline_Module = Import-PSSession -Session $script:MilneIT_365_CsOnline_PSSession -DisableNameChecking -ErrorAction Stop -AllowClobber

                        Write-Debug '[Connect-MIT365] Skype for Business Online: Importing module into the global scope.'
                        Import-Module $script:MilneIT_365_CsOnline_Module -Global -DisableNameChecking -ErrorAction Stop

                        Write-Debug '[Connect-MIT365] Skype for Business Online: Connected.'
                        $script:MilneIT_365_CsOnline_Connected = $true
                    } catch {
                        $script:MilneIT_365_CsOnline_ConnectionError = $_
                        Write-Warning 'Skype for Business Online: Could not connect.  This is expected if the tenant is not licenced for Skype for Business Online.  Otherwise review the error saved to $MilneIT_365_CsOnline_ConnectionError.'
                    } #end catch
                } #end if ($SkypeConnectorModuleImported)

            } else {
                Write-Verbose '[Connect-MIT365] Skype for Business Online: Not attempting to connect.'
            } #end else

That’s our Connect-MIT365 function completed.

        } catch {
            throw
        } #end catch

    } #end Function Connect-MIT365

Disconnect-MIT365

As for the disconnect function, it takes largely the same parameters as Connect-MIT365, though no credential is required.

    Function Disconnect-MIT365 {
        [cmdletbinding()]
        Param(
            [switch]
            $Office365 = $true,

            [switch]
            $ExchangeOnline = $true,

            [switch]
            $SkypeForBusinessOnline = $true,

            [switch]
            $Force = $false
        )

First, we “disconnect” Office 365 PowerShell. There isn’t actually an opposite cmdlet to Connect-MsolService, nor is there exactly a need for one – you simply run Connect-MsolService again and it will overwrite the previous connection. If you need to change Office 365 tenants, you just run it again with different credentials. But we want the behaviour to appear uniform between this and the other 365 components that can and should be explicitly disconnected.

        if ($Office365 -and ($script:MilneIT_365_MSOnline_Connected -or $Force) ) {
            # This doesn't actually do anything other than unset the variables indicating we are connected.
            Write-Warning 'Office 365: Note that Office 365 PowerShell cannot be disconnected, but you can now reconnect with Connect-MIT365 or Connect-MsolService.'
            $script:MilneIT_365_MSOnline_ConnectionError = $null
            $script:MilneIT_365_MSOnline_Connected       = $false
        } #end if

Now for Exchange Online and Skype for Business Online each, we do three things:

  1. Remove the module, which removes all functions and cmdlets*.
  2. Disconnect the PSSession. If we don’t do this the session will remain connected whilst PowerShell is kept open, and will linger for a bit after PowerShell is closed, which can cause issues with concurrent session limits.
  3. *I said removing the module removes all functions and cmdlets, however it doesn’t remove the proxy functions properly, likely as we imported the module into the global scope and Remove-Module can’t undo that. As a workaround, we locate the proxy functions in the Functions:\ PSDrive and remove them using Remove-Item.
        if ($ExchangeOnline -and ($script:MilneIT_365_Exchange_Connected -or $Force) ) {
            Write-Debug '[Disconnect-MIT365]: Disconnecting from Exchange Online'
            Remove-Module $script:MilneIT_365_Exchange_Module -Force
            Remove-PSSession -Session $script:MilneIT_365_Exchange_PSSession -ErrorAction SilentlyContinue
            <#
                Removing the module above does not remove the proxy functions.
                This is likely as we imported the module into the global scope, which Remove-Module cannot touch here.
                The workaround is to remove the proxy functions directly.
            #>
            Get-ChildItem Function:\ | Where-Object Source -eq $MilneIT_365_Exchange_Module.Name | Remove-Item -Force
            $script:MilneIT_365_Exchange_PSSession       = $null
            $script:MilneIT_365_Exchange_Module          = $null
            $script:MilneIT_365_Exchange_ConnectionError = $null
            $script:MilneIT_365_Exchange_Connected       = $false
        } #end if

        if ($SkypeForBusinessOnline -and ($script:MilneIT_365_CsOnline_Connected -or $Force) ) {
            Write-Debug '[Disconnect-MIT365]: Disconnecting from Skype for Business Online'
            Remove-Module $script:MilneIT_365_CsOnline_Module -Force
            Remove-PSSession -Session $script:MilneIT_365_CsOnline_PSSession -ErrorAction SilentlyContinue
            Get-ChildItem Function:\ | Where-Object Source -eq $MilneIT_365_CsOnline_Module.Name | Remove-Item -Force
            $script:MilneIT_365_CsOnline_PSSession       = $null
            $script:MilneIT_365_CsOnline_Module          = $null
            $script:MilneIT_365_CsOnline_ConnectionError = $null
            $script:MilneIT_365_CsOnline_Connected       = $false
        } #end if

    } #end Function Disconnect-MIT365

Automatically disconnect sessions

Above I mentioned the problems with lingering PSSessions. One way we can mitigate this is to modify the module so that whenever it is unloaded, it disconnects the sessions for us. It’s easy to specify a ScriptBlock of arbitrary commands that run when we unload the module, and in this case we just need to run Disconnect-MIT365.

$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
    Disconnect-MIT365
}

Trying it out

Here we can see that we can connect, and immediately run cmdlets from MSOnline, Exchange Online, and Skype for Business Online. And we can also run our custom Get-MITUser cmdlet which currently uses MSOnline and Exchange Online.

PS> $Lab01Credential = Get-Credential
PS> Connect-MIT365 -Credential $Lab01Credential

PS> Get-MsolUser

UserPrincipalName                    DisplayName     isLicensed
-----------------                    -----------     ----------
Roy@milneitlab01.onmicrosoft.com     Roy Hitchcock   True      
Brady2@milneitlab01.onmicrosoft.com  Brady           True      
Brady@milneitlab01.onmicrosoft.com   Brady Tyree     True      
Will2@milneitlab01.onmicrosoft.com   Will Cook       True      
Sandra2@milneitlab01.onmicrosoft.com Sandra Martinez True      
Sandra3@milneitlab01.onmicrosoft.com Sandra Phillips True      
Mary@milneitlab01.onmicrosoft.com    Mary Johnson    True      
Sandra@milneitlab01.onmicrosoft.com  Sandra Martinez True      
Will@milneitlab01.onmicrosoft.com    Will Richard    True      
Vicki@milneitlab01.onmicrosoft.com   Vicki Marsh     True   

PS> Get-Mailbox

Name                      Alias                ServerName       ProhibitSendQuota
----                      -----                ----------       -----------------
Brady2                    Brady2               mmxp123mb0062    99 GB (106,300,440,576 bytes)
Brady                     Brady                loxp123mb1062    99 GB (106,300,440,576 bytes)
DiscoverySearchMailbox... DiscoverySearchMa... mm1p123mb1068    50 GB (53,687,091,200 bytes)
Mary                      Mary                 mm1p123mb1084    99 GB (106,300,440,576 bytes)
Sandra2                   Sandra2              mmxp123mb0653    99 GB (106,300,440,576 bytes)
Sandra                    Sandra               mmxp123mb0959    99 GB (106,300,440,576 bytes)
Sandra3                   SandraP              loxp123mb1335    99 GB (106,300,440,576 bytes)
Vicki                     Vicki                mm1p12301mb1609  99 GB (106,300,440,576 bytes)
Will2                     Will                 mmxp123mb0751    99 GB (106,300,440,576 bytes)

PS> Get-CsOnlineUser | ft DisplayName,SipAddress

DisplayName     SipAddress                                                            
-----------     ----------                                                            
Brady Tyree     sip:Brady@milneitlab01.onmicrosoft.com                                
Roy Hitchcock   sip:Roy@milneitlab01.onmicrosoft.com                                  
Sandra Martinez sip:Sandra2@milneitlab01.onmicrosoft.com                              
Mary Ford       sip:c4f2b7c39cbb42f18dcffce8f82f44b6Mary2@milneitlab01.onmicrosoft.com
Vicki Marsh     sip:Vicki@milneitlab01.onmicrosoft.com                                
Sandra Martinez sip:Sandra@milneitlab01.onmicrosoft.com                               
Sandra Phillips sip:Sandra3@milneitlab01.onmicrosoft.com                              
Mary Johnson    sip:Mary@milneitlab01.onmicrosoft.com                                 
Brady           sip:Brady2@milneitlab01.onmicrosoft.com                               
Will Cook       sip:Will2@milneitlab01.onmicrosoft.com                                
Will Richard    sip:Will@milneitlab01.onmicrosoft.com                                 

PS> Get-MITUser Vicki

DisplayName UserPrincipalName                  PrimarySmtpAddress                 MsolUserFound MailboxFound
----------- -----------------                  ------------------                 ------------- ------------
Vicki Marsh Vicki@milneitlab01.onmicrosoft.com Vicki@milneitlab01.onmicrosoft.com True          True        

We can now disconnect and then connect to another tenant using a different credential. This tenant doesn’t have Skype for Business Online – so it fails to connect and thus warns us, but we know it’s not a problem.

PS> Disconnect-MIT365
WARNING: Office 365: Note that Office 365 PowerShell cannot be disconnected, but you can now reconnect with Connect-MIT365 or Connect-MsolService.

PS> $OtherTenantCredential = Get-Credential

PS> Connect-MIT365 -Credential $OtherTenantCredential
WARNING: Skype for Business Online: Could not connect.  This is expected if the tenant is not licenced for Skype for Business Online.  Otherwise review the error saved to $MilneIT_365_CsOnline_ConnectionError.

PS> Get-MITUser john@example.com

DisplayName UserPrincipalName            PrimarySmtpAddress           MsolUserFound MailboxFound
----------- -----------------            ------------------           ------------- ------------
John Smith  john@example.com             john@example.som             True          True        

If we run Get-PSSession we can see that two sessions are currently open (one for Exchange Online, one for Skype for Business Online). We could disconnect them using Disconnect-MIT365; but here we see that if we unload the entire MilneIT module, it automatically disconnects the PSSessions for us.

PS> Get-PSSession

 Id Name            ComputerName    ComputerType    State         ConfigurationName     Availability
 -- ----            ------------    ------------    -----         -----------------     ------------
  5 Session5        admingb1.onl... RemoteMachine   Opened        Microsoft.PowerShell     Available
  4 Session4        outlook.offi... RemoteMachine   Opened        Microsoft.Exchange       Available

PS> Remove-Module MilneIT
WARNING: Office 365: Note that Office 365 PowerShell cannot be disconnected, but you can now reconnect with Connect-MIT365 or Connect-MsolService.

PS> Get-PSSession

PS>

The full MilneIT.psm1 module as of this blog post can be viewed on Bitbucket here. You can always view the latest version of the full repository here.

Leave a Comment

Your email address will not be published. Required fields are marked *