Zum Hauptinhalt springen
Diese Website enthält mit KI erstellte Inhalte. Mehr erfahren
Hintergrundbild zu „Exchange nach dem Oktober-Patchday: Zähler überwachen statt Ausfall abwarten“KI
Aktuell

Exchange nach dem Oktober-Patchday: Zähler überwachen statt Ausfall abwarten

Greift der Exchange-Workaround wirklich? Unser kostenloses PowerShell-Skript liest den RPC-Zähler aus und warnt per Mail vor dem nächsten Ausfall.

  • E-Mail
  • Monitoring
  • Updates

Seit dem Oktober-Patchday fallen Exchange-Server nach rund einem Tag aus, weil die Windows-Updates im RPC Endpoint Mapper eine neue Grenze von 512 gespeicherten Kontexten eingeführt haben. Microsoft empfiehlt als Workaround den Registry-Wert EpmapSavedContextCap. Die Hintergründe stehen in unserem Beitrag zu den Exchange-Ausfällen nach dem Oktober-Patchday. Offen bleiben zwei Fragen, die Microsoft nicht beantwortet: Wie voll ist der Zähler gerade? Und greift der Registry-Wert auf diesem Server wirklich? Dafür haben wir ein kleines Überwachungsskript gebaut, das wir hier kostenlos zur Verfügung stellen.

Gesetzt heißt nicht wirksam

Aus der Praxis

Bei einem von uns betreuten Exchange-Server war der Registry-Wert korrekt als DWORD 65536 gesetzt. Im Speicher des Endpoint Mappers stand trotzdem weiterhin 512 – der Wert war gesetzt worden, ohne dass der Server danach neu gestartet wurde. Der Zähler lag zu diesem Zeitpunkt bei 47. Ohne Blick in den Speicher wäre das erst am nächsten Morgen beim erneuten Ausfall aufgefallen. Genau diesen Fall erkennt das Skript jetzt automatisch.

Was hinter der Grenze steckt

Exchange legt im Endpoint Mapper laufend Suchkontexte an und gibt sie erst frei, wenn der zugehörige Prozess endet. Seit dem Oktober-Update zählt Windows diese Kontexte mit und verweigert ab 512 neue RPC-Verbindungen. Der Registry-Wert hebt diese Grenze an, wird aber nur beim Systemstart eingelesen. Denis Grebner hat in einem Kommentar in Borns IT- und Windows-Blog beschrieben, an welchen Stellen im Speicher Grenze und Zähler unter Windows Server 2025 liegen. Auf dieser Grundlage liest unser Skript beide Werte aus – ausschließlich lesend, ohne etwas am System zu verändern.

Standardgrenze seit Oktober

512

Gemessener Zuwachs

18 bis 24

Bis zum Ausfall

rund 21 Stunden

Was das Skript macht

Funktionen im Überblick

Zähler und Grenze

Liest unter Windows Server 2025 die aktuelle Grenze und den Zählerstand des Endpoint Mappers aus.

Workaround-Prüfung

Meldet, wenn der Registry-Wert gesetzt ist, im Speicher aber noch 512 gilt – also ein Neustart aussteht.

Funktionstest

Prüft alle 15 Minuten die echte MAPI-Verbindung, die Warteschlangen und das Ereignis 1006 mit MdbOffline.

Warnmail

Schickt eine Warnmail, sobald der Zähler 90 Prozent der Grenze erreicht – vor dem Ausfall, nicht danach.

Protokoll und Ereignisse

Schreibt jeden Lauf in eine CSV-Datei und Warnungen ins Anwendungsprotokoll, damit vorhandenes Monitoring sie aufgreifen kann.

Einrichtung in Minuten

Ein Installer prüft die Voraussetzungen, fragt die Zieladresse ab und richtet alles ein. Ebenso einfach lässt es sich wieder entfernen.

Was das Skript auf welchem Server kann

Vollständig

Windows Server 2025 mit Oktober-Update

Zähler und Grenze werden ausgelesen, der Workaround wird geprüft, alle Funktionstests laufen.

Volle Überwachung inklusive Warnmail vor dem Ausfall.

Eingeschränkt

Windows Server 2019 und 2022

Für diese Versionen sind die Speicheradressen noch nicht bekannt. MAPI-Test, Warteschlangen und Ereignis 1006 laufen trotzdem.

Überwachung meldet den Ausfall sofort, warnt aber nicht vorher.

Noch nicht betroffen

Oktober-Update noch nicht aktiv

Ist das Oktober-Update noch nicht installiert oder wartet auf den Neustart, gibt es die Grenze noch nicht. Das Skript sagt das ausdrücklich.

Nach dem Neustart füllen sich die Spalten unter Server 2025 automatisch.

Installation in drei Schritten

  1. Skript speichern: Den vollständigen Code am Ende dieses Beitrags kopieren und auf dem Exchange-Server als Install-ExRpcWatch.ps1 speichern, zum Beispiel unter C:\Temp. Das Skript enthält bewusst keine Umlaute, damit es unabhängig von der Zeichenkodierung läuft.
  2. Als Administrator ausführen: In Windows PowerShell 5.1 (nicht PowerShell 7) mit den Befehlen unten. Der Installer prüft Administratorrechte und Exchange-Umgebung, liest den Servernamen aus und fragt nach der Adresse für Warnmails.
  3. Status und Testmail prüfen: Zum Schluss zeigt der Installer die aktuellen Werte an und verschickt auf Wunsch eine Testmail. Ab dann läuft die Überwachung alle 15 Minuten als geplante Aufgabe.

Befehle

Alle Befehle in einer Windows PowerShell mit Administratorrechten auf dem Exchange-Server ausführen.

Installieren
cd C:\Temp
Unblock-File .\Install-ExRpcWatch.ps1
powershell -ExecutionPolicy Bypass -File .\Install-ExRpcWatch.ps1
Aktuellen Status anzeigen
& 'C:\Scripts\ExRpcWatch\ExRpcWatch.ps1' -Status
Testmail erneut senden
& 'C:\Scripts\ExRpcWatch\ExRpcWatch.ps1' -TestMail
Wieder entfernen
powershell -ExecutionPolicy Bypass -File .\Install-ExRpcWatch.ps1 -Uninstall

Optional lassen sich Zieladresse, Pfad, Intervall und Warnschwelle direkt übergeben, etwa mit -MailTo it@ihre-firma.de -InstallPath D:\Tools\ExRpcWatch -ThresholdPercent 80.

Was der Installer anlegt

  • Verzeichnis C:\Scripts\ExRpcWatch: das Überwachungsskript ExRpcWatch.ps1, die Einstellungen in config.json sowie nach dem ersten Lauf die Messwerte in ExRpcWatch.csv und das Protokoll ExRpcWatch.log. Beide Dateien werden ab 5 MB automatisch beiseitegelegt.
  • Geplante Aufgabe „ExRpcWatch“: läuft unter SYSTEM alle 15 Minuten, mit einem Zeitlimit von 5 Minuten und ohne parallele Läufe. Eine vorhandene Aufgabe gleichen Namens wird ersetzt.
  • Ereignisquelle „ExRpcWatch“ im Anwendungsprotokoll für die Ereignisse 9001 und 9002.
  • Registry-Wert EpmapSavedContextCap: nur, wenn Sie ausdrücklich zustimmen. Alle anderen Systemeinstellungen bleiben unverändert.

Was in der Protokolldatei steht

SpalteBedeutung
UptimeStdLaufzeit seit dem letzten Neustart in Stunden.
EpmapGrenzeDie im Speicher tatsächlich gültige Grenze: 512 ohne wirksamen Workaround, sonst der Registry-Wert.
EpmapZaehlerAktuelle Zahl der gespeicherten Kontexte. Steigt gleichmäßig und beginnt nach jedem Neustart bei null.
RegWertDer in der Registry eingetragene Wert. Weicht er von der Grenze im Speicher ab, steht ein Neustart aus.
MapiFehlerZahl der Datenbanken, bei denen der echte MAPI-Verbindungstest fehlschlägt.
QueueSummeSumme aller Nachrichten in den Warteschlangen des Servers.
Ereignis1006Exchange-Ereignisse mit MdbOffline in den letzten 15 Minuten.

Ereignisse im Anwendungsprotokoll

Ereignis-IDTypWann es geschrieben wird
9001FehlerDer Zähler hat die Warnschwelle erreicht, der MAPI-Test schlägt fehl oder Exchange meldet MdbOffline (Ereignis 1006).
9002WarnungDer Registry-Wert ist gesetzt, im Speicher gilt aber noch 512. Ein Neustart des Servers steht aus.

Quelle ist jeweils „ExRpcWatch“. Beide Ereignisse lassen sich so direkt in ein vorhandenes Monitoring übernehmen.

Auswertung: Was tun, wenn …?

Typische Befunde und die passende Maßnahme

Neustart erforderlich

Grenze 512 trotz Registry-Wert

EpmapGrenze steht auf 512, RegWert auf 65536: Der Wert ist gesetzt, aber noch nicht aktiv.

Server in einem passenden Fenster neu starten. Danach muss EpmapGrenze 65536 zeigen.

Kein Schutz

Workaround fehlt

EpmapGrenze steht auf 512, RegWert ist leer: Der Server fällt rund 21 Stunden nach dem Start aus.

Registry-Wert setzen und neu starten oder bis zu einem Microsoft-Fix regelmäßig neu starten.

Warnmail „kritisch“

Zähler nahe der Grenze

Der Zähler hat 90 Prozent der Grenze erreicht. Exchange läuft noch, der Ausfall ist aber absehbar.

Kontrollierten Neustart einplanen, bevor die Grenze erreicht ist.

Störung

Exchange gestört

MapiFehler oder Ereignis1006 sind größer als null: Exchange nimmt keine Verbindungen mehr an.

Test-MAPIConnectivity und Warteschlangen prüfen, beim bekannten Fehlerbild den Server neu starten.

In Ordnung

Workaround wirkt

EpmapGrenze steht auf 65536, der Zähler steigt langsam, MAPI-Test und Ereignisse sind unauffällig.

Nichts zu tun. Nach einem korrigierten Microsoft-Update den Wert nach Microsofts Angaben zurücknehmen.

Keine Messwerte

Zähler und Grenze leer

Entweder ist das Oktober-Update noch nicht aktiv, oder für die DLL-Version sind keine Speicheradressen bekannt.

Mit dem Schalter -Status den Grund anzeigen lassen.

Häufige Fragen

Ist das Auslesen des Speichers gefährlich?+
Nein. Das Skript öffnet den Prozess des Endpoint Mappers nur mit Leserechten und liest zwei Zahlen. Es schreibt nichts in den Speicher und verändert keine Einstellungen. Passen die gelesenen Werte nicht zur bekannten Grenze oder zum Registry-Wert, verwirft das Skript sie, statt falsche Zahlen anzuzeigen.
Ersetzt das Skript den Workaround von Microsoft?+
Nein. Der Registry-Wert verhindert den Ausfall, das Skript zeigt, ob er wirkt und wie viel Spielraum bleibt. Der Installer bietet an, den von Microsoft empfohlenen Wert 65536 zu setzen, fragt aber ausdrücklich nach.
Reicht ein Neustart der Exchange-Dienste, damit der Registry-Wert greift?+
Nein. Der Endpoint Mapper liest den Registry-Wert nur beim Systemstart ein. Ein Neustart der Exchange-Dienste kann den Zählerstand senken, weil beendete Prozesse ihre Kontexte freigeben. Die Grenze bleibt dabei aber bei 512. Die Erfahrungen von Administratoren damit sind zudem gemischt.
Wie verschickt das Skript Mails?+
Das Skript legt die Mail als Datei im Pickup-Verzeichnis von Exchange ab, Exchange stellt sie von dort zu. So sind weder ein Relay-Connector noch Zugangsdaten im Skript nötig. Pro Warnart geht höchstens eine Mail alle zwölf Stunden beziehungsweise pro Systemstart raus. Die Mail bei einer Störung ist ein Versuch nach bestem Bemühen, weil ein gestörter Server sie eventuell verzögert zustellt. Die Warnung vor dem Erreichen der Grenze kommt dagegen, solange Exchange noch läuft.
Warum bleiben Zähler und Grenze auf manchen Servern leer?+
Die Speicheradressen hängen von der genauen Version der Datei RpcEpMap.dll ab. Bekannt sind sie bisher nur für Windows Server 2025 mit dem Oktober-Update. Auf anderen Versionen bleiben Zähler und Grenze leer, das Skript nennt den Grund, und alle übrigen Prüfungen laufen weiter.
Wie ändere ich Zieladresse oder Warnschwelle nachträglich?+
Zieladresse, Warnschwelle und Mailsperre stehen in der Datei config.json im Installationsverzeichnis und lassen sich dort direkt ändern. Für ein anderes Prüfintervall den Installer einfach erneut mit dem Parameter -IntervalMinutes ausführen.
Der Installer verlangt Windows PowerShell 5.1 – warum?+
Das Skript benötigt die Exchange-Verwaltungsbefehle, und die gibt es nur in Windows PowerShell 5.1. Bitte powershell.exe verwenden, nicht pwsh.exe.
Die geplante Aufgabe schreibt keine Zeilen in die CSV-Datei+
Zuerst den Status der Aufgabe prüfen: Get-ScheduledTaskInfo -TaskName 'ExRpcWatch'. Steht LastTaskResult nicht auf 0, zeigt die Datei ExRpcWatch.log im Installationsverzeichnis den Fehler im Klartext. Häufige Ursache ist außerdem eine CSV-Datei, die in Excel geöffnet ist: Excel sperrt sie, und die Aufgabe kann nicht anhängen. Zum Ansehen deshalb eine Kopie öffnen.
MapiFehler meldet Fehler, obwohl Exchange läuft+
Den Fehler gibt der MAPI-Test selbst zurück. Test-MAPIConnectivity -Server <Servername> von Hand ausführen und die Spalte Error ansehen. Das Skript wertet das Ergebnis unabhängig von der Sprache der Exchange-Installation aus.
Die Testmail kommt nicht an+
Zuerst die Warteschlangen mit Get-Queue prüfen, dann den Spamfilter beim Empfänger. Absender ist exrpcwatch@ mit der Standarddomäne der Exchange-Organisation; diese Adresse eventuell freigeben. Scheitert schon das Ablegen der Mail, steht „Mail fehlgeschlagen“ im Protokoll ExRpcWatch.log.
Was passiert, wenn Microsoft das Problem behebt?+
Mit dem Parameter -Uninstall entfernt der Installer geplante Aufgabe und Ereignisquelle, das Verzeichnis auf Nachfrage. Den Registry-Wert fasst er bewusst nicht an. Ob und wann er nach einem korrigierten Update entfernt werden soll, sollte sich nach Microsofts Angaben richten.

Das vollständige Skript

Install-ExRpcWatch.ps1

Installer und Überwachungsskript in einer Datei. Der Installer legt das eigentliche Überwachungsskript selbst an.

Install-ExRpcWatch.ps1
<#
=====================================================================
 Install-ExRpcWatch.ps1
 Ueberwachung fuer Exchange-Server nach den Windows-Oktober-Updates 2026
 (Grenze der gespeicherten RPC-Endpoint-Mapper-Kontexte,
  Registry-Wert EpmapSavedContextCap)

 Das Skript
   - prueft die Voraussetzungen (Admin, Windows PowerShell 5.1, Exchange)
   - liest den lokalen Servernamen aus
   - fragt die Zieladresse fuer Warnmails ab
   - legt Verzeichnis, Konfiguration und Ueberwachungsskript an
   - richtet eine geplante Aufgabe ein (Standard: alle 15 Minuten)
   - bietet optional den Microsoft-Workaround (Registry-Wert) an
   - verschickt auf Wunsch eine Testmail

 Aufruf (PowerShell als Administrator, NICHT PowerShell 7):
   .\Install-ExRpcWatch.ps1
   .\Install-ExRpcWatch.ps1 -MailTo it@example.com -InstallPath D:\Tools\ExRpcWatch
   .\Install-ExRpcWatch.ps1 -Uninstall

 Wichtig:
   - Das Ueberwachungsskript liest den Prozessspeicher des Endpoint Mappers
     ausschliesslich lesend. Es veraendert nichts am System.
   - Zaehler und Grenze werden nur fuer bekannte Versionen von RpcEpMap.dll
     ausgelesen (Stand: 10.0.26100.33648, Windows Server 2025). Bei anderen
     Versionen laufen MAPI-Test, Warteschlangen- und Ereignispruefung weiter,
     die Spalten fuer Zaehler und Grenze bleiben leer.
   - Warnmails werden ueber das Pickup-Verzeichnis von Exchange zugestellt.
     Es ist kein Relay-Connector und kein Kennwort noetig.

 Stand: 19.10.2026 - codekunst systems GmbH
 Nutzung auf eigene Verantwortung. Vor dem produktiven Einsatz testen.
=====================================================================
#>

[CmdletBinding()]
param(
    [string]$MailTo,
    [string]$InstallPath = 'C:\Scripts\ExRpcWatch',
    [ValidateRange(5, 60)][int]$IntervalMinutes = 15,
    [ValidateRange(50, 99)][int]$ThresholdPercent = 90,
    [ValidateRange(1, 72)][int]$TicketLockHours = 12,
    [switch]$Uninstall
)

$TaskName    = 'ExRpcWatch'
$EventSource = 'ExRpcWatch'
$RegPath     = 'HKLM:\SOFTWARE\Microsoft\Rpc'
$RegName     = 'EpmapSavedContextCap'

function Write-Step([string]$Text) { Write-Host "==> $Text" -ForegroundColor Cyan }
function Write-Ok([string]$Text)   { Write-Host "    $Text" -ForegroundColor Green }
function Write-Warn([string]$Text) { Write-Host "    $Text" -ForegroundColor Yellow }

function Ask-YesNo([string]$Question, [bool]$Default) {
    $hint = if ($Default) { '[J/n]' } else { '[j/N]' }
    $a = Read-Host "$Question $hint"
    if ([string]::IsNullOrWhiteSpace($a)) { return $Default }
    return ($a.Trim().ToLower() -in @('j', 'ja', 'y', 'yes'))
}

# ---------------------------------------------------------------------
# Voraussetzungen
# ---------------------------------------------------------------------
Write-Step 'Pruefe Voraussetzungen'

$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
    [Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) { throw 'Bitte PowerShell als Administrator starten.' }

if ($PSVersionTable.PSVersion.Major -ne 5) {
    throw 'Bitte in Windows PowerShell 5.1 ausfuehren (powershell.exe), nicht in PowerShell 7 (pwsh.exe).'
}

if (-not $env:ExchangeInstallPath -or
    -not (Get-PSSnapin -Registered -Name 'Microsoft.Exchange.Management.PowerShell.SnapIn' -ErrorAction SilentlyContinue)) {
    throw 'Auf diesem Server ist keine Exchange-Verwaltungsumgebung installiert.'
}
Write-Ok 'Administrator, Windows PowerShell 5.1 und Exchange gefunden.'

# ---------------------------------------------------------------------
# Deinstallation
# ---------------------------------------------------------------------
if ($Uninstall) {
    Write-Step 'Entferne ExRpcWatch'
    if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) {
        Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
        Write-Ok "Geplante Aufgabe '$TaskName' entfernt."
    }
    if ([System.Diagnostics.EventLog]::SourceExists($EventSource)) {
        Remove-EventLog -Source $EventSource
        Write-Ok "Ereignisquelle '$EventSource' entfernt."
    }
    if ((Test-Path $InstallPath) -and (Ask-YesNo "Verzeichnis $InstallPath inklusive Protokollen loeschen?" $false)) {
        Remove-Item $InstallPath -Recurse -Force
        Write-Ok "Verzeichnis $InstallPath geloescht."
    }
    Write-Warn "Der Registry-Wert $RegName wurde NICHT veraendert."
    return
}

# ---------------------------------------------------------------------
# Server und Umgebung
# ---------------------------------------------------------------------
$computer = $env:COMPUTERNAME
$os       = Get-CimInstance Win32_OperatingSystem
$dll      = [System.Diagnostics.FileVersionInfo]::GetVersionInfo("$env:windir\System32\RpcEpMap.dll")
$dllVer   = '{0}.{1}.{2}.{3}' -f $dll.FileMajorPart, $dll.FileMinorPart, $dll.FileBuildPart, $dll.FilePrivatePart

Write-Step 'Server'
Write-Ok "Name           : $computer"
Write-Ok "Betriebssystem : $($os.Caption) (Build $($os.BuildNumber))"
Write-Ok "RpcEpMap.dll   : $dllVer"
if ($dllVer -ne '10.0.26100.33648') {
    Write-Warn 'Fuer diese DLL-Version sind keine Speicher-Offsets hinterlegt.'
    Write-Warn 'Zaehler und Grenze werden nicht ausgelesen, die uebrigen Pruefungen laufen.'
}

# ---------------------------------------------------------------------
# Zieladresse
# ---------------------------------------------------------------------
Write-Step 'Zieladresse fuer Warnmails'
$mailPattern = '^[^@\s]+@[^@\s]+\.[^@\s]+$'
while (-not ($MailTo -match $mailPattern)) {
    if ($MailTo) { Write-Warn "'$MailTo' ist keine gueltige Adresse." }
    $MailTo = Read-Host 'Zieladresse (z. B. ticket@ihre-firma.de)'
}
Write-Ok "Warnmails gehen an: $MailTo"

# ---------------------------------------------------------------------
# Verzeichnis und Konfiguration
# ---------------------------------------------------------------------
Write-Step "Lege $InstallPath an"
New-Item -ItemType Directory -Path $InstallPath -Force | Out-Null

[pscustomobject]@{
    Server           = $computer
    MailTo           = $MailTo
    ThresholdPercent = $ThresholdPercent
    TicketLockHours  = $TicketLockHours
    IntervalMinutes  = $IntervalMinutes
    Installed        = (Get-Date -Format s)
} | ConvertTo-Json | Set-Content -Path (Join-Path $InstallPath 'config.json') -Encoding ASCII
Write-Ok 'config.json geschrieben.'

# ---------------------------------------------------------------------
# Ueberwachungsskript
# ---------------------------------------------------------------------
$watch = @'
# =====================================================================
# ExRpcWatch.ps1 - angelegt von Install-ExRpcWatch.ps1
# Einstellungen in config.json im selben Verzeichnis.
#
#   .\ExRpcWatch.ps1            normaler Lauf (geplante Aufgabe)
#   .\ExRpcWatch.ps1 -Status    aktuelle Werte anzeigen, nichts schreiben
#   .\ExRpcWatch.ps1 -TestMail  Testmail an die Zieladresse senden
#
# Liest den Speicher des Endpoint Mappers ausschliesslich lesend.
# =====================================================================
param([switch]$Status, [switch]$TestMail)

$base  = Split-Path -Parent $MyInvocation.MyCommand.Path
$cfg   = Get-Content (Join-Path $base 'config.json') -Raw | ConvertFrom-Json
$csv   = Join-Path $base 'ExRpcWatch.csv'
$tlog  = Join-Path $base 'ExRpcWatch.log'

# Bekannte Speicher-Offsets je DLL-Version (Build.Revision)
$offsets = @{
    '26100.33648' = @{ Cap = 0x1C5EC; Cnt = 0x1CD50 }
}

# Protokolle begrenzen
foreach ($f in @($tlog, $csv)) {
    if ((Test-Path $f) -and (Get-Item $f).Length -gt 5MB) { Move-Item $f "$f.old" -Force }
}

if (-not $Status) { Start-Transcript -Path $tlog -Append | Out-Null }

try {
    Add-PSSnapin Microsoft.Exchange.Management.PowerShell.SnapIn -ErrorAction Stop

    if (-not ('EpMem' -as [type])) {
        Add-Type @"
using System;
using System.Runtime.InteropServices;
public static class EpMem {
    [DllImport("kernel32.dll", SetLastError=true)]
    public static extern IntPtr OpenProcess(uint access, bool inherit, int procId);
    [DllImport("kernel32.dll", SetLastError=true)]
    public static extern bool ReadProcessMemory(IntPtr h, IntPtr addr, byte[] buf, int size, out IntPtr read);
    [DllImport("kernel32.dll")]
    public static extern bool CloseHandle(IntPtr h);
}
"@
    }

    # --- Grunddaten ---
    $svc       = Get-CimInstance Win32_Service -Filter "Name='RpcEptMapper'"
    $os        = Get-CimInstance Win32_OperatingSystem
    $uptimeMin = ((Get-Date) - $os.LastBootUpTime).TotalMinutes
    $regVal    = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Rpc' -Name EpmapSavedContextCap -ErrorAction SilentlyContinue).EpmapSavedContextCap

    # --- MAPI-Test (sprachunabhaengig) ---
    $mapi = Test-MAPIConnectivity -Server $env:COMPUTERNAME -ErrorAction SilentlyContinue
    $mapiFail = $null
    if ($mapi) {
        $mapiFail = @($mapi | Where-Object {
            -not [string]::IsNullOrWhiteSpace("$($_.Error)") -or
            @('Success', 'Erfolgreich') -notcontains "$($_.Result)"
        }).Count
    }

    # --- Warteschlangen ---
    $queue = (Get-Queue -Server $env:COMPUTERNAME -ErrorAction SilentlyContinue | Measure-Object MessageCount -Sum).Sum

    # --- Ereignis 1006 (nur Exchange + MdbOffline, nicht in den ersten 30 Min.) ---
    $ev1006 = 0
    if ($uptimeMin -gt 30) {
        $ev1006 = @(Get-WinEvent -FilterHashtable @{ LogName = 'Application'; Id = 1006; StartTime = (Get-Date).AddMinutes(-15) } -ErrorAction SilentlyContinue |
                    Where-Object { $_.ProviderName -like 'MSExchange*' -and $_.Message -like '*MdbOffline*' }).Count
    }

    # --- Grenze und Zaehler aus dem Speicher (nur bekannte DLL-Versionen) ---
    $cap = $null; $cnt = $null; $grund = $null
    try {
        $v   = [System.Diagnostics.FileVersionInfo]::GetVersionInfo("$env:windir\System32\RpcEpMap.dll")
        $ver = "$($v.FileBuildPart).$($v.FilePrivatePart)"
        if (-not $offsets.ContainsKey($ver)) {
            if ($v.FileBuildPart -eq 26100 -and $v.FilePrivatePart -lt 33648) {
                $grund = "RpcEpMap.dll $ver ist aelter als das Oktober-Update 2026 - Update nicht installiert oder Neustart ausstehend. Die Grenze von 512 existiert hier noch nicht."
            } else {
                $grund = "Fuer RpcEpMap.dll $ver sind keine Speicher-Offsets hinterlegt (bisher nur Windows Server 2025, 10.0.26100.33648). MAPI-Test, Warteschlangen und Ereignisse werden trotzdem geprueft."
            }
        }
        else {
            $mod = (Get-Process -Id $svc.ProcessId).Modules | Where-Object { $_.ModuleName -eq 'RpcEpMap.dll' }
            $h   = [EpMem]::OpenProcess(0x0410, $false, $svc.ProcessId)
            if ($h -ne [IntPtr]::Zero -and $mod) {
                $rd = {
                    param($off)
                    $b = New-Object byte[] 4; $r = [IntPtr]::Zero
                    [void][EpMem]::ReadProcessMemory($h, [IntPtr]($mod.BaseAddress.ToInt64() + $off), $b, 4, [ref]$r)
                    [BitConverter]::ToUInt32($b, 0)
                }
                $c = & $rd $offsets[$ver].Cap
                $n = & $rd $offsets[$ver].Cnt
                [void][EpMem]::CloseHandle($h)
                # Plausibilitaet: Grenze muss 512 (Standard) oder der Registry-Wert sein
                if ($c -eq 512 -or ($regVal -and $c -eq [uint32]$regVal)) { $cap = $c; $cnt = $n }
                else { $grund = "Unplausible Grenze im Speicher ($c) - Werte verworfen." }
            }
        }
    } catch { $grund = "Speicher nicht lesbar: $($_.Exception.Message)" }
    if ($grund -and -not $Status) { Write-Output "Zaehler/Grenze leer: $grund" }

    $regAusstehend = ($regVal -and $cap -eq 512 -and [uint32]$regVal -ne 512)

    $zeile = [pscustomobject]@{
        Zeit         = Get-Date -Format s
        Server       = $env:COMPUTERNAME
        UptimeStd    = [math]::Round($uptimeMin / 60, 1)
        EpmapGrenze  = $cap
        EpmapZaehler = $cnt
        RegWert      = $regVal
        MapiFehler   = $mapiFail
        QueueSumme   = $queue
        Ereignis1006 = $ev1006
    }

    if ($Status) {
        $zeile | Format-List
        if ($grund) { Write-Output "Hinweis zu Zaehler/Grenze: $grund" }
        if ($regAusstehend) { Write-Output 'Hinweis: Registry-Wert gesetzt, aber noch nicht aktiv - Neustart erforderlich.' }
        return
    }

    $zeile | Export-Csv $csv -Append -NoTypeInformation -Delimiter ';' -Encoding UTF8

    # --- Bewertung ---
    $schwelle = [double]$cfg.ThresholdPercent / 100
    $kritisch = ($cnt -ne $null -and $cap -gt 0 -and $cnt -gt ($schwelle * $cap))
    $ausfall  = (($mapiFail -gt 0) -or ($ev1006 -gt 0))

    if (-not [System.Diagnostics.EventLog]::SourceExists('ExRpcWatch')) {
        New-EventLog -LogName Application -Source 'ExRpcWatch'
    }
    $info = "MAPI-Fehler: $mapiFail, Ereignis 1006 (MdbOffline): $ev1006, Queue: $queue, " +
            "Uptime: $($zeile.UptimeStd) h, Zaehler: $cnt / Grenze: $cap, Registry: $regVal"

    if ($kritisch -or $ausfall) {
        Write-EventLog -LogName Application -Source 'ExRpcWatch' -EventId 9001 -EntryType Error -Message "ExRpcWatch: $info"
    } elseif ($regAusstehend) {
        Write-EventLog -LogName Application -Source 'ExRpcWatch' -EventId 9002 -EntryType Warning `
            -Message "ExRpcWatch: Registry-Wert $regVal gesetzt, im Speicher gilt noch 512. Neustart erforderlich. $info"
    }

    # --- Mail ueber das Pickup-Verzeichnis ---
    function Send-ExTicket([string]$Art, [string]$Betreff, [string[]]$Text, [switch]$Force) {
        $marker = Join-Path $base "ticket_$Art.marker"
        if (-not $Force -and (Test-Path $marker)) {
            $z = (Get-Item $marker).LastWriteTime
            if ($z -gt $os.LastBootUpTime -and $z -gt (Get-Date).AddHours(-[int]$cfg.TicketLockHours)) { return }
        }
        try {
            $pickup = "$((Get-TransportService -Identity $env:COMPUTERNAME).PickupDirectoryPath)"
            if (-not $pickup) { $pickup = Join-Path $env:ExchangeInstallPath 'TransportRoles\Pickup' }
            $domain = "$((Get-AcceptedDomain | Where-Object { $_.Default }).DomainName)"
            $mail = @(
                "From: ExRpcWatch <exrpcwatch@$domain>",
                "To: $($cfg.MailTo)",
                "Subject: $Betreff",
                "Date: $((Get-Date).ToUniversalTime().ToString('r'))",
                'MIME-Version: 1.0',
                'Content-Type: text/plain; charset=us-ascii',
                'X-ExRpcWatch: 1',
                ''
            ) + $Text
            $datei = Join-Path $pickup ("ExRpcWatch_{0}_{1}.eml" -f $Art, (Get-Date -Format 'yyyyMMdd_HHmmss'))
            [System.IO.File]::WriteAllText($datei, ($mail -join "`r`n"), [System.Text.Encoding]::ASCII)
            if (-not $Force) { Set-Content -Path $marker -Value (Get-Date -Format s) }
            Write-Output "Mail abgelegt: $datei"
        } catch { Write-Output "Mail fehlgeschlagen: $($_.Exception.Message)" }
    }

    $details = @(
        "Server        : $env:COMPUTERNAME",
        "Zeit          : $(Get-Date -Format 'dd.MM.yyyy HH:mm:ss')",
        "Laufzeit      : $($zeile.UptimeStd) Stunden seit $($os.LastBootUpTime.ToString('dd.MM.yyyy HH:mm'))",
        "Epmap-Zaehler : $cnt",
        "Epmap-Grenze  : $cap",
        "Registry-Wert : $regVal",
        "MAPI-Fehler   : $mapiFail",
        "Queue         : $queue",
        "Ereignis 1006 : $ev1006",
        '',
        'Automatisch erzeugt von ExRpcWatch.ps1'
    )

    if ($TestMail) {
        Send-ExTicket -Art 'test' -Force -Betreff "[ExRpcWatch] TEST $env:COMPUTERNAME - Testmail" `
            -Text (@('Dies ist eine Testmail von ExRpcWatch.', '') + $details)
    }
    if ($kritisch) {
        Send-ExTicket -Art 'kritisch' -Betreff "[ExRpcWatch] $env:COMPUTERNAME - RPC-Zaehler kritisch ($cnt von $cap)" `
            -Text (@('Der Zaehler der gespeicherten RPC-Endpoint-Mapper-Kontexte hat die Warnschwelle erreicht.',
                     'Bei Erreichen der Grenze faellt Exchange aus (MapiExceptionMdbOffline).',
                     'Massnahme: kontrollierten Neustart einplanen, EpmapSavedContextCap pruefen.', '') + $details)
    }
    if ($ausfall) {
        Send-ExTicket -Art 'ausfall' -Betreff "[ExRpcWatch] $env:COMPUTERNAME - Exchange-Stoerung (MdbOffline/MAPI)" `
            -Text (@('Exchange meldet MAPI-Fehler oder MdbOffline-Ereignisse.',
                     'Hinweis: Bei einem Ausfall kann diese Mail verzoegert zugestellt werden.', '') + $details)
    }
}
catch { Write-Output "FEHLER: $($_.Exception.Message)" }
finally { if (-not $Status) { Stop-Transcript | Out-Null } }
'@

$watchPath = Join-Path $InstallPath 'ExRpcWatch.ps1'
Set-Content -Path $watchPath -Value $watch -Encoding ASCII
Write-Ok "Ueberwachungsskript angelegt: $watchPath"

# ---------------------------------------------------------------------
# Ereignisquelle
# ---------------------------------------------------------------------
if (-not [System.Diagnostics.EventLog]::SourceExists($EventSource)) {
    New-EventLog -LogName Application -Source $EventSource
    Write-Ok "Ereignisquelle '$EventSource' registriert."
}

# ---------------------------------------------------------------------
# Geplante Aufgabe
# ---------------------------------------------------------------------
Write-Step "Richte geplante Aufgabe '$TaskName' ein (alle $IntervalMinutes Minuten)"
$psExe     = "$env:windir\System32\WindowsPowerShell\v1.0\powershell.exe"
$action    = New-ScheduledTaskAction -Execute $psExe -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$watchPath`""
$trigger   = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval (New-TimeSpan -Minutes $IntervalMinutes)
$settings  = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 5) -MultipleInstances IgnoreNew -StartWhenAvailable
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Settings $settings -Principal $principal -Force | Out-Null
Write-Ok 'Geplante Aufgabe eingerichtet (Konto SYSTEM, Zeitlimit 5 Minuten).'

# ---------------------------------------------------------------------
# Registry-Workaround (optional)
# ---------------------------------------------------------------------
Write-Step 'Microsoft-Workaround EpmapSavedContextCap'
$regVal = (Get-ItemProperty $RegPath -Name $RegName -ErrorAction SilentlyContinue).$RegName
if ($regVal) {
    Write-Ok "Bereits gesetzt: $regVal"
} else {
    Write-Warn 'Nicht gesetzt. Microsoft empfiehlt den Wert 65536 (wirksam erst nach einem Neustart).'
    if (Ask-YesNo 'Jetzt auf 65536 setzen?' $false) {
        New-ItemProperty -Path $RegPath -Name $RegName -PropertyType DWord -Value 65536 -Force | Out-Null
        Write-Ok 'Gesetzt. Bitte den Server in einem passenden Fenster neu starten.'
    }
}

# ---------------------------------------------------------------------
# Erster Lauf und Testmail
# ---------------------------------------------------------------------
Write-Step 'Aktueller Status'
& $watchPath -Status

if (Ask-YesNo "Testmail an $MailTo senden?" $true) {
    & $watchPath -TestMail | Out-Null
    Write-Ok 'Testmail im Pickup-Verzeichnis abgelegt. Bitte Eingang pruefen.'
}

Write-Step 'Fertig'
Write-Ok "Protokolle   : $InstallPath\ExRpcWatch.csv und ExRpcWatch.log"
Write-Ok "Status       : & '$watchPath' -Status"
Write-Ok "Ereignisse   : Anwendungsprotokoll, Quelle $EventSource, ID 9001 (Fehler) und 9002 (Neustart ausstehend)"
Write-Ok "Entfernen    : .\Install-ExRpcWatch.ps1 -Uninstall -InstallPath '$InstallPath'"

Den gesamten Inhalt mit der Kopieren-Schaltfläche übernehmen, in einem Editor einfügen und als Install-ExRpcWatch.ps1 speichern. Das Skript enthält nur ASCII-Zeichen.

Bitte beachten

Das Skript ist ein Hilfsmittel für Administratoren und keine offizielle Lösung von Microsoft. Die Speicheradressen stammen aus einer unabhängigen Analyse und können sich mit jedem Update ändern. Bitte vor dem produktiven Einsatz testen. Die Nutzung erfolgt auf eigene Verantwortung. Wenn Sie Unterstützung bei Einrichtung oder Auswertung brauchen, sprechen Sie uns gern an.

Quellen & weiterführende Links

Stand: 19. Oktober 2026. Unser Dank geht an Denis Grebner für die Analyse von RpcEpMap.dll.

Exchange: RPC-Zähler überwachen – kostenloses Skript