Migrating a 107GB Gmail Mailbox with LAO: What Microsoft's Docs Don't Tell You

Migrating a single Accounts Payable mailbox from Google Workspace to Exchange Online should have been the easy part of a larger phased migration. It wasn’t. The mailbox was 100GB+ with 600,000+ items, which put it past Exchange Online’s standard migration path and into Large Archive Onboarding (LAO) territory. LAO is the only supported mechanism for migrating a mailbox this size from Gmail while splitting content between primary and archive by date or folder. ...

August 12, 2026 · 9 min · Zac Lohrenz

Automating Microsoft Teams Provisioning with PowerShell and Graph API

The Challenge of Teams Sprawl When organizations adopt Microsoft Teams, they often face a common challenge: how to provision Teams consistently while maintaining security standards. Manual creation leads to inconsistent settings, security gaps, and administrative overhead. This guide shows how to automate Teams provisioning with PowerShell to ensure every team follows your organization’s security baseline. Prerequisites # Install required modules Install-Module Microsoft.Graph -Scope CurrentUser Install-Module MicrosoftTeams -Scope CurrentUser # Import modules Import-Module Microsoft.Graph Import-Module MicrosoftTeams Authentication Setup # Connect to Microsoft Graph Connect-MgGraph -Scopes "Group.ReadWrite.All", "TeamSettings.ReadWrite.All", "Directory.ReadWrite.All" # Connect to Teams PowerShell Connect-MicrosoftTeams The Automation Script Core Team Creation Function Example function New-StandardizedTeam { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string]$TeamName, [Parameter(Mandatory=$true)] [string]$Description, [Parameter(Mandatory=$false)] [string[]]$Owners = @(), [Parameter(Mandatory=$false)] [string[]]$Members = @() ) try { Write-Host "Creating team: $TeamName" -ForegroundColor Yellow # Create the Microsoft 365 Group first $groupParams = @{ displayName = $TeamName description = $Description mailEnabled = $true mailNickname = ($TeamName -replace '[^a-zA-Z0-9]', '') securityEnabled = $false groupTypes = @("Unified") visibility = "Private" } $group = New-MgGroup -BodyParameter $groupParams Write-Host "Group created with ID: $($group.Id)" -ForegroundColor Green # Wait for group provisioning Start-Sleep -Seconds 10 # Convert to Team $teamParams = @{ "memberSettings" = @{ "allowCreateUpdateChannels" = $false "allowDeleteChannels" = $false "allowCreatePrivateChannels" = $false } "guestSettings" = @{ "allowCreateUpdateChannels" = $false "allowDeleteChannels" = $false } "messagingSettings" = @{ "allowUserEditMessages" = $true "allowUserDeleteMessages" = $false "allowTeamMentions" = $true "allowChannelMentions" = $true } "funSettings" = @{ "allowGiphy" = $true "giphyContentRating" = "strict" "allowStickersAndMemes" = $false "allowCustomMemes" = $false } } $team = New-MgTeam -GroupId $group.Id -BodyParameter $teamParams Write-Host "Team provisioned successfully" -ForegroundColor Green # Configure General channel Set-TeamChannelModerators -GroupId $group.Id # Add owners and members Add-TeamMembers -GroupId $group.Id -Owners $Owners -Members $Members # Hide from GAL Update-MgGroup -GroupId $group.Id -HideFromAddressLists return @{ Success = $true GroupId = $group.Id TeamName = $TeamName } } catch { Write-Error "Failed to create team: $_" return @{ Success = $false Error = $_.Exception.Message } } } Channel Configuration function Set-TeamChannelModerators { param( [Parameter(Mandatory=$true)] [string]$GroupId ) # Get the General channel $channels = Get-MgTeamChannel -TeamId $GroupId $generalChannel = $channels | Where-Object { $_.DisplayName -eq "General" } if ($generalChannel) { # Update channel to require moderation $channelParams = @{ moderationSettings = @{ userNewMessageRestriction = "everyoneExceptGuests" replyRestriction = "everyone" allowNewMessageFromBots = $true allowNewMessageFromConnectors = $true } } Update-MgTeamChannel -TeamId $GroupId -ChannelId $generalChannel.Id -BodyParameter $channelParams Write-Host "General channel moderation configured" -ForegroundColor Green } } Bulk Provisioning function Import-TeamsFromCSV { param( [Parameter(Mandatory=$true)] [string]$CsvPath ) $teams = Import-Csv -Path $CsvPath $results = @() foreach ($team in $teams) { $result = New-StandardizedTeam ` -TeamName $team.TeamName ` -Description $team.Description ` -Owners ($team.Owners -split ';') ` -Members ($team.Members -split ';') $results += $result # Rate limiting Start-Sleep -Seconds 5 } # Export results $results | Export-Csv -Path "TeamsProvisioningResults.csv" -NoTypeInformation # Summary $successful = ($results | Where-Object { $_.Success -eq $true }).Count $failed = ($results | Where-Object { $_.Success -eq $false }).Count Write-Host "Provisioning Summary:" -ForegroundColor Cyan Write-Host "Successful: $successful" -ForegroundColor Green Write-Host "Failed: $failed" -ForegroundColor Red } CSV Template TeamName,Description,Owners,Members "Marketing Team","Marketing collaboration space","[email protected];[email protected]","[email protected];[email protected]" "Sales Team","Sales collaboration space","[email protected];[email protected]","[email protected];[email protected]" Advanced Configuration Apply Organization-Wide Settings function Set-OrganizationTeamsDefaults { # Get current policies $messagingPolicy = Get-CsTeamsMessagingPolicy -Identity Global # Update messaging policy Set-CsTeamsMessagingPolicy -Identity Global ` -AllowGiphy $true ` -GiphyRatingType "Strict" ` -AllowMemes $false ` -AllowStickers $false ` -AllowUserDeleteMessages $false ` -AllowUserEditMessages $true # Guest access configuration Set-CsTeamsGuestMessagingConfiguration ` -AllowGiphy $false ` -AllowMemes $false ` -AllowStickers $false ` -AllowUserDeleteMessages $false ` -AllowUserEditMessages $false Write-Host "Organization defaults applied" -ForegroundColor Green } Error Handling and Logging function Write-TeamsLog { param( [string]$Message, [string]$Level = "Info" ) $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $logEntry = "$timestamp [$Level] $Message" Add-Content -Path "TeamsProvisioning.log" -Value $logEntry switch ($Level) { "Error" { Write-Host $Message -ForegroundColor Red } "Warning" { Write-Host $Message -ForegroundColor Yellow } "Success" { Write-Host $Message -ForegroundColor Green } default { Write-Host $Message } } } Best Practices Rate Limiting: Microsoft Graph has throttling limits. Add delays between API calls. Error Recovery: Implement retry logic for transient failures. Validation: Verify team settings post-creation. Documentation: Maintain a log of all provisioned teams. Regular Audits: Schedule periodic reviews of team settings. Conclusion Automating Teams provisioning ensures consistency, improves security posture, and reduces administrative overhead. This approach scales from small deployments to enterprise-wide migrations while maintaining your organization’s governance standards. ...

April 3, 2026 · 4 min · Me

Google Workspace to Microsoft 365 Migration: Two Weeks of Pilot Migration Stories

Two weeks into a Google Workspace to Microsoft 365 pilot migration for a mid-size org. Thirteen mailboxes in the pilot batch. What should have been a straightforward cutover turned into a deep education in Exchange migration edge cases, MRS throttling behavior, and the gap between what Microsoft documents and what actually happens in production. The Setup The architecture: phased cutover with Mimecast coexistence routing, GAM-managed forwarding during the transition window, and batch migration via Exchange Online migration endpoints backed by a GCP service account. A verified alias subdomain (o365.[domain]) serves as the target delivery domain, with workspace.[domain] as the coexistence routing domain pointing back to Google. ...

March 24, 2026 · 8 min · Zac

Implementing Azure AD Privileged Identity Management (PIM)

Why Implement PIM? Privileged Identity Management (PIM) is one of the most effective security controls available in Azure AD. Traditional role assignments grant permanent, standing privileges that create significant security risks: Expanded Attack Surface: Compromised accounts with permanent privileges give attackers immediate access Compliance Gaps: Audit requirements often mandate Just-In-Time (JIT) access for privileged operations Privilege Creep: Over time, users accumulate unnecessary permanent role assignments PIM transforms these permanent assignments into time-bound, audited, and justified access - dramatically reducing your organization’s risk profile. ...

September 26, 2025 · 11 min · Me

Automating NewRelic Agent Updates in Windows Environments

The Challenge of Agent Updates Keeping monitoring agents up-to-date is very important for maintaining effective observability in your systems. However, manual updates can be time-consuming and often get overlooked. This post demonstrates a very simple way of how to automate NewRelic agent updates in a Windows environment. Infrastructure Agent Update Script Create a PowerShell script to update the NewRelic Infrastructure agent: Stop-Service -Name “newrelic-infra” (New-Object System.Net.WebClient).DownloadFile( “https://download.newrelic.com/infrastructure_agent/windows/newrelic-infra.msi", “$env:TEMP\newrelic-infra.msi” ) msiexec.exe /qn /i “$env:TEMP\newrelic-infra.msi” Start-Service -Name “newrelic-infra” ...

August 1, 2025 · 2 min · Me