PowerShell Basics for Office 365 Administration (Episode 4): Conditional Logic and Looping Structures

Welcome back to our PowerShell Basics series! In the previous episodes, we've covered connecting to Office 365 services (now primarily using Microsoft Graph), using cmdlets to retrieve and manage objects, and understanding variables and the pipeline. You now have the tools to interact with your environment.

However, administration tasks often involve dealing with many objects (users, mailboxes, licenses) or performing actions only when certain conditions are met. Manually running commands for each item or decision is tedious and prone to errors. This is where the true power of scripting comes in, using conditional logic to make decisions and looping structures to process multiple items efficiently.

A Note on Modules: As the MSOnline and AzureAD PowerShell modules are being retired, it's crucial to transition to the Microsoft.Graph PowerShell module. All examples in this episode will use Graph PowerShell for user and group management. For Exchange Online examples, we will continue using the ExchangeOnlineManagement module, as it remains the standard for mailbox administration.

In this episode, we will dive deep into how you can make your scripts smart enough to decide what to do and powerful enough to handle your entire Office 365 tenant using Graph PowerShell.

The Power of Decision: Conditional Logic (If, ElseIf, Else)

Conditional logic allows your script to execute different blocks of code based on whether a specific condition is true or false. The most common structure for this in PowerShell is the If statement.

The basic syntax looks like this:

If (condition) {
    # Code to execute if the condition is TRUE
}

You can extend this with an Else block to specify what happens if the condition is false:

If (condition) {
    # Code to execute if the condition is TRUE
} Else {
    # Code to execute if the condition is FALSE
}

For multiple conditions, you use ElseIf:

If (condition1) {
    # Code if condition1 is TRUE
} ElseIf (condition2) {
    # Code if condition1 is FALSE, but condition2 is TRUE
} ElseIf (condition3) {
    # Code if condition1 and condition2 are FALSE, but condition3 is TRUE
} Else {
    # Code if none of the above conditions are TRUE
}

Comparison Operators

Conditions are built using comparison operators to compare values. Here are some frequently used ones:

Operator Description
-eq Equal to
-ne Not equal to
-gt Greater than
-lt Less than
-ge Greater than or equal to
-le Less than or equal to
-like Matches a wildcard pattern (e.g., *-Admin*)
-notlike Does not match a wildcard pattern
-match Matches a regular expression pattern
-notmatch Does not match a regular expression pattern
-contains Collection contains the value
-notcontains Collection does not contain the value
-in Value is in the collection
-notin Value is not in the collection
-is Object is of a specific .NET type
-isnot Object is not of a specific .NET type

You can also use -not or ! to negate a condition.

Checking User License Status

With Microsoft Graph PowerShell, checking license status is different. The IsLicensed property doesn't exist directly on the user object retrieved by default. Instead, you check if the user has any assignedLicenses. You need to request the AssignedLicenses property specifically when getting the user.

Connect-MgGraph -Scopes "User.Read.All", "Directory.Read.All"
$UserPrincipalName = "khalid@saudakori.onmicrosoft.com"
$User = Get-MgUser -UserId $UserPrincipalName -Select UserPrincipalName, DisplayName, AssignedLicenses -ErrorAction SilentlyContinue

If ($User -ne $null)
{
    If ($User.AssignedLicenses.Count -gt 0)
    {
        Write-Host "$($User.UserPrincipalName) is licensed."
    }
    Else
    {
        Write-Host "$($User.UserPrincipalName) is NOT licensed."
    }
}
Else
{
    Write-Warning "User $UserPrincipalName not found."
}

Note: Get-MgUser defaults to selecting only a few properties. Always use -Select or -Property to request additional properties you need, like AssignedLicenses, Department, etc.

Checking Mailbox Size

This example still uses the Exchange Online module, as mailbox statistics are not available directly via the Graph Users endpoint in the same way.

Connect-ExchangeOnline
$MailboxStats = Get-EXOMailboxStatistics -Identity "khalid@saudakori.onmicrosoft.com" -ErrorAction SilentlyContinue

if ($MailboxStats -ne $null){
    $SizeBytes = $MailboxStats.TotalItemSize.Value.ToBytes()

    # Define 1 Gigabyte in bytes
    $1GB = 1GB

    # Convert bytes to gigabytes and round to 2 decimal places
    $SizeGB = [math]::Round($SizeBytes / $1GB, 2)

    # Get the display name of the mailbox owner from the stats
    $DisplayName = $MailboxStats.DisplayName

    # Conditional checks based on mailbox size in GB
    if ($SizeGB -gt 50)
    {
        Write-Warning "$DisplayName mailbox size is $SizeGB GB, exceeding 50 GB."
    }
    elseif ($SizeGB -gt 20)
    {
        Write-Host "$DisplayName mailbox size is $SizeGB GB, within limits but growing."
    }
    else
    {
        Write-Host "$DisplayName mailbox size is $SizeGB GB, well within limits."
    }
}
else
{
    Write-Warning "Mailbox not found."
}

Handling Multiple Cases: The Switch Statement

When you have a single value or expression that you want to compare against multiple possible values, the Switch statement can make your code cleaner than a long If/ElseIf chain.

The basic syntax is:

Switch (value or expression) {
    "Value1" {
        # Code if value matches "Value1"
    }
    "Value2" {
        # Code if value matches "Value2"
    }
    Default {
        # Code if value matches none of the above (Optional)
    }
}

Acting Based on User Job Title

Suppose you want to perform different actions based on a user's Job Title using Graph. We need to request the JobTitle property using -Select.

Connect-MgGraph -Scopes "User.Read.All", "Group.ReadWrite.All", "Directory.ReadWrite.All"
$UserPrincipalName = "khalid@saudakori.onmicrosoft.com"
$User = Get-MgUser -UserId $UserPrincipalName -Select UserPrincipalName, DisplayName, JobTitle -ErrorAction SilentlyContinue

If ($User -ne $null -and -not [string]::IsNullOrEmpty($User.JobTitle))
{
    Write-Host "Processing user $($User.DisplayName) with Job Title: $($User.JobTitle)"
    Switch ($User.JobTitle)
    {
        "Software Engineer"
        {
            Write-Host "$($User.DisplayName) is a Software Engineer. You could assign a Visual Studio subscription license or add to a 'Engineering' security group."
        }
        "Project Manager"
        {
            Write-Host "$($User.DisplayName) is a Project Manager. You could assign a Project Online license or add to a 'Project Management' security group."
        }
        "Analyst"
        {
            Write-Host "$($User.DisplayName) is an Analyst. You could ensure they have a Power BI Pro license."
        }
        Default
        {
            Write-Host "$($User.DisplayName) has the job title $($User.JobTitle). No specific actions defined for this title."
        }
    }
}
ElseIf ($User -ne $null)
{
    Write-Warning "$($User.DisplayName) does not have a job title specified."
}
Else
{
    Write-Warning "User $UserPrincipalName not found."
}

Repeating Actions: Looping Structures

Loops are essential for automation. They allow you to execute a block of code multiple times, often iterating over a collection of items. PowerShell offers several types of loops; the most common for Office 365 administration are ForEach and ForEach-Object.

The ForEach Loop

The ForEach loop is designed to iterate over a collection of items (like an array or a list of objects returned by a cmdlet).

The syntax is:

ForEach ($item in $collection) {
    # Code to execute for each $item in the $collection
}

Inside the loop, the variable $item (you can name it anything, like $user, $mailbox, etc.) holds the current item being processed in that iteration.

Processing a List of Users

Let's get a list of all users and display their display name and UPN using Graph. We use -All to retrieve all pages of results from Graph and -Select to ensure we get the properties we need.

Connect-MgGraph -Scopes "User.Read.All"
$Users = Get-MgUser -All -Select UserPrincipalName, DisplayName

Write-Host "Starting user processing..."

ForEach ($user in $Users)
{
    Write-Host "Processing user: $($user.DisplayName) - $($user.UserPrincipalName)"
    # Add more complex logic here if needed
}
Write-Host "Finished user processing."

This retrieves user objects into the $Users variable (including all pages thanks to -All), and then the ForEach loop processes each user object one by one, making its properties available via $user.

The ForEach-Object Cmdlet

While the ForEach loop works on a collection stored in a variable, the ForEach-Object cmdlet is designed to work with the pipeline. This is incredibly common in PowerShell scripting because it allows you to process objects as they come down the pipeline, often without needing to store the entire collection in memory first.

The syntax typically uses the $_ automatic variable, which represents the current object in the pipeline:

Command-ThatOutputsObjects | ForEach-Object {
    # Code to execute for each object ($_) coming from the pipeline
}

Or using the alias %:

Command-ThatOutputsObjects | % {
    # Code using $_
}

Processing Users Directly from the Pipeline

This achieves a similar result to the ForEach loop example but uses the pipeline with Graph. Remember to use -Select on Get-MgUser to ensure the required properties are in the objects coming down the pipeline.

Connect-MgGraph -Scopes "User.Read.All"

Get-MgUser -All -Select UserPrincipalName, DisplayName | ForEach-Object {
    Write-Host "Processing user: $($_.DisplayName) - $($_.UserPrincipalName)"
    # Add more complex logic here if needed
}

When to use ForEach loop vs. ForEach-Object cmdlet?

ForEach Loop: Use when you need to work with the entire collection before processing begins, or if your script structure feels cleaner storing the collection first. With Get-MgUser -All, the -All parameter often handles the paging complexity for you, so you get the full collection anyway before the loop starts, making the distinction less critical for this specific Graph cmdlet compared to older modules.

ForEach-Object Cmdlet: Use when processing objects directly from the pipeline is more efficient (especially for very large datasets when the source cmdlet supports streaming) or when chaining multiple cmdlets together using the pipeline is desired. It's also very useful when the objects you want to process come from the pipeline, not just a variable. This is often considered the more "PowerShell way" of processing collections.

Other Loops (For, While, Do)

PowerShell also has For, While, and Do...While loops, which are less common for iterating over object collections in typical Office 365 tasks but useful for other scenarios:

  • For: Used for running a script block a specific number of times, based on a counter.
  • While: Runs a script block while a condition is true, checking the condition before executing the block. Useful for waiting for a state change.
  • Do...While / Do...Until: Runs a script block at least once, checking the condition after executing the block. Useful for retries.

We won't focus on these as much for O365 object processing basics, but be aware they exist for more complex scripting needs.

Combining Loops and Conditions

The real power emerges when you combine conditional logic within loops. This allows you to iterate through a collection and perform different actions or filter items based on specific criteria.

A very common pattern is using ForEach-Object with an If statement inside.

Finding Large Mailboxes and Taking Action

This example remains the same as it uses the Exchange Online module, which is still current for mailbox administration.

Connect-ExchangeOnline

$MailboxSizeThresholdMB = 100

Get-Mailbox -ResultSize Unlimited | ForEach-Object {
    $UserMailbox = $_
    $Stats = Get-MailboxStatistics -Identity $UserMailbox.Identity
    $SizeString = $Stats.TotalItemSize.ToString()  # e.g., "95.2 MB (99,800,000 bytes)" or "1.23 GB (1,320,000,000 bytes)"

    if ($SizeString -match "^([\d\.]+)\s+(MB|GB)") {
        $SizeValue = [double]$matches[1]
        $SizeUnit = $matches[2]

        # Convert GB to MB if needed
        if ($SizeUnit -eq "GB") {
            $MailboxSizeMB = [math]::Round($SizeValue * 1024, 2)
        } else {
            $MailboxSizeMB = $SizeValue
        }

        if ($MailboxSizeMB -gt $MailboxSizeThresholdMB) {
            Write-Warning "$($UserMailbox.DisplayName) is large ($MailboxSizeMB MB). Checking forwarding..."

            if ($UserMailbox.ForwardingAddress -ne $null) {
                Write-Host "--> $($UserMailbox.DisplayName) has forwarding enabled to $($UserMailbox.ForwardingAddress)."
                Write-Host "--> Forwarding disabled for $($UserMailbox.DisplayName)."  # Optional action
            } else {
                Write-Host "--> $($UserMailbox.DisplayName) does not have forwarding enabled."
            }
        }
    }
}

In this script:

  • We get all mailboxes and their statistics.
  • We pipe each mailbox statistics object to ForEach-Object.
  • Inside the loop, we calculate the size in MB.
  • An If statement checks if the size exceeds our threshold.
  • If it exceeds the threshold, we write a warning and then get the corresponding Mailbox object (because statistics don't contain forwarding info).
  • Another If statement checks the ForwardingAddress property.
  • If forwarding is set, we report it and could use Set-Mailbox to disable it (the line is commented out for safety).

This demonstrates how you can process items individually within a loop and make decisions about each item based on its properties.

Filtering with Where-Object

Before we conclude, it's worth mentioning the Where-Object cmdlet, which is frequently used before or in conjunction with loops/pipeline processing. Where-Object filters objects from the pipeline based on a condition. It's essentially a way to apply conditional logic before sending objects further down the pipeline.

Syntax:

Command-ThatOutputsObjects | Where-Object { condition using $_ } | Another-Command

Or using the alias ?:

Command-ThatOutputsObjects | ? { condition using $_ } | Another-Command

Get only unlicensed users

Using Graph, we filter based on the AssignedLicenses.Count being zero. Remember to -Select AssignedLicenses when retrieving the users.

Connect-MgGraph -Scopes "User.Read.All", "Directory.Read.All" # User.Read.All might be sufficient

Get-MgUser -All -Property UserPrincipalName, DisplayName, AssignedLicenses, CreatedDateTime |
    Where-Object {$_.AssignedLicenses.Count -eq 0} |
    Select-Object DisplayName, UserPrincipalName, CreatedDateTime

This pipes all users (retrieved with -All and relevant -Select properties) to Where-Object. Where-Object filters them, keeping only those where the AssignedLicenses collection count is 0. The resulting filtered collection is then sent to Select-Object. This is often more efficient than looping through all users and using an If inside to check the license status, although for very large tenants, consider server-side filtering with -Filter if the condition supports it efficiently via OData.

You can combine Where-Object with ForEach-Object:

Connect-MgGraph -Scopes "User.Read.All", "Directory.Read.All"

Get-MgUser -All -Select UserPrincipalName, DisplayName, AssignedLicenses |
    Where-Object {$_.AssignedLicenses.Count -eq 0} |
    ForEach-Object {
        Write-Host "$($_.DisplayName) ($($_.UserPrincipalName)) is unlicensed. Consider license assignment."
        # Add code here to perform action on unlicensed users (requires LicenseAssignment.ReadWrite.All scope for assignment)
    }

This is a powerful pattern: Filter first using Where-Object, then process the smaller, relevant set of objects using ForEach-Object.

Conclusion

Mastering conditional logic and looping structures is a crucial step in automating your Office 365 administration tasks with PowerShell. Using the Microsoft Graph PowerShell module for user and group management, you can now write scripts that:

  • Make decisions based on user properties (retrieved with -Select), mailbox sizes, and more.
  • Process entire lists of users, mailboxes, groups, etc., efficiently, leveraging Graph's capabilities.
  • Combine decisions and loops to perform targeted actions on specific subsets of your environment.

We've covered the fundamental If/ElseIf/Else for decision making and the vital ForEach loop and ForEach-Object cmdlet for iterating over collections retrieved from Graph (remembering to -Select necessary properties), including how Where-Object can be used for efficient filtering.

In our next episode, we'll likely delve into handling errors in your scripts and potentially introduce functions to make your code reusable and organized.

Keep practicing these concepts using Microsoft Graph PowerShell! Try writing scripts to:

  • Find users without a specific property set (e.g., Department, JobTitle).
  • List mailboxes with specific features enabled or disabled using Exchange Online cmdlets within a loop.
  • Identify users who are members of certain Graph groups (Get-MgUserMembership) and potentially process them.
  • Identify inactive users based on SignInActivity property (requires specific scope and might need Graph API calls if cmdlet doesn't expose it easily, but the conditional/loop logic applies).

Happy scripting with Microsoft Graph!

Comments

Popular posts from this blog

PowerShell Basics for Office 365 Administration (Episode 3)

Unveiling Primary Mailbox Statistics