Why PowerShell fails to detect the new Outlook in Windows (and how to fix it)
Back to blog

Why PowerShell fails to detect the new Outlook in Windows (and how to fix it)

9/28/2026 · 2 min · Infrastructure

Anyone writing automation or support scripts for Windows has built a diagnostic collector: a lightweight script users run to gather OS builds, network interfaces, public/local IPs, DNS lookups, open ports (443, 587), certificate expiration dates, and traceroute paths.

While testing a PowerShell collector designed for email connectivity triage, I encountered a strange behavior. With the New Outlook open and running on the taskbar, the diagnostic report insisted:

== OUTLOOK VERSION (INSTALLED OR NOT) ==
Outlook not found

The application was actively running in front of me, yet the script claimed it was not installed on the system.

Here is why traditional Windows Registry checks fail on modern Office versions, and how to reliably detect Outlook using Get-AppxPackage.


1. Root cause: the false assumption of registry keys#

The original verification logic relied on user registry keys (HKCU), searching for traditional Office suite flags:

# ORIGINAL FLAWED APPROACH
Write-Line "`n== OUTLOOK VERSION =="
$officeRegPaths = @(
    "HKCU:\Software\Microsoft\Office\16.0\Common\ExperimentEcs\Overrides",
    "HKCU:\Software\Microsoft\Office\16.0\Outlook\Options"
)
$outlookStatus = "Outlook not found"
foreach ($path in $officeRegPaths) {
    if (Test-Path $path) {
        $reg = Get-ItemProperty -Path $path -ErrorAction SilentlyContinue
        if ($reg -ne $null -and $reg.EnableNewOutlook -eq 1) {
            $outlookStatus = "New Experience (New)"
        } elseif ($reg -ne $null -and $reg.EnableNewOutlook -eq 0) {
            $outlookStatus = "Classic Experience (Classic)"
        }
    }
}
Write-Line $outlookStatus

Why did this fail?#

This logic assumes that Classic Outlook (Win32 / MSO) is installed and the user toggled the "Try the new Outlook" switch. In that specific upgrade path, Office writes the EnableNewOutlook key into HKCU.

However, the New Outlook is an AppX/MSIX packaged app (a UWP/PWA distributed via the Microsoft Store or pre-provisioned in Windows 11). On machines provisioned with New Outlook directly, or where the traditional Win32 Office package was never installed, the entire registry tree HKCU:\Software\Microsoft\Office\16.0 simply does not exist.

Because the path was not found, the loop exited silently and the status remained set to "Outlook not found".


2. The solution: appx inspection with Get-AppxPackage#

To reliably identify the email client, the detection order must be inverted:

  1. First priority: Query the Windows modern app catalog (Get-AppxPackage -Name "Microsoft.OutlookForWindows").
  2. Second step (Fallback): If no AppX package is found, inspect traditional Click-to-Run registry settings in HKLM.
  3. Third step (Legacy fallback): Check for older MSI installations in HKCU.

Here is the refactored detection block:

# Outlook Version Detection (New Outlook and Classic Support)
Write-Line "`n== OUTLOOK VERSION (INSTALLED OR NOT) =="
$outlookStatus = "Outlook not found"

try {
    # 1. Primary check for modern New Outlook App (Store / PWA)
    $newOutlook = Get-AppxPackage -Name "Microsoft.OutlookForWindows" -ErrorAction SilentlyContinue
    if ($newOutlook) {
        $outlookStatus = "New Experience (New Outlook installed via App)"
    } else {
        # 2. Check for Classic Office installation (Click-to-Run)
        $classicOutlookPath = "HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration"
        if (Test-Path $classicOutlookPath) {
             $officeSuite = Get-ItemProperty -Path $classicOutlookPath
             if ($officeSuite.ProductReleaseIds -like "*Outlook*") {
                 $outlookStatus = "Classic Experience installed via ClickToRun"
             }
        } else {
             # 3. Fallback for legacy MSI installations
             $classicOutlookMSIPath = "HKCU:\Software\Microsoft\Office\16.0\Outlook\Setup"
             if (Test-Path $classicOutlookMSIPath) {
                $outlookStatus = "Classic Experience installed via MSI"
             }
        }
    }
} catch {
    $outlookStatus = "Error checking Outlook version."
}

Write-Line $outlookStatus

3. Complete network and mail diagnostic script#

Below is the complete, sanitized PowerShell collector script. It includes character normalization, raw .NET TCP socket tests, user root certificate expiration auditing, and real-time tracert progress tracking:

# Diagnostic Collector Script - Final Version
$DataHora = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
$arquivo = "C:\diagnostics_$DataHora.txt"

# Function to remove diacritics before writing
function Remove-Acento {
    param ($texto)
    $normalized = $texto.Normalize([System.Text.NormalizationForm]::FormD)
    return -join ($normalized.ToCharArray() | Where-Object { [System.Globalization.CharUnicodeInfo]::GetUnicodeCategory($_) -ne 'NonSpacingMark' })
}

# Fast TCP socket connectivity test
function Test-Port {
    param(
        [string]$hostname,
        [int]$port,
        [int]$timeout = 3000
    )
    $tcpClient = New-Object System.Net.Sockets.TcpClient
    try {
        $asyncResult = $tcpClient.BeginConnect($hostname, $port, $null, $null)
        $wait = $asyncResult.AsyncWaitHandle.WaitOne($timeout, $false)
        if (!$wait) {
            return $false
        }
        $tcpClient.EndConnect($asyncResult)
        return $true
    }
    catch {
        return $false
    }
    finally {
        $tcpClient.Close()
    }
}

function Write-Line {
    param([string]$line)
    $cleanLine = Remove-Acento $line
    $cleanLine | Out-File -FilePath $arquivo -Append
}

Write-Line "===== DIAGNOSTIC START =====`n"
Write-Line "Timestamp: $DataHora`n"

# 1. System Information
Write-Line "== SYSTEM INFO =="
systeminfo | Out-String | ForEach-Object { Write-Line $_ }

# 2. Logged In User
Write-Line "`n== LOGGED IN USER =="
whoami | Out-String | ForEach-Object { Write-Line $_ }

# 3. Local & Public IP
Write-Line "`n== LOCAL & PUBLIC IP =="
Get-NetIPAddress -AddressFamily IPv4 | Where-Object {$_.InterfaceAlias -notlike "Loopback*"} | Select-Object IPAddress, InterfaceAlias | Out-String | ForEach-Object { Write-Line $_ }

Write-Line "`nPublic IP:"
try {
    $ipPublico = Invoke-RestMethod -Uri 'https://api.ipify.org' -UseBasicParsing
    Write-Line $ipPublico
} catch {
    Write-Line "Could not retrieve public IP."
}

# 4. Outlook Version Detection
Write-Line "`n== OUTLOOK VERSION (INSTALLED OR NOT) =="
$outlookStatus = "Outlook not found"

try {
    $newOutlook = Get-AppxPackage -Name "Microsoft.OutlookForWindows" -ErrorAction SilentlyContinue
    if ($newOutlook) {
        $outlookStatus = "New Experience (New Outlook installed via App)"
    } else {
        $classicOutlookPath = "HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration"
        if (Test-Path $classicOutlookPath) {
             $officeSuite = Get-ItemProperty -Path $classicOutlookPath
             if ($officeSuite.ProductReleaseIds -like "*Outlook*") {
                 $outlookStatus = "Classic Experience installed via ClickToRun"
             }
        } else {
             $classicOutlookMSIPath = "HKCU:\Software\Microsoft\Office\16.0\Outlook\Setup"
             if (Test-Path $classicOutlookMSIPath) {
                $outlookStatus = "Classic Experience installed via MSI"
             }
        }
    }
} catch {
    $outlookStatus = "Error checking Outlook version."
}

Write-Line $outlookStatus

# 5. DNS & Microsoft 365 Connectivity
Write-Line "`n== DNS & M365 CONNECTIVITY =="
Write-Line "Resolving outlook.office365.com:"

$proc = Start-Process nslookup -ArgumentList "outlook.office365.com" -NoNewWindow -RedirectStandardOutput nslookup_output.txt -PassThru
if ($proc.WaitForExit(5000)) {
    Get-Content nslookup_output.txt | ForEach-Object { Write-Line $_ }
} else {
    $proc.Kill()
    Write-Line "Timeout on nslookup outlook.office365.com"
}
Remove-Item nslookup_output.txt -ErrorAction SilentlyContinue

Write-Line "Testing outlook.office365.com port 443:"
if (Test-Port -hostname "outlook.office365.com" -port 443) {
    Write-Line "Port 443 is open and reachable."
} else {
    Write-Line "Failed to connect on port 443."
}

Write-Line "Testing smtp.office365.com port 587:"
if (Test-Port -hostname "smtp.office365.com" -port 587) {
    Write-Line "Port 587 is open and reachable."
} else {
    Write-Line "Failed to connect on port 587."
}

# 6. Installed Root Certificates
Write-Line "`n== ROOT CERTIFICATES (EXPIRING SOON) =="
$certs = Get-ChildItem -Path Cert:\CurrentUser\Root | Where-Object { $_.NotAfter -lt (Get-Date).AddDays(30) }
if ($certs.Count -eq 0) {
    Write-Line "All certificates are valid for at least 30 days."
} else {
    Write-Line "WARNING: Found expiring or expired certificates:"
    $certs | Select-Object Subject, NotAfter | Format-Table -AutoSize | Out-String | ForEach-Object { Write-Line $_ }
}

# 7. Real-Time Traceroute
Write-Line "`n== TRACEROUTE =="
$hostsList = @(
    @{Nome="antispam-01.yourdomain.com"; IP="192.0.2.10"; Ordem=10},
    @{Nome="antispam-02.yourdomain.com"; IP="198.51.100.20"; Ordem=20}
)

function Run-TracertWithProgress {
    param(
        [string]$ip,
        [string]$nome,
        [int]$maxHops = 30
    )

    Write-Line "`n-> Traceroute to $nome [$ip]"

    $proc = New-Object System.Diagnostics.Process
    $proc.StartInfo.FileName = "tracert.exe"
    $proc.StartInfo.Arguments = $ip
    $proc.StartInfo.UseShellExecute = $false
    $proc.StartInfo.RedirectStandardOutput = $true
    $proc.StartInfo.CreateNoWindow = $true

    $proc.Start() | Out-Null

    while (-not $proc.HasExited) {
        while (!$proc.StandardOutput.EndOfStream) {
            $line = $proc.StandardOutput.ReadLine()
            Write-Line $line
            if ($line -match '^\s*(\d+)') {
                $hopCount = [int]$matches[1]
                $percent = [int](($hopCount / $maxHops) * 100)
                Write-Progress -Activity "Running Tracert" -Status "Hop $hopCount of $maxHops" -PercentComplete $percent
            }
        }
        Start-Sleep -Milliseconds 200
    }

    while (!$proc.StandardOutput.EndOfStream) {
        $line = $proc.StandardOutput.ReadLine()
        Write-Line $line
    }

    Write-Progress -Activity "Running Tracert" -Completed
}

foreach ($servidor in $hostsList | Sort-Object Ordem) {
    Run-TracertWithProgress -ip $servidor.IP -nome $servidor.Nome -maxHops 30
}

Write-Line "`n===== DIAGNOSTIC END ====="

# Launch notepad for immediate review
Start-Process notepad.exe $arquivo

Practical rule for Windows scripting#

The transition from Win32 software to packaged Store apps changes where Windows stores application metadata. When authoring PowerShell collectors, checking Get-AppxPackage before examining HKCU:\Software\Microsoft\Office eliminates false negatives and ensures accurate diagnostics across modern Windows endpoints.

Was this article helpful?

Leave a quick reaction to help prioritize future technical guides:

CC BY-NC

This post is licensed under CC BY-NC.

Comments

Join the discussion below.

0 comments