From 9594dab8282dee2ff60b7e43a6ffe499fc3cfc51 Mon Sep 17 00:00:00 2001 From: SirTrek <4106300+SirTrek@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:57:01 -0600 Subject: [PATCH 1/3] Add view/edit page for Remote Mailbox attributes The Remote Mailboxes list already linked to /editremotemailbox?id=... but that route and page did not exist, so clicking a mailbox 404'd. Adds a GET /editremotemailbox route plus web/editremotemailbox.html to: - View a mailbox's Name, PrimarySmtpAddress, and RemoteRoutingAddress - Add or remove proxy addresses (EmailAddresses aliases) - Toggle HiddenFromAddressListsEnabled - Change RemoteRoutingAddress Follows the existing pattern used by the other sections: plain GET forms, server-side HTML templating, and the on-prem Recipient Management Tools snap-in already loaded by the script. --- Start-ExchangeRecipientAdminCenter.ps1 | 114 ++++++++++++- web/editremotemailbox.html | 218 +++++++++++++++++++++++++ 2 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 web/editremotemailbox.html diff --git a/Start-ExchangeRecipientAdminCenter.ps1 b/Start-ExchangeRecipientAdminCenter.ps1 index 289d387..df80f22 100644 --- a/Start-ExchangeRecipientAdminCenter.ps1 +++ b/Start-ExchangeRecipientAdminCenter.ps1 @@ -139,7 +139,119 @@ try { break } - "GET /distributiongroups" { + "GET /editremotemailbox" { + # View / edit a single Remote Mailbox's attributes + + $Table = @{} + if ($REQUEST.Url.Query) { + foreach ($Item in [URI]::UnescapeDataString(($REQUEST.Url.Query.Replace("?", ""))).Split("&")) { + $Table.Add($Item.Split("=")[0], $Item.Split("=")[1]) + } + } + + $Identity = $Table['id'] + + # Apply a requested change, if any, before re-reading the mailbox + if ($Table.ContainsKey('action')) { + try { + switch ($Table['action']) { + "addalias" { + $NewAlias = "$($Table['alias_local'])@$($Table['alias_accepteddomain'])" + Set-RemoteMailbox -Identity $Identity -EmailAddresses @{Add = $NewAlias } + $HTML_RESULT = $HTML_SUCCESS.Replace("{result}", "Added alias $NewAlias") + break + } + "removealias" { + Set-RemoteMailbox -Identity $Identity -EmailAddresses @{Remove = $Table['alias'] } + $HTML_RESULT = $HTML_SUCCESS.Replace("{result}", "Removed alias $($Table['alias'])") + break + } + "togglehidden" { + $NewHidden = $Table['hidden'] -eq 'true' + Set-RemoteMailbox -Identity $Identity -HiddenFromAddressListsEnabled $NewHidden + $HTML_RESULT = $HTML_SUCCESS.Replace("{result}", "Set 'Hidden from address lists' to $NewHidden") + break + } + "setrra" { + Set-RemoteMailbox -Identity $Identity -RemoteRoutingAddress $Table['newrra'] + $HTML_RESULT = $HTML_SUCCESS.Replace("{result}", "Remote routing address updated to $($Table['newrra'])") + break + } + } + } + catch { + $HTML_RESULT = $HTML_WARN.Replace("{result}", $Error -join "
") + } + } + + # Look up the mailbox to display + $Mailbox = Get-RemoteMailbox -Identity $Identity -ErrorAction SilentlyContinue + + if (-not $Mailbox) { + $RESPONSE.StatusCode = 404 + $HTMLRESPONSE = "Remote mailbox '$($Identity)' not found" + break + } + + # Prepare accepted domain list, used for the "add alias" domain picker + $HTMLROWS_AD = "" + foreach ($Item in (Get-AcceptedDomain)) { + $HTMLROWS_AD += "`n" + } + + # Prepare proxy address (EmailAddresses) rows, each with a remove button + $HTMLROWS_PROXY = "" + foreach ($Addr in $Mailbox.EmailAddresses) { + $AddrString = $Addr.ToString() + $IsPrimary = $AddrString.StartsWith("SMTP:") + if ($IsPrimary) { + $Badge = "Primary" + $RemoveBtn = "" + } + else { + $Badge = "Alias" + $EncodedId = [URI]::EscapeDataString($Identity) + $EncodedAddr = [URI]::EscapeDataString($AddrString) + $RemoveBtn = "Remove" + } + $HTMLROWS_PROXY += " + + $($AddrString) + $($Badge) + $($RemoveBtn) + "; + } + + if ($Mailbox.HiddenFromAddressListsEnabled) { + $HiddenBadgeClass = "text-bg-warning" + $HiddenStatusText = "Hidden" + $HiddenToggleValue = "false" + $HiddenToggleLabel = "Unhide from GAL" + } + else { + $HiddenBadgeClass = "text-bg-success" + $HiddenStatusText = "Visible" + $HiddenToggleValue = "true" + $HiddenToggleLabel = "Hide from GAL" + } + + # Create response and replace template placeholders + $HTMLRESPONSE = Get-Content -Path "$($BASEDIR)\editremotemailbox.html" + $HTMLRESPONSE = $HTMLRESPONSE.Replace("{display_name}", $Mailbox.Name) + $HTMLRESPONSE = $HTMLRESPONSE.Replace("{primarysmtpaddress}", $Mailbox.PrimarySmtpAddress) + $HTMLRESPONSE = $HTMLRESPONSE.Replace("{remoteroutingaddress}", $Mailbox.RemoteRoutingAddress) + $HTMLRESPONSE = $HTMLRESPONSE.Replace("{id}", [URI]::EscapeDataString($Identity)) + $HTMLRESPONSE = $HTMLRESPONSE.Replace("{hidden_badge_class}", $HiddenBadgeClass) + $HTMLRESPONSE = $HTMLRESPONSE.Replace("{hidden_status_text}", $HiddenStatusText) + $HTMLRESPONSE = $HTMLRESPONSE.Replace("{hidden_toggle_value}", $HiddenToggleValue) + $HTMLRESPONSE = $HTMLRESPONSE.Replace("{hidden_toggle_label}", $HiddenToggleLabel) + $HTMLRESPONSE = $HTMLRESPONSE.Replace("", $HTMLROWS_PROXY) + $HTMLRESPONSE = $HTMLRESPONSE.Replace("", $HTMLROWS_AD) + $HTMLRESPONSE = $HTMLRESPONSE.Replace("", $HTML_RESULT) + break + } + + "GET /distributiongroups" { # Distribution Groups Section # Prepare Distibution Group lists split into tabs diff --git a/web/editremotemailbox.html b/web/editremotemailbox.html new file mode 100644 index 0000000..ce5d1d5 --- /dev/null +++ b/web/editremotemailbox.html @@ -0,0 +1,218 @@ + + + + + + + + Exchange Recipient Admin Center - Remote Mailbox + + + + + + + + + + + +
+
+ + +
+
+

{display_name}

+ Back to Remote Mailboxes +
+ + + +
+
Properties
+
+
+
Name
+
{display_name}
+ +
Primary SMTP Address
+
{primarysmtpaddress}
+ +
Hidden from Address Lists
+
+ {hidden_status_text} +
+ + + + +
+
+
+
+
+ +
+
Remote Routing Address
+
+

The cloud mailbox this Remote Mailbox is + routed to (RemoteRoutingAddress).

+
+ + +
+ +
+
+ +
+
+
+
+ +
+
+ Email Addresses (Proxy Addresses) + +
+
+ + + + + + + + + + + +
AddressType
+
+
+ + + +
+
+
+ + + From c8b4f274e3c7cf605fb522504a81f5a1df832299 Mon Sep 17 00:00:00 2001 From: SirTrek <4106300+SirTrek@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:57:27 -0600 Subject: [PATCH 2/3] Prevent a single dropped client connection from crashing the webserver The whole request loop ran inside one try/finally with no catch, so any exception writing a response (e.g. the client closes the connection or navigates away before the write completes, surfacing as "The specified network name is no longer available") propagated out of the while loop and killed the entire listener for all users. Wraps each request's handling in its own try/catch so a single failed request is logged and the server keeps listening. --- Start-ExchangeRecipientAdminCenter.ps1 | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Start-ExchangeRecipientAdminCenter.ps1 b/Start-ExchangeRecipientAdminCenter.ps1 index df80f22..dd73e1e 100644 --- a/Start-ExchangeRecipientAdminCenter.ps1 +++ b/Start-ExchangeRecipientAdminCenter.ps1 @@ -52,6 +52,7 @@ try { "$(Get-Date -Format s) Powershell webserver started." $WEBLOG = "$(Get-Date -Format s) Powershell webserver started.`n" while ($LISTENER.IsListening) { + try { # analyze incoming request $CONTEXT = $LISTENER.GetContext() $REQUEST = $CONTEXT.Request @@ -446,6 +447,15 @@ try { "$(Get-Date -Format s) Stopping powershell webserver..." break; } + } + catch { + # A single request failing (e.g. client closed the connection before the + # response finished writing - "network name is no longer available") should + # not take down the whole webserver. Log it and keep listening. + "$(Get-Date -Format s) Request error: $($_.Exception.Message)" + $WEBLOG += "$(Get-Date -Format s) Request error: $($_.Exception.Message)`n" + try { $RESPONSE.Close() } catch { } + } } } finally { From c031cb452a9a9829a99975fff7300a501ffa0996 Mon Sep 17 00:00:00 2001 From: SirTrek <4106300+SirTrek@users.noreply.github.com> Date: Thu, 9 Jul 2026 09:49:41 -0600 Subject: [PATCH 3/3] Add ability to disable a Remote Mailbox Adds a "Danger Zone" card to the Edit Remote Mailbox page with a type-to-confirm control (must type the mailbox's PrimarySmtpAddress before the Disable button enables) backed by a server-side check of the same confirmation text, since the client-side JS is trivially bypassable. Disable-RemoteMailbox strips the Exchange attributes from the AD object; it does not delete the Exchange Online mailbox itself. Since the mailbox no longer exists afterward, the GET handler renders the Remote Mailboxes list directly instead of a 404 edit page for an identity that's gone. Also wraps feather.replace() in try/catch as cheap insurance against icon-library hiccups blocking the confirm-button script - not an active bug on this branch (feather-icons is vendored locally here, not loaded from an unpinned CDN like on the dev branch), but harmless and consistent with the dev-branch fix for the same page. --- Start-ExchangeRecipientAdminCenter.ps1 | 63 ++++++++++++++++++++++++++ web/editremotemailbox.html | 45 +++++++++++++++++- 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/Start-ExchangeRecipientAdminCenter.ps1 b/Start-ExchangeRecipientAdminCenter.ps1 index dd73e1e..3dca292 100644 --- a/Start-ExchangeRecipientAdminCenter.ps1 +++ b/Start-ExchangeRecipientAdminCenter.ps1 @@ -151,6 +151,7 @@ try { } $Identity = $Table['id'] + $MailboxDisabled = $false # Apply a requested change, if any, before re-reading the mailbox if ($Table.ContainsKey('action')) { @@ -178,6 +179,24 @@ try { $HTML_RESULT = $HTML_SUCCESS.Replace("{result}", "Remote routing address updated to $($Table['newrra'])") break } + "disable" { + # Require the confirmation text to match the mailbox's actual + # PrimarySmtpAddress - checked server-side too, not just via the + # UI's type-to-confirm JS, since that's trivially bypassed. + $MailboxToDisable = Get-RemoteMailbox -Identity $Identity -ErrorAction SilentlyContinue + if (-not $MailboxToDisable) { + $HTML_RESULT = $HTML_WARN.Replace("{result}", "Remote mailbox '$($Identity)' not found") + } + elseif ($Table['confirmAddress'] -ne $MailboxToDisable.PrimarySmtpAddress) { + $HTML_RESULT = $HTML_WARN.Replace("{result}", "Confirmation text didn't match $($MailboxToDisable.PrimarySmtpAddress) - no changes were made") + } + else { + Disable-RemoteMailbox -Identity $Identity -Confirm:$false + $HTML_RESULT = $HTML_SUCCESS.Replace("{result}", "Remote Mailbox for $($MailboxToDisable.PrimarySmtpAddress) has been disabled") + $MailboxDisabled = $true + } + break + } } } catch { @@ -185,6 +204,50 @@ try { } } + # A disabled mailbox no longer exists to render an edit page for - + # send the user back to the Remote Mailboxes list instead. + if ($MailboxDisabled) { + $HTMLROWS_USERS = "" + foreach ($Item in (Get-User -Filter "RecipientType -eq 'User' -and RecipientTypeDetails -ne 'DisabledUser'" | Where { $_.UserPrincipalName })) { + $HTMLROWS_USERS += "`n" + } + + $HTMLROWS_AD = "" + foreach ($Item in (Get-AcceptedDomain)) { + if ($Item.Default) { + $HTMLROWS_AD += "`n" + } + else { + $HTMLROWS_AD += "`n" + } + } + + $HTMLROWS_RRA = "" + foreach ($Item in (Get-AcceptedDomain)) { + if ($Item.DomainName -like "*.mail.onmicrosoft.com") { + $HTMLROWS_RRA += "`n" + } + else { + $HTMLROWS_RRA += "`n" + } + } + + $HTMLROWS_MBX = "" + foreach ($Item in (Get-RemoteMailbox | Select DisplayName, PrimarySMTPAddress, RecipientTypeDetails, WhenChanged)) { + $HTMLROWS_MBX += " + + + $($Item.DisplayName) + $($Item.PrimarySMTPAddress) + $($Item.RecipientTypeDetails) + $($Item.WhenChanged) + "; + } + + $HTMLRESPONSE = (Get-Content -Path "$($BASEDIR)\remotemailboxes.html").Replace("", $HTMLROWS_MBX).Replace("", $HTMLROWS_AD).Replace("", $HTMLROWS_USERS).Replace("", $HTMLROWS_RRA).Replace("", $HTML_RESULT) + break + } + # Look up the mailbox to display $Mailbox = Get-RemoteMailbox -Identity $Identity -ErrorAction SilentlyContinue diff --git a/web/editremotemailbox.html b/web/editremotemailbox.html index ce5d1d5..78aa838 100644 --- a/web/editremotemailbox.html +++ b/web/editremotemailbox.html @@ -208,11 +208,54 @@ +
+
Danger Zone
+
+

Disabling this Remote Mailbox removes its Exchange attributes + from Active Directory. It will no longer sync as a Remote + Mailbox. The Exchange Online mailbox itself is not deleted by + this action.

+
+ + +
+ + +
+ +
+
+
+