Skip to content

Practical technology help for homes and small businesses, from someone who knows how to make it work. Based in Martinez, serving Contra Costa County.

Practical IT guides, cheatsheets, and references from Jeremiah Ellington, drawn from more than 20 years of IT work in public sector and higher education.

Contact Info
Location Martinez, CA (Serving Contra Costa County)
Contact Info
Location Martinez, CA (Serving Contra Costa County)

Cheatsheet

PowerShell Essentials Cheatsheet for IT Admins

Everyday PowerShell for Windows administration: discovery, files, processes and services, networking, event logs, users, disks, remoting and safe-change habits, plus walk-throughs for Active Directory, Entra ID and Exchange (on-premises and online).

  • Applies to: PowerShell 7.6 LTS and Windows PowerShell 5.1 on Windows 11 and Windows Server 2019+ (noted where they differ). PowerShell 7.4 and 7.5 reach end of support on November 10, 2026
  • Last reviewed

Free, no email required.

Ask AI about this cheatsheetChatGPT, Claude, or any assistant

These open a new chat with a prompt that points the assistant to this cheatsheet. It only sees the cheatsheet if it can open the link. If it says it can't, use Copy cheatsheet for AI and paste the text into the chat.

Nothing goes to an AI service unless you choose one. AI answers can be wrong: check them against the cheatsheet before you change anything important.

Most cmdlets work in both Windows PowerShell 5.1 (built into Windows) and PowerShell 7 (installed separately, pwsh; 7.6 is the current long-term support release). Several network and Windows-management cmdlets come from Windows modules and run only on Windows.

Windows: three everyday jobs#

The next three sections each walk through one common Windows job: a service that keeps stopping, a PC that can't reach the file server, and a PC that restarted overnight. Each one starts with a short situation, finds out what's going on with read-only commands, makes the change (labelled, with -WhatIf first), confirms it worked, and lists what to try if it didn't. A quick-reference table closes each section. After them come the reference sections, by area, and then the directory and mail jobs.

A service that won't stay running#

The situation: the print spooler stops every morning#

Contoso's front-desk PC, PC-FRONT01, prints the day's visitor badges. For the past week, Grace Kim has found that nothing prints when she arrives, and restarting the PC fixes it until the next morning. The service behind printing is the Print Spooler, Spooler.

Find out what's going on (read-only)#

PowerShellRead-only
Get-Service -Name Spooler | Select-Object Name, Status, StartType
Get-Service -Name Spooler -DependentServices          # services that stop when it stops
Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = 7031, 7034; StartTime = (Get-Date).AddDays(-7) } |
    Format-List TimeCreated, Id, ProviderName, Message
sc.exe qfailure Spooler                               # what Windows does when it fails
Plain textSample output (illustrative)
PS> Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = 7031, 7034; StartTime = (Get-Date).AddDays(-7) } | Format-List TimeCreated, Id, ProviderName, Message

TimeCreated  : 10/27/2026 6:42:10 AM
Id           : 7034
ProviderName : Service Control Manager
Message      : The Print Spooler service terminated unexpectedly. It has done this 1 time(s).

Look for: Status : Stopped with StartType : Automatic (it's meant to be running); events from Service Control Manager at about the same time each day; and, from sc.exe qfailure, the FAILURE_ACTIONS Windows takes when the service fails. A 7031 message ends with the corrective action, for example "The following corrective action will be taken in 5000 milliseconds: Restart the service." The event says the service stopped, not why.

Make the change#

Start the spooler again without restarting the PC.

PowerShellChanges this computer: restarts the service, -WhatIf first
Restart-Service -Name Spooler -WhatIf
Restart-Service -Name Spooler

Restart-Service stops and then starts the service; if it's already stopped, it just starts it. It works only if your account is allowed to control the service, so use an elevated prompt if it's refused. Like Stop-Service, it may refuse to stop a service that has running dependent services unless you add -Force (Microsoft documents this for Stop-Service).

Confirm it worked#

PowerShellRead-only
Get-Service -Name Spooler | Select-Object Name, Status   # Running
Get-Service -Name Spooler -DependentServices | Select-Object Name, Status

Then ask Grace to print a test badge. Run the event query again tomorrow morning: a new 7031 or 7034 means the cause is still there.

If it didn't work#

  • It stops again the next morning. Restarting treats the symptom. Look at what runs at that time (a scheduled job, a print queue that's sent overnight), and check the Application log around the time of the 7031 or 7034 event for an error from the program that crashed: Get-WinEvent -FilterHashtable @{ LogName = 'Application'; Level = 2; StartTime = (Get-Date).AddDays(-1) }.
  • Something else stopped with it. If you added -Force to restart a service that has running dependent services, -Force stops those services too. Check the -DependentServices list after the restart and start anything that should be running.
  • Windows doesn't restart it on its own. If sc.exe qfailure shows no FAILURE_ACTIONS, nothing is set to happen when the service fails. Adding a recovery action changes the service's configuration, so try it on one PC first, and keep looking for the cause.

Quick reference: services#

CommandWhat it doesRead-only or changes something
Get-Service -Name SpoolerShows a service's statusRead-only
Get-Service -Name Spooler -DependentServicesLists the services that depend on itRead-only
Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = 7031, 7034 }Finds "terminated unexpectedly" events from Service Control ManagerRead-only
sc.exe qfailure SpoolerShows the recovery actions for a failed serviceRead-only
Restart-Service -Name SpoolerStops and starts the service (starts it if stopped)Changes this computer

"I can't reach the file server"#

The situation: a share that won't open#

Monday morning at Contoso. Owen Hart can't open \\fs01.corp.contoso.com\shared: Explorer can't find it. Everyone else in the office can open it. Over the weekend, IT moved the file server to new hardware with a new IP address.

Find out what's going on (read-only)#

PowerShellRead-only
Get-NetIPConfiguration                                    # this PC's address, gateway and DNS servers
Get-DnsClientServerAddress -AddressFamily IPv4
Resolve-DnsName fs01.corp.contoso.com                     # the answer this PC is using
Resolve-DnsName fs01.corp.contoso.com -Server 192.0.2.53 -DnsOnly   # ask the DNS server directly
Test-NetConnection fs01.corp.contoso.com -CommonTCPPort SMB          # can it reach file sharing?
Plain textSample output (illustrative)
PS> Resolve-DnsName fs01.corp.contoso.com

Name                     Type   TTL   Section    IPAddress
----                     ----   ---   -------    ---------
fs01.corp.contoso.com    A      2140  Answer     192.0.2.20

PS> Resolve-DnsName fs01.corp.contoso.com -Server 192.0.2.53 -DnsOnly

Name                     Type   TTL   Section    IPAddress
----                     ----   ---   -------    ---------
fs01.corp.contoso.com    A      3600  Answer     192.0.2.45

Look for: DNS servers that are Contoso's own (here 192.0.2.53); whether the first answer matches the one straight from the DNS server (here it doesn't, so this PC is holding an old answer); and TcpTestSucceeded from Test-NetConnection. Use your own DNS server's address with -Server.

Make the change#

Clear this PC's DNS cache so it asks again. It's the same as ipconfig /flushdns.

PowerShellChanges this computer: clears the DNS cache, -WhatIf first
Clear-DnsClientCache -WhatIf
Clear-DnsClientCache

Confirm it worked#

PowerShellRead-only
Resolve-DnsName fs01.corp.contoso.com                     # now the new address
Test-NetConnection fs01.corp.contoso.com -CommonTCPPort SMB   # TcpTestSucceeded : True

Then ask Owen to open the share again.

If it didn't work#

  • The DNS servers aren't Contoso's. If Get-DnsClientServerAddress lists a public resolver or a home router, internal names like fs01.corp.contoso.com won't resolve properly. Fix the adapter's DNS settings (or the DHCP scope that hands them out), not the cache.
  • It still gets the old address. Compare Resolve-DnsName fs01.corp.contoso.com -NoHostsFile. If that answer is different, there's an old entry for the server in this PC's hosts file.
  • The name is right but TcpTestSucceeded is False. DNS is fine and something is blocking the connection: the file server isn't sharing yet, or a firewall on the server or between the two is blocking it. Check from the server's side.

Quick reference: networking#

CommandWhat it doesRead-only or changes something
Get-NetIPConfigurationShows addresses, gateway and DNS servers per adapterRead-only
Get-DnsClientServerAddress -AddressFamily IPv4Lists the DNS servers each adapter usesRead-only
Resolve-DnsName <name> -Server <dns> -DnsOnlyAsks one DNS server, using DNS onlyRead-only
Resolve-DnsName <name> -NoHostsFileResolves a name without the hosts fileRead-only
Test-NetConnection <name> -CommonTCPPort SMBTests a file-sharing connectionRead-only
Clear-DnsClientCacheClears the DNS client cache (same as ipconfig /flushdns)Changes this computer

A PC that restarted overnight#

The situation: open work lost overnight#

Tuesday, 8:10. Priya Shah in Contoso's sales team left a half-finished quote open on PC-SALES07 last night. This morning the PC is at the sign-in screen and the quote is gone. Did Windows Update restart it, or did it crash?

Find out what's going on (read-only)#

PowerShellRead-only
(Get-CimInstance Win32_OperatingSystem).LastBootUpTime
Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = 1074, 6008, 41; StartTime = (Get-Date).AddDays(-2) } |
    Format-List TimeCreated, Id, ProviderName, Message
Get-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 5
Plain textSample output (illustrative)
TimeCreated  : 10/27/2026 3:02:41 AM
Id           : 1074
ProviderName : User32
Message      : The process <process> has initiated the restart of computer PC-SALES07 on behalf of
               user <user> for the following reason: Operating System: Service pack (Planned)
               Reason Code: 0x80020010
               Shutdown Type: restart

Look for:

  • 1074 from User32: a planned restart. The message names the process that asked for it, the account and a reason. Microsoft's guidance is that a normal restart shows 1074, and that an update, an installer or a person can be behind it.
  • 41 from Kernel-Power, or 6008 from EventLog, with no 1074: an unexpected restart (a crash, a hang or a power loss).
  • The source as well as the ID. Microsoft notes that the same event ID can come from a different source, so check ProviderName.

What to do next#

This job may not need a change: the answer may be "it was the update, at the time Windows was set to restart". Act on what the events show.

  • A 1074 that points to an update. Windows restarts after an update outside its active hours, the hours you expect the PC to be in use. The default is 8 AM to 5 PM, so 3 AM is expected. Ask Priya to save her work before she leaves. If the hours are wrong for her, change them in Settings > Windows Update > Advanced options > Active hours (changes this computer). On managed PCs, set them centrally with the Group Policy setting Turn off auto-restart for updates during active hours instead.
  • A 41 or 6008 with no 1074. Treat it as a crash, a hang or a power problem, not an update. Look for 1001 (a bug check, from WER-SystemErrorReporting) and 7045 (a service or driver installed) shortly before it. A new driver just before the first crash is a good lead.

Confirm it worked#

PowerShellRead-only
(Get-CimInstance Win32_OperatingSystem).LastBootUpTime   # tomorrow: no restart during her working hours

After the next update night, run the event query again. A 1074 outside the new active hours is expected; a 41 or 6008 means the problem is still there.

If it didn't work#

  • There are no events at all. Widen StartTime, and check that the System log keeps enough history: Get-WinEvent -ListLog System.
  • It restarted during active hours. On a managed PC, restart policies may be set centrally, under Computer Configuration\Administrative Templates\Windows Components\Windows Update in Group Policy, or through MDM. Microsoft warns that conflicting restart policies can restart a PC when you don't expect it. Ask whoever manages updates.

Quick reference: restarts#

CommandWhat it doesRead-only or changes something
(Get-CimInstance Win32_OperatingSystem).LastBootUpTimeShows when Windows last startedRead-only
Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = 1074, 6008, 41 }Finds planned (1074) and unexpected (41, 6008) restartsRead-only
Get-HotFixLists Windows updates installed through Component-Based Servicing (not every update)Read-only
Settings > Windows Update > Advanced options > Active hoursSets the hours Windows won't restart for updatesChanges this computer

Discover and get help#

PowerShell
Get-Command *service*          # find commands by name
Get-Help Get-Service -Examples # examples for a command
Update-Help -Scope CurrentUser # download help (7.x; in 5.1, run Update-Help elevated)
Get-Service | Get-Member       # properties and methods of the output
$PSVersionTable                # PowerShell version

Files and text#

PowerShell
Get-ChildItem C:\Logs -Recurse -Filter *.log
Get-Content .\app.log -Tail 50 -Wait        # follow a log
Select-String -Path .\*.log -Pattern "error" -SimpleMatch
Copy-Item .\config.json .\config.backup.json
Get-ChildItem C:\Temp -Recurse | Measure-Object -Property Length -Sum
PowerShellDestructive: preview first
Remove-Item C:\Temp\old -Recurse -WhatIf    # shows what would be deleted
Remove-Item C:\Temp\old -Recurse            # deletes; no Recycle Bin

Processes and services#

PowerShell
Get-Process | Sort-Object CPU -Descending | Select-Object -First 10 Name, Id, CPU
Get-Service -Name Spooler
Restart-Service -Name Spooler
Get-Service | Where-Object Status -eq 'Stopped' | Where-Object StartType -eq 'Automatic'
Stop-Process -Name notepad -WhatIf

Networking (Windows)#

PowerShell
Get-NetIPConfiguration                       # addresses, gateway, DNS per adapter
Test-NetConnection example.com -Port 443     # can I reach this service?
Resolve-DnsName example.com -Type MX         # DNS lookup
Get-NetTCPConnection -State Listen | Sort-Object LocalPort
Get-DnsClientServerAddress -AddressFamily IPv4
Clear-DnsClientCache

System information and updates#

PowerShell
Get-ComputerInfo -Property OsName, OsVersion, OsBuildNumber, CsModel
(Get-CimInstance Win32_OperatingSystem).LastBootUpTime
Get-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 10
Get-Volume
Get-PhysicalDisk | Select-Object FriendlyName, MediaType, HealthStatus, Size

Event logs#

PowerShell
# Errors in the System log from the last 24 hours
Get-WinEvent -FilterHashtable @{ LogName = 'System'; Level = 2; StartTime = (Get-Date).AddDays(-1) }

# Unexpected shutdowns (Event ID 6008)
Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = 6008 } -MaxEvents 10

# Failed sign-ins (Event ID 4625; needs an elevated session)
Get-WinEvent -FilterHashtable @{ LogName = 'Security'; Id = 4625 } -MaxEvents 20

Local users and groups#

PowerShell
Get-LocalUser | Select-Object Name, Enabled, LastLogon
Get-LocalGroupMember -Group Administrators   # who has admin rights?
Disable-LocalUser -Name 'olduser' -WhatIf

Working with output#

PowerShell
Get-Service | Where-Object Status -eq 'Running' | Select-Object Name, DisplayName
Get-Process | Sort-Object WS -Descending | Select-Object -First 5
Get-Service | Export-Csv .\services.csv -NoTypeInformation
Get-ComputerInfo | ConvertTo-Json -Depth 2 | Out-File .\info.json
Get-ChildItem | Format-Table Name, Length -AutoSize

Remoting#

PowerShell
Enter-PSSession -ComputerName PC01               # interactive remote shell
Invoke-Command -ComputerName PC01, PC02 -ScriptBlock { Get-Service Spooler }
Test-WSMan -ComputerName PC01                    # is remoting reachable? (errors if WinRM is off)

Remoting must be enabled on the target (Enable-PSRemoting in an elevated session; it's on by default on Windows Server). Use it only on trusted networks or over a VPN.

Scripts and modules#

PowerShell
Get-ExecutionPolicy -List
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser   # allow local scripts for your user
Find-Module -Name PSWindowsUpdate                     # a community module, not from Microsoft
Install-Module -Name PSWindowsUpdate -Scope CurrentUser
Get-InstalledModule

Directory and mail: four everyday jobs#

The next four sections each walk through one common job: unlocking an account, adding someone to a group, chasing a stuck email, and handing over a leaver's mailbox. Each one starts with a short situation, finds out what's going on with read-only commands, makes the change (each step labelled, with -WhatIf first wherever the cmdlet supports it), confirms it worked, and lists what to try if it didn't. A quick-reference table closes each section.

Active Directory (on-premises)#

You need the ActiveDirectory module (part of RSAT) on a domain-joined machine: on Windows Server, install it with Install-WindowsFeature -Name RSAT-AD-Tools -IncludeAllSubFeature (elevated); on Windows 11 Pro or Enterprise, add the RSAT Active Directory tools under Settings > System > Optional features.

The situation: a locked-out user#

Monday, 8:05. Dana Reyes at Contoso calls the help desk: Windows says her account is locked out, and she has a client call at 8:30. Her sign-in name is dreyes in the corp.contoso.com domain.

Find out what's going on (read-only)#

PowerShell
Get-ADUser -Identity dreyes                                   # does the account exist, and is it enabled?
Search-ADAccount -LockedOut -UsersOnly | Select-Object Name, SamAccountName
Get-ADDomainController -Filter * | Select-Object Name, Site   # which DCs might hold a stale copy
Plain textSample output (illustrative)
PS> Search-ADAccount -LockedOut -UsersOnly | Select-Object Name, SamAccountName

Name        SamAccountName
----        --------------
Dana Reyes  dreyes

Look for: Enabled : True from Get-ADUser (a disabled account is a different problem, and unlocking won't help), and her name in the locked-out list.

Make the change#

Unlock the account, previewing the change first.

PowerShellChanges the directory: preview with -WhatIf first
Unlock-ADAccount -Identity dreyes -WhatIf
Unlock-ADAccount -Identity dreyes

Confirm it worked#

PowerShell
Search-ADAccount -LockedOut -UsersOnly | Select-Object Name, SamAccountName   # dreyes should be gone

Then ask Dana to sign in while you're still on the call.

If it didn't work#

  • She's locked out again within minutes. Something is still trying her old password: often a phone mail app, a mapped drive or a saved credential on another PC. Ask what she changed recently, and fix that device; unlocking again won't stick until you do.
  • One office still sees her as locked. The unlock is written to one domain controller and then replicates. Check a specific DC with -Server, for example Search-ADAccount -LockedOut -UsersOnly -Server DC02.corp.contoso.com, and give replication a few minutes.

Quick reference: Active Directory#

CommandWhat it doesRead-only or changes something
Get-ADUser -Identity dreyesShows one user account (add -Properties for more attributes)Read-only
Search-ADAccount -LockedOut -UsersOnlyLists locked-out user accountsRead-only
Get-ADDomainController -Filter *Lists the domain controllersRead-only
Unlock-ADAccount -Identity dreyesUnlocks the accountChanges the directory

Entra ID (Microsoft Graph PowerShell)#

For cloud identity, use the Microsoft Graph PowerShell SDK. The older AzureAD and MSOnline modules are deprecated and past their retirement date: Microsoft deprecated them on March 30, 2024 and said they would keep working only through March 30, 2025. If any of your scripts still use them, migrate those scripts to Microsoft Graph PowerShell.

PowerShellChanges this computer: installs the module
Install-Module Microsoft.Graph -Scope CurrentUser -Repository PSGallery

The situation: a new starter needs a group#

Sam Ortiz starts in Sales at Contoso today. His account sam.ortiz@contoso.com exists, but he can't open the Sales team's files because he isn't in the Sales group yet.

Find out what's going on (read-only)#

Connect with read-only permissions first. Connect-MgGraph opens a sign-in window. The first time you use a scope, it asks you to consent to it, and you consent only to the scopes you name.

PowerShell
Connect-MgGraph -Scopes 'User.ReadBasic.All', 'GroupMember.Read.All'
$user  = Get-MgUser -UserId 'sam.ortiz@contoso.com'
$group = Get-MgGroup -Filter "displayName eq 'Sales'"
$group | Select-Object DisplayName, Id, GroupTypes
Get-MgGroupMember -GroupId $group.Id -All | Where-Object Id -eq $user.Id   # nothing back = not a member
Plain textSample output (illustrative)
DisplayName Id                                   GroupTypes
----------- --                                   ----------
Sales       00000000-0000-4000-8000-000000000a01 {}

Look for: exactly one group called Sales (if the filter returns more than one, use the right Id), and GroupTypes. If it includes DynamicMembership, membership is set by a rule and you can't add people by hand (see below).

Make the change#

Adding a member needs GroupMember.ReadWrite.All. Connect again with that scope only when you're ready to change something.

PowerShellChanges the directory: preview with -WhatIf first
Connect-MgGraph -Scopes 'User.ReadBasic.All', 'GroupMember.ReadWrite.All'
New-MgGroupMember -GroupId $group.Id -DirectoryObjectId $user.Id -WhatIf
New-MgGroupMember -GroupId $group.Id -DirectoryObjectId $user.Id

Confirm it worked#

PowerShell
Get-MgGroupMember -GroupId $group.Id -All | Where-Object Id -eq $user.Id   # now returns Sam's Id
Disconnect-MgGraph

If it didn't work#

  • The group is dynamic. You can't manually add or remove members of a dynamic membership group. Change the user's attributes (such as department) so the rule picks him up, or ask whoever owns the rule.
  • Access denied. The scope isn't enough on its own: your account also needs an Entra role that's allowed to manage group membership, such as Groups Administrator or User Administrator, or you must be an owner of the group.
  • 403 Forbidden, and Sales is a distribution list. Microsoft Graph adds members only to security groups and Microsoft 365 groups. Manage distribution lists and mail-enabled security groups in Exchange Online instead (connect as shown in the Exchange Online section): Add-DistributionGroupMember -Identity 'Sales' -Member sam.ortiz@contoso.com -WhatIf, then run it again without -WhatIf.

Quick reference: Entra ID#

CommandWhat it doesRead-only or changes something
Connect-MgGraph -Scopes '...'Signs in; the first time, asks you to consent to the named permissionsSigns in (grants consent on first use)
Get-MgUser -UserId 'sam.ortiz@contoso.com'Gets one user by ID or sign-in nameRead-only
Get-MgGroup -Filter "displayName eq 'Sales'"Finds a group by nameRead-only
Get-MgGroupMember -GroupId $id -AllLists a group's direct membersRead-only
New-MgGroupMember -GroupId $id -DirectoryObjectId $userIdAdds a member to a security or Microsoft 365 groupChanges the directory
Add-DistributionGroupMember -Identity 'Sales' -Member ...Adds a member to a distribution list (Exchange Online)Changes the directory
Disconnect-MgGraphSigns out of the sessionRead-only (sign-out)

Exchange Server (on-premises)#

Run these in the Exchange Management Shell on an Exchange server or on a machine with the Exchange management tools. Exchange Server Subscription Edition (SE) is the supported version: Exchange Server 2016 and 2019 reached end of support on October 14, 2025.

The situation: an email that never arrived#

Priya Shah in Contoso's sales team sent a quote to buyer@example.com yesterday afternoon. This morning the buyer rings to say it never arrived. Contoso runs its own Exchange server, EX01.

Find out what's going on (read-only)#

PowerShell
Test-ServiceHealth -Server EX01                     # are the required Exchange services running?
Get-MessageTrackingLog -Server EX01 -Sender priya.shah@contoso.com -Recipients buyer@example.com -Start (Get-Date).AddDays(-2) |
    Select-Object Timestamp, EventId, Source, MessageSubject
Get-Queue -Server EX01 | Where-Object MessageCount -gt 0 |
    Select-Object Identity, Status, MessageCount, NextHopDomain, LastError
Plain textSample output (illustrative)
PS> Get-Queue -Server EX01 | Where-Object MessageCount -gt 0 | Select-Object Identity, Status, MessageCount, NextHopDomain, LastError

Identity  Status  MessageCount  NextHopDomain  LastError
--------  ------  ------------  -------------  ---------
EX01\7    Retry   3             example.com    451 4.4.0 Primary target IP address responded with: "421 4.4.2 Connection dropped." Attempted failover to alternate host, but that did not succeed.

Look for: services that aren't running in Test-ServiceHealth; in the tracking log, DEFER means delivery was delayed and FAIL means it failed; and a queue in Retry for the recipient's domain, with the reason in LastError.

Make the change#

The cause here is at the other end: example.com's mail server wasn't accepting connections. Exchange keeps retrying on its own. Once the buyer's IT team says their server is back, you can retry the queue now instead of waiting.

PowerShellChanges mail flow: preview with -WhatIf first
Retry-Queue -Identity 'EX01\7' -WhatIf
Retry-Queue -Identity 'EX01\7'

Confirm it worked#

PowerShell
Get-Queue -Server EX01 | Where-Object NextHopDomain -eq 'example.com'   # empty, or no longer listed
Get-MessageTrackingLog -Server EX01 -Sender priya.shah@contoso.com -Recipients buyer@example.com -EventId SEND -Start (Get-Date).AddDays(-2)

If it didn't work#

  • The queue goes straight back to Retry. The problem isn't fixed at the other end (or it's DNS, a firewall or a blocklist on yours). Read LastError again rather than retrying repeatedly.
  • Nothing in the tracking log. Check the spelling of both addresses and widen -Start. In an organization with more than one Exchange server, run the search on each server that handles mail.

Quick reference: Exchange Server#

CommandWhat it doesRead-only or changes something
Test-ServiceHealth -Server EX01Checks that the required Exchange services are runningRead-only
Get-MessageTrackingLog -Server EX01 -Sender ... -Recipients ...Searches the message tracking logRead-only
Get-Queue -Server EX01Lists mail queues, their status and last errorRead-only
Retry-Queue -Identity 'EX01\7'Forces a new connection attempt for a queueChanges mail flow

Exchange Online (Microsoft 365)#

PowerShell
Install-Module -Name ExchangeOnlineManagement -Scope CurrentUser
Connect-ExchangeOnline -UserPrincipalName admin@contoso.com   # opens a sign-in window (MFA works)
Get-ConnectionInformation                                     # which tenant am I connected to?
Get-EXOMailbox -ResultSize 10 | Select-Object DisplayName, PrimarySmtpAddress, RecipientTypeDetails
Get-EXOMailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited
Disconnect-ExchangeOnline -Confirm:$false

The situation: a leaver's mailbox#

Tom Becker left Contoso on Friday. His manager, Ana Silva, needs to keep reading his mailbox for customer emails, and finance wants his Microsoft 365 license back. A common answer: convert his mailbox to a shared mailbox and give Ana Full Access. But first, make sure Tom can't sign in any more. Converting the mailbox doesn't do that: unless you reset the password, his original username and password keep working on the shared mailbox.

Find out what's going on (read-only)#

The Graph commands need the Microsoft Graph PowerShell module (see the Entra ID section). User.Read.All is enough to read whether the account is enabled. The mailbox commands need an Exchange Online connection: if you ran the block above, it ended with a disconnect, so connect again here.

PowerShell
Connect-MgGraph -Scopes 'User.Read.All'
Connect-ExchangeOnline -UserPrincipalName admin@contoso.com
$tom = Get-MgUser -UserId 'tom.becker@contoso.com' -Property 'Id,DisplayName,AccountEnabled,OnPremisesSyncEnabled'
$tom | Select-Object DisplayName, AccountEnabled, OnPremisesSyncEnabled
Get-EXOMailbox -Identity tom.becker@contoso.com | Select-Object DisplayName, PrimarySmtpAddress, RecipientTypeDetails
Get-EXOMailboxStatistics -Identity tom.becker@contoso.com | Select-Object DisplayName, ItemCount, TotalItemSize
Get-EXOMailboxPermission -Identity tom.becker@contoso.com
Plain textSample output (illustrative)
DisplayName  ItemCount  TotalItemSize
-----------  ---------  -------------
Tom Becker   18452      6.214 GB (6,672,315,392 bytes)

Look for: AccountEnabled (True now) and OnPremisesSyncEnabled (True means the account is synced: see Before you start); RecipientTypeDetails (UserMailbox now); a TotalItemSize under 50 GB (a shared mailbox can hold up to 50 GB without a license); and who already has access.

Make the change#

Secure Tom's account first (steps 1 to 3), then hand over the mailbox (steps 4 to 6). Keep to this order. Blocking sign-in and resetting the password need an Entra role as well as the scopes: Microsoft lists User Administrator for users who aren't admins, and Privileged Authentication Administrator for admin accounts.

Step 1: block sign-in. Changes the directory. Connect again with the scopes for steps 1 and 2.

PowerShellChanges the directory: block sign-in, -WhatIf first
Connect-MgGraph -Scopes 'User.Read.All', 'User.EnableDisableAccount.All', 'User.RevokeSessions.All'
Update-MgUser -UserId $tom.Id -AccountEnabled:$false -WhatIf
Update-MgUser -UserId $tom.Id -AccountEnabled:$false

Step 2: sign him out everywhere. Changes the directory. This revokes the refresh tokens and browser session cookies Tom already has.

PowerShellChanges the directory: revoke sessions, -WhatIf first
Revoke-MgUserSignInSession -UserId $tom.Id -WhatIf
Revoke-MgUserSignInSession -UserId $tom.Id

Step 3: reset the password. Changes the directory. In the Microsoft 365 admin center, go to Users > Active users, select Tom, then select Reset password. Let it create the password automatically, and don't give it to anyone: nobody needs to sign in as Tom.

Step 4: convert the mailbox to shared. Changes the mailbox.

PowerShellChanges the mailbox: convert to shared, -WhatIf first
Set-Mailbox -Identity tom.becker@contoso.com -Type Shared -WhatIf
Set-Mailbox -Identity tom.becker@contoso.com -Type Shared

Step 5: give Ana Full Access. Changes the mailbox.

PowerShellChanges the mailbox: grant Full Access, -WhatIf first
Add-MailboxPermission -Identity tom.becker@contoso.com -User ana.silva@contoso.com -AccessRights FullAccess -InheritanceType All -WhatIf
Add-MailboxPermission -Identity tom.becker@contoso.com -User ana.silva@contoso.com -AccessRights FullAccess -InheritanceType All

Step 6: remove the license. Changes the directory. Only after the conversion has finished, and only if the mailbox is under 50 GB: in the Microsoft 365 admin center, go to Users > Active users, select Tom, select Licenses and apps, clear his license and select Save changes. Don't delete his account: the shared mailbox needs it.

Confirm it worked#

PowerShell
Get-MgUser -UserId $tom.Id -Property 'DisplayName,AccountEnabled' | Select-Object DisplayName, AccountEnabled   # False
Get-EXOMailbox -Identity tom.becker@contoso.com | Select-Object DisplayName, RecipientTypeDetails               # SharedMailbox
Get-EXOMailboxPermission -Identity tom.becker@contoso.com                                                      # Ana listed with FullAccess
Disconnect-MgGraph
Disconnect-ExchangeOnline -Confirm:$false

If it didn't work#

  • Tom can still get in for a while. Microsoft says blocking sign-in can take up to 24 hours to take effect, and resetting the password is what stops new sign-ins straight away; that's why you do both. Revoking sessions can take a few minutes, and Outlook on the web may stay open until he refreshes the page or opens another app.
  • Update-MgUser fails. Changing AccountEnabled is a sensitive action: the scope isn't enough without the right Entra role (see Make the change). If the account is synced from on-premises, disable it in Active Directory instead (see Before you start).
  • Ana doesn't show up when you filter by -User. Get-EXOMailboxPermission -User is case sensitive. List all permissions on the mailbox, as above, and look for her in the User column.
  • The mailbox is over 50 GB, or needs a hold. A shared mailbox needs a license if it holds more than 50 GB, is placed on litigation hold, or needs a bigger archive. Check before you remove Tom's.
  • Ana doesn't want it in her Outlook. Full Access adds the mailbox to her Outlook automatically by default. Add -AutoMapping $false when you grant it; to change that later, remove the permission and add it again.

The same "it never arrived" question, in Exchange Online#

If Priya's mailbox were in Exchange Online, you'd use message trace instead of the tracking log and queues (read-only):

PowerShell
Connect-ExchangeOnline -UserPrincipalName admin@contoso.com
Get-MessageTraceV2 -SenderAddress priya.shah@contoso.com -RecipientAddress buyer@example.com -StartDate (Get-Date).AddDays(-2) -EndDate (Get-Date)
Get-MessageTraceV2 -SenderAddress priya.shah@contoso.com -RecipientAddress buyer@example.com -StartDate (Get-Date).AddDays(-2) -EndDate (Get-Date) |
    Get-MessageTraceDetailV2                                    # each step, with the reason for a failure
Disconnect-ExchangeOnline -Confirm:$false

Look at Status: Delivered, Failed, Pending, FilteredAsSpam or Quarantined. Message trace covers the last 90 days, and each query can cover up to 10 days. Get-MessageTraceV2 replaces the older Get-MessageTrace.

No change needed: this is a read-only investigation. If the trace shows a failure, read the detail for the reason.

Quick reference: Exchange Online#

CommandWhat it doesRead-only or changes something
Connect-ExchangeOnline -UserPrincipalName admin@contoso.comSigns in to Exchange OnlineRead-only (sign-in)
Get-EXOMailbox -Identity user@contoso.comShows one mailboxRead-only
Get-EXOMailboxStatistics -Identity user@contoso.comShows item count and mailbox sizeRead-only
Get-EXOMailboxPermission -Identity user@contoso.comLists who has access to a mailboxRead-only
Get-MgUser -UserId $id -Property 'AccountEnabled,OnPremisesSyncEnabled'Shows whether an account is enabled, and whether it's synced from on-premisesRead-only
Update-MgUser -UserId $id -AccountEnabled:$falseBlocks sign-in for a cloud accountChanges the directory
Revoke-MgUserSignInSession -UserId $idSigns the user out of existing sessionsChanges the directory
Disable-ADAccount -Identity tbeckerDisables an account synced from on-premises Active DirectoryChanges the directory
Set-Mailbox -Identity user@contoso.com -Type SharedConverts a user mailbox to a shared mailboxChanges the mailbox
Add-MailboxPermission -Identity ... -User ... -AccessRights FullAccess -InheritanceType AllGives someone Full Access to a mailboxChanges the mailbox
Get-MessageTraceV2 -SenderAddress ... -RecipientAddress ...Traces messages from the last 90 days (10 days per query)Read-only
Get-MessageTraceDetailV2Shows each step for a traced messageRead-only
Disconnect-ExchangeOnline -Confirm:$falseEnds the sessionRead-only (sign-out)

Splatting (readable long commands)#

PowerShell
$params = @{
    ComputerName = 'PC01'
    ScriptBlock  = { Get-Service Spooler }
    ErrorAction  = 'Stop'
}
Invoke-Command @params

Sources and official documentation

Review history

  • Technical review: Jeremiah Ellington,
  • Editorial review: Jeremiah Ellington,
  • Next scheduled review:

Spotted something out of date or wrong? Email a correction and I'll fix it. See how these guides are made.