From d8febaa222c4b2d7f0738aef1e9e79058cf24e19 Mon Sep 17 00:00:00 2001 From: SirTrek <4106300+SirTrek@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:07:32 -0600 Subject: [PATCH 1/7] Extend Edit Remote Mailbox page with alias and GAL management The existing editremotemailbox page (GET/POST) only edited DisplayName, PrimarySmtpAddress-as-identity, Alias (mail nickname), and RemoteRoutingAddress. It had no way to manage secondary SMTP proxy addresses or GAL visibility. Adds: - An Email Addresses card listing all EmailAddresses proxy addresses, with a modal to add a new alias (Set-RemoteMailbox -EmailAddresses @{Add=...}) and a Remove button per alias (@{Remove=...}) - A Hidden from Address Lists card with a one-click toggle for HiddenFromAddressListsEnabled Both actions post back to /editremotemailbox with an Action field, and the page re-renders with the mailbox's current state afterwards. Factored the shared rendering logic (used by GET and by every POST action) into Get-RemoteMailboxEditPage to avoid duplicating the proxy-address/GAL-badge HTML building four times. --- Start-ExchangeRecipientAdminCenter.ps1 | 120 +++++++++++++++++++--- web/editremotemailbox.html | 131 ++++++++++++++++++++----- 2 files changed, 218 insertions(+), 33 deletions(-) diff --git a/Start-ExchangeRecipientAdminCenter.ps1 b/Start-ExchangeRecipientAdminCenter.ps1 index 9955ee4..cf9defb 100644 --- a/Start-ExchangeRecipientAdminCenter.ps1 +++ b/Start-ExchangeRecipientAdminCenter.ps1 @@ -35,6 +35,82 @@ $MIMEHASH = @{".avi" = "video/x-msvideo"; ".crt" = "application/x-x509-ca-cert"; $HTML_SUCCESS = "
{result}
" $HTML_WARN = "
{result}
" +function Get-RemoteMailboxEditPage { + # Renders editremotemailbox.html for a given mailbox identity. Shared by the + # GET (initial view) and POST (after an update) handlers below. + param( + [Parameter(Mandatory)][string]$Identity, + [string]$ResultHtml = "" + ) + + $Mailbox = Get-RemoteMailbox -Identity $Identity -ErrorAction SilentlyContinue + + if (-not $Mailbox) { + return "Remote mailbox '$($Identity)' not found" + } + + # Accepted domain list, used for the "add alias" domain picker + $HTMLROWS_AD = "" + foreach ($Item in (Get-AcceptedDomain)) { + $HTMLROWS_AD += "`n" + } + + # 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" + $RemoveBtn = " +
+ + + + +
" + } + $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" + } + + $HTMLRESPONSE = Get-Content -Path "$($BASEDIR)\editremotemailbox.html" + $HTMLRESPONSE = $HTMLRESPONSE.Replace("{DisplayName}", $Mailbox.DisplayName) + $HTMLRESPONSE = $HTMLRESPONSE.Replace("{PrimarySmtpAddress}", $Mailbox.PrimarySmtpAddress) + $HTMLRESPONSE = $HTMLRESPONSE.Replace("{Alias}", $Mailbox.Alias) + $HTMLRESPONSE = $HTMLRESPONSE.Replace("{RemoteRoutingAddress}", $Mailbox.RemoteRoutingAddress) + $HTMLRESPONSE = $HTMLRESPONSE.Replace("{id}", $Mailbox.PrimarySmtpAddress) + $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("", $ResultHtml) + return $HTMLRESPONSE +} + # Starting the powershell webserver "$(Get-Date -Format s) Starting Exchange Recipient Admin Webserver at: $($BINDING)" $LISTENER = New-Object System.Net.HttpListener @@ -137,14 +213,9 @@ try { "GET /editremotemailbox" { # Edit Remote Mailbox Section - $id = $REQUEST.Url.Query.Split("=")[1] - $remotemailbox = Get-RemoteMailbox -Identity $id - - $HTMLRESPONSE = (Get-Content -Path "$($BASEDIR)\editremotemailbox.html") - $HTMLRESPONSE = $HTMLRESPONSE.Replace("{DisplayName}", $remotemailbox.DisplayName) - $HTMLRESPONSE = $HTMLRESPONSE.Replace("{PrimarySmtpAddress}", $remotemailbox.PrimarySmtpAddress) - $HTMLRESPONSE = $HTMLRESPONSE.Replace("{Alias}", $remotemailbox.Alias) - $HTMLRESPONSE = $HTMLRESPONSE.Replace("{RemoteRoutingAddress}", $remotemailbox.RemoteRoutingAddress) + $id = [URI]::UnescapeDataString($REQUEST.Url.Query.TrimStart('?').Split("=")[1]) + + $HTMLRESPONSE = Get-RemoteMailboxEditPage -Identity $id break } @@ -161,15 +232,42 @@ try { $params[$key] = [System.Web.HttpUtility]::UrlDecode($value) } + # "id" is set by the alias/hidden mini-forms; the main properties + # form has no id field and keys off PrimarySmtpAddress instead. + $Identity = if ($params.ContainsKey('id')) { $params['id'] } else { $params['PrimarySmtpAddress'] } + try { - Set-RemoteMailbox -Identity $params['PrimarySmtpAddress'] -DisplayName $params['DisplayName'] -Alias $params['Alias'] -RemoteRoutingAddress $params['RemoteRoutingAddress'] - $HTML_RESULT = $HTML_SUCCESS.Replace("{result}", "Remote Mailbox updated successfully") + switch ($params['Action']) { + "addalias" { + $NewAlias = "$($params['alias_local'])@$($params['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 = $params['alias'] } + $HTML_RESULT = $HTML_SUCCESS.Replace("{result}", "Removed alias $($params['alias'])") + break + } + "togglehidden" { + $NewHidden = $params['hidden'] -eq 'true' + Set-RemoteMailbox -Identity $Identity -HiddenFromAddressListsEnabled $NewHidden + $HTML_RESULT = $HTML_SUCCESS.Replace("{result}", "Set 'Hidden from address lists' to $NewHidden") + break + } + default { + Set-RemoteMailbox -Identity $Identity -DisplayName $params['DisplayName'] -Alias $params['Alias'] -RemoteRoutingAddress $params['RemoteRoutingAddress'] + $HTML_RESULT = $HTML_SUCCESS.Replace("{result}", "Remote Mailbox updated successfully") + # DisplayName/Alias changes don't affect PrimarySmtpAddress, so it still identifies the mailbox below + $Identity = $params['PrimarySmtpAddress'] + } + } } catch { $HTML_RESULT = $HTML_WARN.Replace("{result}", $Error -join "
") } - $HTMLRESPONSE = (Get-Content -Path "$($BASEDIR)\remotemailboxes.html").Replace("", $HTML_RESULT) + $HTMLRESPONSE = Get-RemoteMailboxEditPage -Identity $Identity -ResultHtml $HTML_RESULT break } diff --git a/web/editremotemailbox.html b/web/editremotemailbox.html index ff838dd..8cfaa63 100644 --- a/web/editremotemailbox.html +++ b/web/editremotemailbox.html @@ -90,34 +90,121 @@

Edit Remote Mailbox

+ Back to Remote Mailboxes
-
-
- - + + +
+
Properties
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
-
- - +
+ +
+
Hidden from Address Lists
+
+ {hidden_status_text} +
+ + + + +
+
+
+ +
+
+ Email Addresses (Proxy Addresses) +
-
- - +
+ + + + + + + + + + + +
AddressType
-
- - +
+ + +
From 30138d0a779a79ed10ca240beca59623ffa0b421 Mon Sep 17 00:00:00 2001 From: SirTrek <4106300+SirTrek@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:08:01 -0600 Subject: [PATCH 2/7] 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. Same issue and fix as on main; ported here since dev has the identical request loop. --- Start-ExchangeRecipientAdminCenter.ps1 | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Start-ExchangeRecipientAdminCenter.ps1 b/Start-ExchangeRecipientAdminCenter.ps1 index cf9defb..96872be 100644 --- a/Start-ExchangeRecipientAdminCenter.ps1 +++ b/Start-ExchangeRecipientAdminCenter.ps1 @@ -124,6 +124,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 @@ -613,6 +614,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 4f6c64f9ca0558855a643b388ebedd7990607f53 Mon Sep 17 00:00:00 2001 From: SirTrek <4106300+SirTrek@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:47:30 -0600 Subject: [PATCH 3/7] 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 POST handler redirects to the Remote Mailboxes list (via a new Get-RemoteMailboxListPage helper) instead of re-rendering an edit page for an identity that's gone. Local-only change, not opened as a PR upstream. --- Start-ExchangeRecipientAdminCenter.ps1 | 85 +++++++++++++++++++++++++- web/editremotemailbox.html | 39 ++++++++++++ 2 files changed, 121 insertions(+), 3 deletions(-) diff --git a/Start-ExchangeRecipientAdminCenter.ps1 b/Start-ExchangeRecipientAdminCenter.ps1 index 96872be..86deeda 100644 --- a/Start-ExchangeRecipientAdminCenter.ps1 +++ b/Start-ExchangeRecipientAdminCenter.ps1 @@ -111,6 +111,59 @@ function Get-RemoteMailboxEditPage { return $HTMLRESPONSE } +function Get-RemoteMailboxListPage { + # Renders remotemailboxes.html. Used after disabling a Remote Mailbox, since + # the mailbox no longer exists to redirect back to an edit page for. + param( + [string]$ResultHtml = "" + ) + + $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" + $HTMLRESPONSE = $HTMLRESPONSE.Replace("", $HTMLROWS_MBX) + $HTMLRESPONSE = $HTMLRESPONSE.Replace("", $HTMLROWS_AD) + $HTMLRESPONSE = $HTMLRESPONSE.Replace("", $HTMLROWS_USERS) + $HTMLRESPONSE = $HTMLRESPONSE.Replace("", $HTMLROWS_RRA) + $HTMLRESPONSE = $HTMLRESPONSE.Replace("", $ResultHtml) + return $HTMLRESPONSE +} + # Starting the powershell webserver "$(Get-Date -Format s) Starting Exchange Recipient Admin Webserver at: $($BINDING)" $LISTENER = New-Object System.Net.HttpListener @@ -233,9 +286,10 @@ try { $params[$key] = [System.Web.HttpUtility]::UrlDecode($value) } - # "id" is set by the alias/hidden mini-forms; the main properties - # form has no id field and keys off PrimarySmtpAddress instead. + # "id" is set by the alias/hidden/disable mini-forms; the main + # properties form has no id field and keys off PrimarySmtpAddress. $Identity = if ($params.ContainsKey('id')) { $params['id'] } else { $params['PrimarySmtpAddress'] } + $Disabled = $false try { switch ($params['Action']) { @@ -256,6 +310,24 @@ try { $HTML_RESULT = $HTML_SUCCESS.Replace("{result}", "Set 'Hidden from address lists' to $NewHidden") 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 ($params['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") + $Disabled = $true + } + break + } default { Set-RemoteMailbox -Identity $Identity -DisplayName $params['DisplayName'] -Alias $params['Alias'] -RemoteRoutingAddress $params['RemoteRoutingAddress'] $HTML_RESULT = $HTML_SUCCESS.Replace("{result}", "Remote Mailbox updated successfully") @@ -268,7 +340,14 @@ try { $HTML_RESULT = $HTML_WARN.Replace("{result}", $Error -join "
") } - $HTMLRESPONSE = Get-RemoteMailboxEditPage -Identity $Identity -ResultHtml $HTML_RESULT + # A disabled mailbox no longer exists to render an edit page for - + # send the user back to the list instead. + if ($Disabled) { + $HTMLRESPONSE = Get-RemoteMailboxListPage -ResultHtml $HTML_RESULT + } + else { + $HTMLRESPONSE = Get-RemoteMailboxEditPage -Identity $Identity -ResultHtml $HTML_RESULT + } break } diff --git a/web/editremotemailbox.html b/web/editremotemailbox.html index 8cfaa63..ea4f43e 100644 --- a/web/editremotemailbox.html +++ b/web/editremotemailbox.html @@ -205,12 +205,51 @@
+
+
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.

+
+ + +
+ + +
+ +
+
+
+ From c93f7f2fe64aa4d1facc081f88a7abb8607511bb Mon Sep 17 00:00:00 2001 From: SirTrek <4106300+SirTrek@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:53:49 -0600 Subject: [PATCH 4/7] Fix disable-mailbox confirm button never enabling PowerShell's -eq/-ne string comparison is case-insensitive by default, but the JS confirm-text check used strict !== (case-sensitive, no trim). If the AD-stored PrimarySmtpAddress casing differed at all from what the user typed, or there was stray whitespace, the button stayed disabled even though the server-side check would have accepted it. Trim and lowercase both sides of the JS comparison so it matches the server's more forgiving behavior. --- web/editremotemailbox.html | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/web/editremotemailbox.html b/web/editremotemailbox.html index ea4f43e..8bd5e2f 100644 --- a/web/editremotemailbox.html +++ b/web/editremotemailbox.html @@ -235,17 +235,21 @@ feather.replace() (function () { - var expected = "{PrimarySmtpAddress}"; + var expected = "{PrimarySmtpAddress}".trim().toLowerCase(); var input = document.getElementById('confirmAddress'); var button = document.getElementById('disableButton'); var form = document.getElementById('disableForm'); + function matches() { + return input.value.trim().toLowerCase() === expected; + } + input.addEventListener('input', function () { - button.disabled = (input.value !== expected); + button.disabled = !matches(); }); form.addEventListener('submit', function (e) { - if (input.value !== expected || !confirm('This will disable the Remote Mailbox for ' + expected + '. Continue?')) { + if (!matches() || !confirm('This will disable the Remote Mailbox for ' + expected + '. Continue?')) { e.preventDefault(); } }); From 9da22cfa989e16e5e0120cfd83b6d95790254682 Mon Sep 17 00:00:00 2001 From: SirTrek <4106300+SirTrek@users.noreply.github.com> Date: Thu, 9 Jul 2026 09:42:05 -0600 Subject: [PATCH 5/7] Guard feather.replace() so it can't block the confirm-button script dev's feather-icons diff --git a/web/contacts.html b/web/contacts.html index 412da30..0c29adc 100644 --- a/web/contacts.html +++ b/web/contacts.html @@ -159,6 +159,7 @@ + diff --git a/web/distributiongroups.html b/web/distributiongroups.html index 47ea399..82edc6b 100644 --- a/web/distributiongroups.html +++ b/web/distributiongroups.html @@ -236,6 +236,7 @@ + diff --git a/web/table-tools.js b/web/table-tools.js new file mode 100644 index 0000000..3e23932 --- /dev/null +++ b/web/table-tools.js @@ -0,0 +1,78 @@ +// Adds a client-side search box and clickable sortable column headers to +// every on the page. Purely client-side - it only +// reorders/hides rows already rendered by the server, no data is refetched. +(function () { + function cellText(cell) { + return cell.textContent.trim().toLowerCase(); + } + + function sortTable(table, colIndex, ascending) { + var tbody = table.tBodies[0]; + var rows = Array.prototype.slice.call(tbody.rows); + rows.sort(function (a, b) { + var av = cellText(a.cells[colIndex]); + var bv = cellText(b.cells[colIndex]); + if (av < bv) return ascending ? -1 : 1; + if (av > bv) return ascending ? 1 : -1; + return 0; + }); + rows.forEach(function (row) { tbody.appendChild(row); }); + } + + function addSortHandlers(table) { + var headerRow = table.tHead && table.tHead.rows[0]; + if (!headerRow) return; + + Array.prototype.forEach.call(headerRow.cells, function (th, index) { + if (!th.textContent.trim()) return; // skip empty/action columns + + th.style.cursor = 'pointer'; + th.dataset.sortDir = ''; + + th.addEventListener('click', function () { + var ascending = th.dataset.sortDir !== 'asc'; + + Array.prototype.forEach.call(headerRow.cells, function (sib) { + sib.dataset.sortDir = ''; + sib.textContent = sib.textContent.replace(/ [▲▼]$/, ''); + }); + + th.dataset.sortDir = ascending ? 'asc' : 'desc'; + th.textContent = th.textContent.replace(/ [▲▼]$/, '') + (ascending ? ' ▲' : ' ▼'); + + sortTable(table, index, ascending); + }); + }); + } + + function addSearchBox(table) { + var wrapper = document.createElement('div'); + wrapper.className = 'mb-2'; + + var input = document.createElement('input'); + input.type = 'search'; + input.className = 'form-control'; + input.placeholder = 'Search...'; + wrapper.appendChild(input); + + table.parentNode.insertBefore(wrapper, table); + + input.addEventListener('input', function () { + var query = input.value.trim().toLowerCase(); + var rows = table.tBodies[0].rows; + Array.prototype.forEach.call(rows, function (row) { + var text = row.textContent.toLowerCase(); + row.style.display = text.indexOf(query) === -1 ? 'none' : ''; + }); + }); + } + + document.addEventListener('DOMContentLoaded', function () { + var tables = document.querySelectorAll('table.table'); + Array.prototype.forEach.call(tables, function (table) { + if (!table.tHead || !table.tBodies.length) return; + addSearchBox(table); + addSortHandlers(table); + }); + }); +})(); From 5d8a7d4d1f9d3b582c18825511a7cb942e4a2d28 Mon Sep 17 00:00:00 2001 From: SirTrek <4106300+SirTrek@users.noreply.github.com> Date: Thu, 9 Jul 2026 10:46:49 -0600 Subject: [PATCH 7/7] Add desktop shortcut launcher Launch-ExchangeRecipientAdmin.bat self-elevates via UAC (the Exchange Management Tools assemblies require admin rights to load, which a plain double-clicked shortcut doesn't have by default) and resolves its own folder via %~dp0, so it works regardless of where this project is placed or which user runs it. Create-Shortcut.ps1 is a one-time setup script that creates a Desktop .lnk pointing at the .bat, using the repo's icon if present and falling back to the stock PowerShell icon otherwise (dev's asset rewrite removed images/favicon.ico, so that fallback is needed here). Both are fully dynamic (no hardcoded paths/usernames) - safe to publish, verified against the actual file contents before committing. --- Create-Shortcut.ps1 | 29 +++++++++++++++++++++++++++++ Launch-ExchangeRecipientAdmin.bat | 2 ++ 2 files changed, 31 insertions(+) create mode 100644 Create-Shortcut.ps1 create mode 100644 Launch-ExchangeRecipientAdmin.bat diff --git a/Create-Shortcut.ps1 b/Create-Shortcut.ps1 new file mode 100644 index 0000000..3948666 --- /dev/null +++ b/Create-Shortcut.ps1 @@ -0,0 +1,29 @@ +<# +.Synopsis +Creates a Desktop shortcut for launching Exchange Recipient Admin Center. +.Description +Run this once from wherever this project's files actually live. It resolves +its own folder and the current user's Desktop dynamically, so the same +script works unmodified for any user/location - no hardcoded paths. +#> + +$targetFolder = Split-Path -Parent $MyInvocation.MyCommand.Path +$batPath = Join-Path $targetFolder "Launch-ExchangeRecipientAdmin.bat" +$iconPath = Join-Path $targetFolder "images\favicon.ico" +$desktop = [Environment]::GetFolderPath('Desktop') +$shortcutPath = Join-Path $desktop "Exchange Recipient Admin Center.lnk" + +$shell = New-Object -ComObject WScript.Shell +$shortcut = $shell.CreateShortcut($shortcutPath) +$shortcut.TargetPath = $batPath +$shortcut.WorkingDirectory = $targetFolder +if (Test-Path $iconPath) { + $shortcut.IconLocation = $iconPath +} +else { + $shortcut.IconLocation = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe,0" +} +$shortcut.Description = "Exchange Recipient Admin Center" +$shortcut.Save() + +Write-Host "Shortcut created at: $shortcutPath" diff --git a/Launch-ExchangeRecipientAdmin.bat b/Launch-ExchangeRecipientAdmin.bat new file mode 100644 index 0000000..2bd8882 --- /dev/null +++ b/Launch-ExchangeRecipientAdmin.bat @@ -0,0 +1,2 @@ +@echo off +powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "Start-Process powershell.exe -Verb RunAs -ArgumentList '-ExecutionPolicy Bypass -File \"%~dp0Start-ExchangeRecipientAdminCenter.ps1\"'"